diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index c85fec01528..6bde1a93a7f 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -48,7 +48,8 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri # JPA Messages -ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request. +ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.externalReferenceNotAllowed=Resource contains external reference to URL "{0}" but this server is not configured to allow external references +ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}" ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlMultipleMatches=Invalid match URL "{0}" - Multiple resources match this search diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 82746b00a35..ef3430b400a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -243,14 +243,13 @@ public abstract class BaseHapiFhirDao implements IDao { } @SuppressWarnings("unchecked") - protected Set extractResourceLinks(ResourceTable theEntity, IBaseResource theResource) { - Set retVal = new HashSet(); + protected void extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Set theLinks) { /* * For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing.. */ if (theResource instanceof IBaseBundle) { - return retVal; + return; } RuntimeResourceDefinition def = getContext().getResourceDefinition(theResource); @@ -274,7 +273,6 @@ public abstract class BaseHapiFhirDao implements IDao { for (PathAndRef nextPathAndRef : refs) { Object nextObject = nextPathAndRef.getRef(); - ResourceLink nextEntity; IIdType nextId; if (nextObject instanceof IBaseReference) { IBaseReference nextValue = (IBaseReference) nextObject; @@ -304,6 +302,7 @@ public abstract class BaseHapiFhirDao implements IDao { } } + String baseUrl = nextId.getBaseUrl(); String typeString = nextId.getResourceType(); if (isBlank(typeString)) { throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue()); @@ -316,6 +315,18 @@ public abstract class BaseHapiFhirDao implements IDao { "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); } + if (isNotBlank(baseUrl)) { + if (!getConfig().getTreatBaseUrlsAsLocal().contains(baseUrl) && !getConfig().isAllowExternalReferences()) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue()); + throw new InvalidRequestException(msg); + } else { + if (theLinks.add(new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId))) { + ourLog.info("Indexing remote resource reference URL: {}", nextId); + } + continue; + } + } + Class type = resourceDefinition.getImplementingClass(); String id = nextId.getIdPart(); if (StringUtils.isBlank(id)) { @@ -357,45 +368,13 @@ public abstract class BaseHapiFhirDao implements IDao { continue; } - // /* - // * Is the target type an allowable type of resource for the path where it is referenced? - // */ - // - // if (allowedTypesInField == null) { - // BaseRuntimeChildDefinition childDef = getContext().newTerser().getDefinition(theResource.getClass(), nextPathAndRef.getPath()); - // if (childDef instanceof RuntimeChildResourceDefinition) { - // RuntimeChildResourceDefinition resRefDef = (RuntimeChildResourceDefinition) childDef; - // allowedTypesInField = resRefDef.getResourceTypes(); - // } else { - // allowedTypesInField = new ArrayList>(); - // allowedTypesInField.add(IBaseResource.class); - // } - // } - // - // boolean acceptableLink = false; - // for (Class next : allowedTypesInField) { - // if (next.isAssignableFrom(targetResourceDef.getImplementingClass())) { - // acceptableLink = true; - // break; - // } - // } - // - // if (!acceptableLink) { - // throw new UnprocessableEntityException( - // "Invalid reference found at path '" + nextPathAndRef.getPath() + "'. Resource type '" + targetResourceDef.getName() + "' is not valid for this path"); - // } - - nextEntity = new ResourceLink(nextPathAndRef.getPath(), theEntity, target); - if (nextEntity != null) { - retVal.add(nextEntity); - } + theLinks.add(new ResourceLink(nextPathAndRef.getPath(), theEntity, target)); } } - theEntity.setHasLinks(retVal.size() > 0); + theEntity.setHasLinks(theLinks.size() > 0); - return retVal; } protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { @@ -1268,7 +1247,8 @@ public abstract class BaseHapiFhirDao implements IDao { } } - links = extractResourceLinks(theEntity, theResource); + links = new HashSet(); + extractResourceLinks(theEntity, theResource, links); /* * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 43a5c563e1d..783c9d76aa8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -88,6 +88,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ObjectUtil; +import ca.uhn.fhir.util.ResourceReferenceInfo; @Transactional(propagation = Propagation.REQUIRED) public abstract class BaseHapiFhirResourceDao extends BaseHapiFhirDao implements IFhirResourceDao { @@ -708,6 +709,24 @@ public abstract class BaseHapiFhirResourceDao extends B throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart())); } } + + /* + * Replace absolute references with relative ones if configured to + * do so + */ + if (getConfig().getTreatBaseUrlsAsLocal().isEmpty() == false) { + FhirTerser t = getContext().newTerser(); + List refs = t.getAllResourceReferences(theResource); + for (ResourceReferenceInfo nextRef : refs) { + IIdType refId = nextRef.getResourceReference().getReferenceElement(); + if (refId != null && refId.hasBaseUrl()) { + if (getConfig().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) { + IIdType newRefId = refId.toUnqualified(); + nextRef.getResourceReference().setReference(newRefId.getValue()); + } + } + } + } } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 33c26e62545..c096b17688d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -3,8 +3,11 @@ package ca.uhn.fhir.jpa.dao; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; @@ -34,19 +37,24 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; public class DaoConfig { + // *** + // update setter javadoc if default changes + // *** + private boolean myAllowExternalReferences = false; + // *** // update setter javadoc if default changes // *** private boolean myAllowInlineMatchUrlReferences = false; - private boolean myAllowMultipleDelete; - + private boolean myAllowMultipleDelete; // *** // update setter javadoc if default changes // *** private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR; private int myHardSearchLimit = 1000; private int myHardTagListLimit = 1000; + private int myIncludeLimit = 2000; // *** @@ -55,39 +63,49 @@ public class DaoConfig { private boolean myIndexContainedResources = true; private List myInterceptors; - // *** // update setter javadoc if default changes // *** private int myMaximumExpansionSize = 5000; - + private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC; + private boolean mySchedulingDisabled; + private boolean mySubscriptionEnabled; + private long mySubscriptionPollDelay = 1000; + private Long mySubscriptionPurgeInactiveAfterMillis; + private Set myTreatBaseUrlsAsLocal = new HashSet(); /** - * Search results are stored in the database so that they can be paged through. After this - * number of milliseconds, they will be deleted from the database. Defaults to 1 hour. + * Sets the number of milliseconds that search results for a given client search + * should be preserved before being purged from the database. + *

