Fix #363 - Allow remote references in JPA resources

This commit is contained in:
jamesagnew 2016-05-21 12:58:04 -04:00
parent b03bea3a58
commit 3d8776f6ed
13 changed files with 490 additions and 67 deletions

View File

@ -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

View File

@ -243,14 +243,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
}
@SuppressWarnings("unchecked")
protected Set<ResourceLink> extractResourceLinks(ResourceTable theEntity, IBaseResource theResource) {
Set<ResourceLink> retVal = new HashSet<ResourceLink>();
protected void extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Set<ResourceLink> 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<T extends IBaseResource> 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<T extends IBaseResource> 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<T extends IBaseResource> 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<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) {
@ -357,45 +368,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> 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<Class<? extends IBaseResource>>();
// allowedTypesInField.add(IBaseResource.class);
// }
// }
//
// boolean acceptableLink = false;
// for (Class<? extends IBaseResource> 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<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
@ -1268,7 +1247,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
}
}
links = extractResourceLinks(theEntity, theResource);
links = new HashSet<ResourceLink>();
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

View File

@ -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<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
@ -708,6 +709,24 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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<ResourceReferenceInfo> 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

View File

@ -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;
// ***
// 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<IServerInterceptor> 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<String> myTreatBaseUrlsAsLocal = new HashSet<String>();
/**
* 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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* For example, if the set contains the value <code>http://example.com/base/</code>
* and a resource is submitted to the server that contains a reference to
* <code>http://example.com/base/Patient/1</code>, the server will automatically
* convert this reference to <code>Patient/1</code>
* </p>
*/
public Set<String> getTreatBaseUrlsAsLocal() {
return myTreatBaseUrlsAsLocal;
}
/**
* If set to <code>true</code> (default is <code>false</code>) the server will allow
* resources to have references to external servers. For example if this server is
* running at <code>http://example.com/fhir</code> and this setting is set to
* <code>true</code> the server will allow a Patient resource to be saved with a
* Patient.organization value of <code>http://foo.com/Organization/1</code>.
* <p>
* 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.
* </p>
* <p>
* Note that external references will be indexed by the server and may be searched
* (e.g. <code>Patient:organization</code>), but
* chained searches (e.g. <code>Patient:organization.name</code>) will not work across
* these references.
* </p>
* <p>
* It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value
* is set to <code>true</code>
* </p>
*
* @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 <code>true</code> (default is <code>false</code>) the server will allow
* resources to have references to external servers. For example if this server is
* running at <code>http://example.com/fhir</code> and this setting is set to
* <code>true</code> the server will allow a Patient resource to be saved with a
* Patient.organization value of <code>http://foo.com/Organization/1</code>.
* <p>
* 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.
* </p>
* <p>
* Note that external references will be indexed by the server and may be searched
* (e.g. <code>Patient:organization</code>), but
* chained searches (e.g. <code>Patient:organization.name</code>) will not work across
* these references.
* </p>
* <p>
* It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value
* is set to <code>true</code>
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* For example, if the set contains the value <code>http://example.com/base/</code>
* and a resource is submitted to the server that contains a reference to
* <code>http://example.com/base/Patient/1</code>, the server will automatically
* convert this reference to <code>Patient/1</code>
* </p>
*
* @param theTreatBaseUrlsAsLocal The set of base URLs. May be <code>null</code>, which
* means no references will be treated as external
*/
public void setTreatBaseUrlsAsLocal(Set<String> theTreatBaseUrlsAsLocal) {
HashSet<String> treatBaseUrlsAsLocal = new HashSet<String>();
for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet<String>())) {
while (next.endsWith("/")) {
next = next.substring(0, next.length() - 1);
}
treatBaseUrlsAsLocal.add(next);
}
myTreatBaseUrlsAsLocal = treatBaseUrlsAsLocal;
}
}

View File

@ -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<Long> 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<Long> targetPid;
try {
targetPid = myCallingDao.translateForcedIdToPids(dt);
} catch (ResourceNotFoundException e) {
doSetPids(new ArrayList<Long>());
return;
}
for (Long next : targetPid) {
ourLog.debug("Searching for resource link with target PID: {}", next);
Predicate eq = builder.equal(from.get("myTargetResourcePid"), next);

View File

@ -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();

View File

@ -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;

View File

@ -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<String> urls = new HashSet<String>();
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<String> urls = new HashSet<String>();
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());
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.
</action>
<action type="add" issue="363">
JPA server can now be configured to allow external references (i.e. references that
point to resources on other servers). See
<![CDATA[<a href="./doc_jpa.html">JPA Documentation</a>]]> for information on
how to use this. Thanks to Naminder Soorma for the suggestion!
</action>
</release>
<release version="1.5" date="2016-04-20">
<action type="fix" issue="339">

View File

@ -147,6 +147,89 @@ $ mvn install]]></source>
Configures the database connection settings
</li>
</ul>
</section>
<section name="DaoConfig">
<p>
The Spring confguration contains a definition for a bean called <code>daoConfig</code>,
which will look something like the following:
</p>
<pre><![CDATA[@Bean()
public DaoConfig daoConfig() {
DaoConfig retVal = new DaoConfig();
retVal.setAllowMultipleDelete(true);
retVal.setAllowInlineMatchUrlReferences(true);
return retVal;
}]]></pre>
<p>
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 <a href="./apidocs-jpaserver/ca/uhn/fhir/jpa/dao/DaoConfig.html">DaoConfig JavaDoc</a>
for information about the available settings.
</p>
<subsection name="External/Absolute Resource References">
<p>
Clients may sometimes post resources to your server that contain
absolute resource references. For example, consider the following resource:
</p>
<pre><![CDATA[<Patient xmlns="http://hl7.org/fhir">
<id value="patient-infant-01"/>
<name>
<use value="official"/>
<family value="Miller"/>
<given value="Samuel"/>
</name>
<managingOrganization>
<reference value="http://example.com/fhir/Organization/123"/>
</managingOrganization>
</Patient>]]></pre>
<p>
By default, the server will reject this reference, as only
local references are permitted by the server. This can be changed
however.
</p>
<p>
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
<code>http://example.com/fhir/</code>) you can
configure the server to recognize this URL via the following DaoConfig
setting:
</p>
<pre><![CDATA[@Bean()
public DaoConfig daoConfig() {
DaoConfig retVal = new DaoConfig();
// ... other config ...
retVal.getTreatBaseUrlsAsLocal().add("http://example.com/fhir/");
return retVal;
}]]></pre>
<p>
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 <code>setAllowInlineMatchUrlReferences</code> means that
it will be possible to search for references that refer to these
external references.
</p>
<pre><![CDATA[@Bean()
public DaoConfig daoConfig() {
DaoConfig retVal = new DaoConfig();
// Allow external references
retVal.setAllowInlineMatchUrlReferences(true);
// If you are allowing external references, it is recommended to
// also tell the server which references actually will be local
retVal.getTreatBaseUrlsAsLocal().add("http://mydomain.com/fhir");
return retVal;
}]]></pre>
</subsection>
</section>
<section name="Not yet complete">