+ * Search results are stored in the database so that they can be paged over multiple + * requests. After this + * number of milliseconds, they will be deleted from the database, and any paging links + * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. + *

* * @since 1.5 */ public long getExpireSearchResultsAfterMillis() { return myExpireSearchResultsAfterMillis; } + /** - * See {@link #setIncludeLimit(int)} + * Gets the maximum number of results to return in a GetTags query (DSTU1 only) */ - public int getHardSearchLimit() { - return myHardSearchLimit; - } public int getHardTagListLimit() { return myHardTagListLimit; } + public int getIncludeLimit() { return myIncludeLimit; } + /** * Returns the interceptors which will be notified of operations. * @@ -111,11 +129,51 @@ public class DaoConfig { public long getSubscriptionPollDelay() { return mySubscriptionPollDelay; } - public Long getSubscriptionPurgeInactiveAfterMillis() { return mySubscriptionPurgeInactiveAfterMillis; } - + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

+ * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

+ */ + public Set getTreatBaseUrlsAsLocal() { + return myTreatBaseUrlsAsLocal; + } + /** + * If set to true (default is false) the server will allow + * resources to have references to external servers. For example if this server is + * running at http://example.com/fhir and this setting is set to + * true the server will allow a Patient resource to be saved with a + * Patient.organization value of http://foo.com/Organization/1. + *

+ * Under the default behaviour if this value has not been changed, the above + * resource would be rejected by the server because it requires all references + * to be resolvable on the local server. + *

+ *

+ * Note that external references will be indexed by the server and may be searched + * (e.g. Patient:organization), but + * chained searches (e.g. Patient:organization.name) will not work across + * these references. + *

+ *

+ * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value + * is set to true + *

+ * + * @see #setTreatBaseUrlsAsLocal(Set) + * @see #setAllowExternalReferences(boolean) + */ + public boolean isAllowExternalReferences() { + return myAllowExternalReferences; + } /** * @see #setAllowInlineMatchUrlReferences(boolean) */ @@ -146,6 +204,35 @@ public class DaoConfig { return mySubscriptionEnabled; } + /** + * If set to true (default is false) the server will allow + * resources to have references to external servers. For example if this server is + * running at http://example.com/fhir and this setting is set to + * true the server will allow a Patient resource to be saved with a + * Patient.organization value of http://foo.com/Organization/1. + *

+ * Under the default behaviour if this value has not been changed, the above + * resource would be rejected by the server because it requires all references + * to be resolvable on the local server. + *

+ *

+ * Note that external references will be indexed by the server and may be searched + * (e.g. Patient:organization), but + * chained searches (e.g. Patient:organization.name) will not work across + * these references. + *

+ *

+ * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value + * is set to true + *

+ * + * @see #setTreatBaseUrlsAsLocal(Set) + * @see #setAllowExternalReferences(boolean) + */ + public void setAllowExternalReferences(boolean theAllowExternalReferences) { + myAllowExternalReferences = theAllowExternalReferences; + } + /** * Should references containing match URLs be resolved and replaced in create and update operations. For * example, if this property is set to true and a resource is created containing a reference @@ -165,8 +252,14 @@ public class DaoConfig { } /** - * Search results are stored in the database so that they can be paged through. After this - * number of milliseconds, they will be deleted from the database. Defaults to 1 hour. + * Sets the number of milliseconds that search results for a given client search + * should be preserved before being purged from the database. + *

+ * Search results are stored in the database so that they can be paged over multiple + * requests. After this + * number of milliseconds, they will be deleted from the database, and any paging links + * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. + *

* * @since 1.5 */ @@ -178,6 +271,9 @@ public class DaoConfig { myHardSearchLimit = theHardSearchLimit; } + /** + * Gets the maximum number of results to return in a GetTags query (DSTU1 only) + */ public void setHardTagListLimit(int theHardTagListLimit) { myHardTagListLimit = theHardTagListLimit; } @@ -246,8 +342,8 @@ public class DaoConfig { } /** - * Does this server support subscription? If set to true, the server will enable the subscription monitoring mode, - * which adds a bit of overhead. Note that if this is enabled, you must also include Spring task scanning to your XML + * If set to true, the server will enable support for subscriptions. Subscriptions + * will by default be handled via a polling task. Note that if this is enabled, you must also include Spring task scanning to your XML * config for the scheduled tasks used by the subscription module. */ public void setSubscriptionEnabled(boolean theSubscriptionEnabled) { @@ -269,4 +365,29 @@ public class DaoConfig { setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND); } + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

+ * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

+ * + * @param theTreatBaseUrlsAsLocal The set of base URLs. May be null, which + * means no references will be treated as external + */ + public void setTreatBaseUrlsAsLocal(Set theTreatBaseUrlsAsLocal) { + HashSet treatBaseUrlsAsLocal = new HashSet(); + for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet())) { + while (next.endsWith("/")) { + next = next.substring(0, next.length() - 1); + } + treatBaseUrlsAsLocal.add(next); + } + myTreatBaseUrlsAsLocal = treatBaseUrlsAsLocal; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index afef8667660..eee30fba545 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -67,6 +67,7 @@ import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; +import com.google.common.collect.HashBasedTable; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @@ -577,9 +578,26 @@ public class SearchBuilder { ReferenceParam ref = (ReferenceParam) params; if (isBlank(ref.getChain())) { - String resourceId = ref.getValueAsQueryToken(myContext); - IIdType dt = new IdDt(resourceId); - List targetPid = myCallingDao.translateForcedIdToPids(dt); + IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null); + + if (dt.hasBaseUrl()) { + if (myCallingDao.getConfig().getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) { + dt = dt.toUnqualified(); + } else { + ourLog.debug("Searching for resource link with target URL: {}", dt.getValue()); + Predicate eq = builder.equal(from.get("myTargetResourceUrl"), dt.getValue()); + codePredicates.add(eq); + continue; + } + } + + List targetPid; + try { + targetPid = myCallingDao.translateForcedIdToPids(dt); + } catch (ResourceNotFoundException e) { + doSetPids(new ArrayList()); + return; + } for (Long next : targetPid) { ourLog.debug("Searching for resource link with target PID: {}", next); Predicate eq = builder.equal(from.get("myTargetResourcePid"), next); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java index 489e4d64c63..de0a3b359ba 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java @@ -38,6 +38,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.hibernate.annotations.ColumnDefault; import org.hibernate.search.annotations.Field; +import org.hl7.fhir.instance.model.api.IIdType; @Entity @Table(name = "HFJ_RES_LINK" , indexes= { @@ -70,11 +71,11 @@ public class ResourceLink implements Serializable { @Field() private String mySourceResourceType; - @ManyToOne(optional = false, fetch=FetchType.LAZY) - @JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = false) + @ManyToOne(optional = true, fetch=FetchType.LAZY) + @JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = true) private ResourceTable myTargetResource; - @Column(name = "TARGET_RESOURCE_ID", insertable = false, updatable = false, nullable = false) + @Column(name = "TARGET_RESOURCE_ID", insertable = false, updatable = false, nullable = true) @Field() private Long myTargetResourcePid; @@ -83,6 +84,10 @@ public class ResourceLink implements Serializable { @Field() private String myTargetResourceType; + @Column(name = "TARGET_RESOURCE_URL", length=200, nullable = true) + @Field() + private String myTargetResourceUrl; + public ResourceLink() { super(); } @@ -94,6 +99,13 @@ public class ResourceLink implements Serializable { setTargetResource(theTargetResource); } + public ResourceLink(String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl) { + super(); + setSourcePath(theSourcePath); + setSourceResource(theSourceResource); + setTargetResourceUrl(theTargetResourceUrl); + } + @Override public boolean equals(Object theObj) { if (this == theObj) { @@ -110,6 +122,7 @@ public class ResourceLink implements Serializable { b.append(mySourcePath, obj.mySourcePath); b.append(mySourceResource, obj.mySourceResource); b.append(myTargetResourcePid, obj.myTargetResourcePid); + b.append(myTargetResourceUrl, obj.myTargetResourceUrl); return b.isEquals(); } @@ -133,12 +146,17 @@ public class ResourceLink implements Serializable { return myTargetResourcePid; } + public String getTargetResourceUrl() { + return myTargetResourceUrl; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); b.append(mySourcePath); b.append(mySourceResource); b.append(myTargetResourcePid); + b.append(myTargetResourceUrl); return b.toHashCode(); } @@ -159,6 +177,15 @@ public class ResourceLink implements Serializable { myTargetResourceType = theTargetResource.getResourceType(); } + public void setTargetResourceUrl(IIdType theTargetResourceUrl) { + Validate.isTrue(theTargetResourceUrl.hasBaseUrl()); + Validate.isTrue(theTargetResourceUrl.hasResourceType()); + Validate.isTrue(theTargetResourceUrl.hasIdPart()); + + myTargetResourceType = theTargetResourceUrl.getResourceType(); + myTargetResourceUrl = theTargetResourceUrl.getValue(); + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -166,6 +193,7 @@ public class ResourceLink implements Serializable { b.append("path=").append(mySourcePath); b.append(", src=").append(mySourceResourcePid); b.append(", target=").append(myTargetResourcePid); + b.append(", targetUrl=").append(myTargetResourceUrl); b.append("]"); return b.toString(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java index 51d19b7b54f..8a409eb21b0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java @@ -149,8 +149,8 @@ public class ResourceTable extends BaseHasResource implements Serializable { private boolean myHasLinks; @Id - @SequenceGenerator(name="SEQ_RESOURCE_ID", sequenceName="SEQ_RESOURCE_ID") - @GeneratedValue(strategy = GenerationType.AUTO, generator="SEQ_RESOURCE_ID") + @SequenceGenerator(name = "SEQ_RESOURCE_ID", sequenceName = "SEQ_RESOURCE_ID") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESOURCE_ID") @Column(name = "RES_ID") private Long myId; @@ -187,7 +187,7 @@ public class ResourceTable extends BaseHasResource implements Serializable { @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) private Collection myParamsNumber; - + @Column(name = "SP_NUMBER_PRESENT") private boolean myParamsNumberPopulated; @@ -540,14 +540,14 @@ public class ResourceTable extends BaseHasResource implements Serializable { retVal.setForcedId(getForcedId()); retVal.getTags().clear(); - + retVal.setHasTags(isHasTags()); if (isHasTags()) { for (ResourceTag next : getTags()) { retVal.addTag(next); } } - + return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java new file mode 100644 index 00000000000..dec1c02991f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java @@ -0,0 +1,158 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.util.HashSet; +import java.util.Set; + +import org.hl7.fhir.dstu3.model.Organization; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.TestUtil; + +public class FhirResourceDaoDstu3ExternalReferenceTest extends BaseJpaDstu3Test { + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @Before + @After + public void resetDefaultBehaviour() { + // Reset to default + myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); + myDaoConfig.setTreatBaseUrlsAsLocal(null); + } + + @Test + public void testInternalReferenceBlockedByDefault() { + Patient p = new Patient(); + p.getManagingOrganization().setReference("Organization/FOO"); + try { + myPatientDao.create(p, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource Organization/FOO not found, specified in path: Patient.managingOrganization", e.getMessage()); + } + } + + @Test + public void testExternalReferenceBlockedByDefault() { + Organization org = new Organization(); + org.setId("FOO"); + org.setName("Org Name"); + myOrganizationDao.update(org, mySrd); + + Patient p = new Patient(); + p.getManagingOrganization().setReference("http://example.com/base/Organization/FOO"); + try { + myPatientDao.create(p, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource contains external reference to URL \"http://example.com/base/Organization/FOO\" but this server is not configured to allow external references", e.getMessage()); + } + } + + @Test + public void testExternalReferenceAllowed() { + Organization org = new Organization(); + org.setId("FOO"); + org.setName("Org Name"); + myOrganizationDao.update(org, mySrd); + + myDaoConfig.setAllowExternalReferences(true); + + Patient p = new Patient(); + p.getManagingOrganization().setReference("http://example.com/base/Organization/FOO"); + IIdType pid = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_ORGANIZATION, new ReferenceParam("http://example.com/base/Organization/FOO")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(pid.getValue())); + + map = new SearchParameterMap(); + map.add(Patient.SP_ORGANIZATION, new ReferenceParam("http://example2.com/base/Organization/FOO")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); + } + + @Test + public void testExternalReferenceReplaced() { + Organization org = new Organization(); + org.setId("FOO"); + org.setName("Org Name"); + org.getPartOf().setDisplay("Parent"); // <-- no reference, make sure this works + myOrganizationDao.update(org, mySrd); + + Set urls = new HashSet(); + urls.add("http://example.com/base/"); + myDaoConfig.setTreatBaseUrlsAsLocal(urls); + + Patient p = new Patient(); + p.getManagingOrganization().setReference("http://example.com/base/Organization/FOO"); + IIdType pid = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + p = myPatientDao.read(pid, mySrd); + assertEquals("Organization/FOO", p.getManagingOrganization().getReference()); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add(Patient.SP_ORGANIZATION, new ReferenceParam("http://example.com/base/Organization/FOO")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(pid.getValue())); + } + + @Test + public void testSearchForInvalidLocalReference() { + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add(Patient.SP_ORGANIZATION, new ReferenceParam("Organization/FOO")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); + + map = new SearchParameterMap(); + map.add(Patient.SP_ORGANIZATION, new ReferenceParam("Organization/9999999999")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); + } + + @Test + public void testExternalReferenceReplacedWrongDoesntMatch() { + Organization org = new Organization(); + org.setId("FOO"); + org.setName("Org Name"); + org.getPartOf().setDisplay("Parent"); // <-- no reference, make sure this works + myOrganizationDao.update(org, mySrd); + + Set urls = new HashSet(); + urls.add("http://example.com/base/"); + myDaoConfig.setTreatBaseUrlsAsLocal(urls); + + Patient p = new Patient(); + p.getManagingOrganization().setReference("http://example.com/base/Organization/FOO"); + IIdType pid = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + p = myPatientDao.read(pid, mySrd); + assertEquals("Organization/FOO", p.getManagingOrganization().getReference()); + + SearchParameterMap map; + + // Different base + map = new SearchParameterMap(); + map.add(Patient.SP_ORGANIZATION, new ReferenceParam("http://foo.com/base/Organization/FOO")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); + } + +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu1Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu1Config.java index 731b4068dc9..8b8896627c2 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu1Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu1Config.java @@ -43,6 +43,9 @@ public class TestDstu1Config extends BaseJavaConfigDstu1 { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(false); retVal.setAllowMultipleDelete(false); + retVal.setAllowExternalReferences(true); + retVal.getTreatBaseUrlsAsLocal().add("http://fhirtest.uhn.ca/baseDstu1"); + retVal.getTreatBaseUrlsAsLocal().add("https://fhirtest.uhn.ca/baseDstu1"); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index 91cf7ae519a..69380db0ce5 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -54,6 +54,9 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); retVal.setAllowMultipleDelete(true); retVal.setAllowInlineMatchUrlReferences(true); + retVal.setAllowExternalReferences(true); + retVal.getTreatBaseUrlsAsLocal().add("http://fhirtest.uhn.ca/baseDstu2"); + retVal.getTreatBaseUrlsAsLocal().add("https://fhirtest.uhn.ca/baseDstu2"); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index 7fba189a175..c41e4c64935 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -51,6 +51,9 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); retVal.setAllowMultipleDelete(true); retVal.setAllowInlineMatchUrlReferences(true); + retVal.setAllowExternalReferences(true); + retVal.getTreatBaseUrlsAsLocal().add("http://fhirtest.uhn.ca/baseDstu3"); + retVal.getTreatBaseUrlsAsLocal().add("https://fhirtest.uhn.ca/baseDstu3"); return retVal; } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 62c38fef48c..a8994289068 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -197,6 +197,12 @@ search parameters that did not start with an underscore. E.g. "Patient?_id=1" failed even though this is a valid conditional reference. + + JPA server can now be configured to allow external references (i.e. references that + point to resources on other servers). See + JPA Documentation]]> for information on + how to use this. Thanks to Naminder Soorma for the suggestion! + diff --git a/src/site/xdoc/doc_jpa.xml b/src/site/xdoc/doc_jpa.xml index 179d82cb449..ff7e6e409ee 100644 --- a/src/site/xdoc/doc_jpa.xml +++ b/src/site/xdoc/doc_jpa.xml @@ -147,6 +147,89 @@ $ mvn install]]> Configures the database connection settings + + + +
+ +

+ The Spring confguration contains a definition for a bean called daoConfig, + which will look something like the following: +

+
+ +

+ You can use this method to change various configuration settings on the DaoConfig bean + which define the way that the JPA server will behave. + See the DaoConfig JavaDoc + for information about the available settings. +

+ + + +

+ Clients may sometimes post resources to your server that contain + absolute resource references. For example, consider the following resource: +

+

+   
+   
+      
+      
+      
+   
+   
+      
+   
+]]>
+ +

+ By default, the server will reject this reference, as only + local references are permitted by the server. This can be changed + however. +

+

+ If you want the server to recognize that this URL is actually a local + reference (i.e. because the server will be deployed to the base URL + http://example.com/fhir/) you can + configure the server to recognize this URL via the following DaoConfig + setting: +

+
+ +

+ On the other hand, if you want the server to be configurable to + allow remote references, you can set this with the confguration below. + Using the setAllowInlineMatchUrlReferences means that + it will be possible to search for references that refer to these + external references. +

+ +
+
+