JPA server now has configurable properties that allow referential integrity to be disabled for both writes and deletes. This is useful in some cases where data integrity is not wanted or not possible. It can also be useful if you want to delete large amounts interconnected data quickly.

This commit is contained in:
James 2017-05-22 15:34:44 -04:00
parent a834770e38
commit 1ec180628f
22 changed files with 764 additions and 58 deletions

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3;
public class RunServerCommand extends BaseCommand { public class RunServerCommand extends BaseCommand {
private static final String DISABLE_REFERENTIAL_INTEGRITY = "disable-referential-integrity";
private static final String OPTION_LOWMEM = "lowmem"; private static final String OPTION_LOWMEM = "lowmem";
private static final String OPTION_ALLOW_EXTERNAL_REFS = "allow-external-refs"; private static final String OPTION_ALLOW_EXTERNAL_REFS = "allow-external-refs";
private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_PORT = 8080;
@ -49,6 +50,7 @@ public class RunServerCommand extends BaseCommand {
options.addOption(OPTION_P, "port", true, "The port to listen on (default is " + DEFAULT_PORT + ")"); options.addOption(OPTION_P, "port", true, "The port to listen on (default is " + DEFAULT_PORT + ")");
options.addOption(null, OPTION_LOWMEM, false, "If this flag is set, the server will operate in low memory mode (some features disabled)"); options.addOption(null, OPTION_LOWMEM, false, "If this flag is set, the server will operate in low memory mode (some features disabled)");
options.addOption(null, OPTION_ALLOW_EXTERNAL_REFS, false, "If this flag is set, the server will allow resources to be persisted contaning external resource references"); options.addOption(null, OPTION_ALLOW_EXTERNAL_REFS, false, "If this flag is set, the server will allow resources to be persisted contaning external resource references");
options.addOption(null, DISABLE_REFERENTIAL_INTEGRITY, false, "If this flag is set, the server will not enforce referential integrity");
return options; return options;
} }

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one # Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file # or more contributor license agreements. See the NOTICE file

View File

@ -9,6 +9,7 @@ public class ContextHolder {
private static boolean ourAllowExternalRefs; private static boolean ourAllowExternalRefs;
private static FhirContext ourCtx; private static FhirContext ourCtx;
private static boolean ourDisableReferentialIntegrity;
private static String ourPath; private static String ourPath;
public static FhirContext getCtx() { public static FhirContext getCtx() {
@ -25,6 +26,10 @@ public class ContextHolder {
return ourAllowExternalRefs; return ourAllowExternalRefs;
} }
public static boolean isDisableReferentialIntegrity() {
return ourDisableReferentialIntegrity;
}
public static void setAllowExternalRefs(boolean theAllowExternalRefs) { public static void setAllowExternalRefs(boolean theAllowExternalRefs) {
ourAllowExternalRefs = theAllowExternalRefs; ourAllowExternalRefs = theAllowExternalRefs;
} }
@ -44,4 +49,8 @@ public class ContextHolder {
ourCtx = theCtx; ourCtx = theCtx;
} }
public static void setDisableReferentialIntegrity(boolean theDisableReferentialIntegrity) {
ourDisableReferentialIntegrity = theDisableReferentialIntegrity;
}
} }

View File

@ -168,6 +168,8 @@ public class JpaServerDemo extends RestfulServer {
DaoConfig daoConfig = myAppCtx.getBean(DaoConfig.class); DaoConfig daoConfig = myAppCtx.getBean(DaoConfig.class);
daoConfig.setAllowExternalReferences(ContextHolder.isAllowExternalRefs()); daoConfig.setAllowExternalReferences(ContextHolder.isAllowExternalRefs());
daoConfig.setEnforceReferentialIntegrityOnDelete(!ContextHolder.isDisableReferentialIntegrity());
daoConfig.setEnforceReferentialIntegrityOnWrite(!ContextHolder.isDisableReferentialIntegrity());
} }

View File

@ -122,7 +122,7 @@ public class BaseDstu2Config extends BaseConfig {
@Bean @Bean
@Lazy @Lazy
public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu3Interceptor() { public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu2Interceptor() {
return new RestHookSubscriptionDstu2Interceptor(); return new RestHookSubscriptionDstu2Interceptor();
} }

View File

@ -378,6 +378,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
try { try {
valueOf = translateForcedIdToPid(typeString, id); valueOf = translateForcedIdToPid(typeString, id);
} catch (ResourceNotFoundException e) { } catch (ResourceNotFoundException e) {
if (myConfig.isEnforceReferentialIntegrityOnWrite() == false) {
continue;
}
String resName = getContext().getResourceDefinition(type).getName(); String resName = getContext().getResourceDefinition(type).getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
} }

View File

@ -43,6 +43,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.*;
@ -79,6 +80,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
protected PlatformTransactionManager myPlatformTransactionManager; protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired @Autowired
private IResourceHistoryTableDao myResourceHistoryTableDao; private IResourceHistoryTableDao myResourceHistoryTableDao;
@Autowired
private IResourceLinkDao myResourceLinkDao;
private String myResourceName; private String myResourceName;
@Autowired @Autowired
protected IResourceTableDao myResourceTableDao; protected IResourceTableDao myResourceTableDao;
@ -87,8 +90,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
protected IFulltextSearchSvc mySearchDao; protected IFulltextSearchSvc mySearchDao;
@Autowired() @Autowired()
protected ISearchResultDao mySearchResultDao; protected ISearchResultDao mySearchResultDao;
private String mySecondaryPrimaryKeyParamName; private String mySecondaryPrimaryKeyParamName;
@Override @Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
@ -118,7 +123,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() }); ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() });
} }
@Override @Override
public DaoMethodOutcome create(final T theResource) { public DaoMethodOutcome create(final T theResource) {
return create(theResource, null, true, null); return create(theResource, null, true, null);
@ -175,7 +179,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
@Override @Override
public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) { public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theRequestDetails) {
if (theId == null || !theId.hasIdPart()) { if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided"); throw new InvalidRequestException("Can not perform delete, no ID provided");
} }
@ -188,7 +192,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resourceToDelete = toResource(myResourceType, entity, false); T resourceToDelete = toResource(myResourceType, entity, false);
validateOkToDelete(deleteConflicts, entity); validateOkToDelete(theDeleteConflicts, entity);
preDelete(resourceToDelete, entity); preDelete(resourceToDelete, entity);
@ -615,6 +619,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return retVal; return retVal;
} }
@Override @Override
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequestDetails) { public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequestDetails) {
// Notify interceptors // Notify interceptors
@ -636,7 +641,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return retVal; return retVal;
} }
@Override @Override
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) { public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
// Notify interceptors // Notify interceptors
@ -911,6 +915,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName()); return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName());
} }
@Override @Override
public Set<Long> searchForIds(SearchParameterMap theParams) { public Set<Long> searchForIds(SearchParameterMap theParams) {
@ -929,9 +936,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return retVal; return retVal;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Required @Required
public void setResourceType(Class<? extends IBaseResource> theTableType) { public void setResourceType(Class<? extends IBaseResource> theTableType) {
@ -1158,12 +1162,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return outcome; return outcome;
} }
@Override @Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails); return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
} }
@Override @Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, true, theRequestDetails); return update(theResource, theMatchUrl, true, theRequestDetails);
@ -1210,6 +1214,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return; return;
} }
if (myDaoConfig.isEnforceReferentialIntegrityOnDelete() == false) {
ourLog.info("Deleting {} resource dependencies which can no longer be satisfied", resultList.size());
myResourceLinkDao.delete(resultList);
return;
}
ResourceLink link = resultList.get(0); ResourceLink link = resultList.get(0);
IdDt targetId = theEntity.getIdDt(); IdDt targetId = theEntity.getIdDt();
IdDt sourceId = link.getSourceResource().getIdDt(); IdDt sourceId = link.getSourceResource().getIdDt();

View File

@ -1,6 +1,11 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
import java.util.*; 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.ObjectUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
@ -78,11 +83,14 @@ public class DaoConfig {
private boolean myDeleteStaleSearches = true; private boolean myDeleteStaleSearches = true;
private boolean myEnforceReferentialIntegrityOnDelete = true;
private boolean myEnforceReferentialIntegrityOnWrite = true;
// *** // ***
// update setter javadoc if default changes // update setter javadoc if default changes
// *** // ***
private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR; private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR;
private int myHardTagListLimit = 1000; private int myHardTagListLimit = 1000;
private int myIncludeLimit = 2000; private int myIncludeLimit = 2000;
@ -90,7 +98,6 @@ public class DaoConfig {
// update setter javadoc if default changes // update setter javadoc if default changes
// *** // ***
private boolean myIndexContainedResources = true; private boolean myIndexContainedResources = true;
private List<IServerInterceptor> myInterceptors; private List<IServerInterceptor> myInterceptors;
// *** // ***
// update setter javadoc if default changes // update setter javadoc if default changes
@ -323,6 +330,39 @@ public class DaoConfig {
return myDefaultSearchParamsCanBeOverridden; return myDefaultSearchParamsCanBeOverridden;
} }
/**
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
* deleted even if other resources currently contain references to them.
* <p>
* This property can cause confusing results for clients of the server since searches, includes,
* and other FHIR features may not behave as expected when referential integrity is not
* preserved. Use this feature with caution.
* </p>
*
* @return
*/
public boolean isEnforceReferentialIntegrityOnDelete() {
return myEnforceReferentialIntegrityOnDelete;
}
/**
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
* created or updated even if they contain references to local resources that do not exist.
* <p>
* For example, if a patient contains a reference to managing organization <code>Organization/FOO</code>
* but FOO is not a valid ID for an organization on the server, the operation will be blocked unless
* this propery has been set to <code>false</code>
* </p>
* <p>
* This property can cause confusing results for clients of the server since searches, includes,
* and other FHIR features may not behave as expected when referential integrity is not
* preserved. Use this feature with caution.
* </p>
*/
public boolean isEnforceReferentialIntegrityOnWrite() {
return myEnforceReferentialIntegrityOnWrite;
}
/** /**
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion * If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
@ -445,6 +485,37 @@ public class DaoConfig {
myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize; myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize;
} }
/**
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
* deleted even if other resources currently contain references to them.
* <p>
* This property can cause confusing results for clients of the server since searches, includes,
* and other FHIR features may not behave as expected when referential integrity is not
* preserved. Use this feature with caution.
* </p>
*/
public void setEnforceReferentialIntegrityOnDelete(boolean theEnforceReferentialIntegrityOnDelete) {
myEnforceReferentialIntegrityOnDelete = theEnforceReferentialIntegrityOnDelete;
}
/**
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
* created or updated even if they contain references to local resources that do not exist.
* <p>
* For example, if a patient contains a reference to managing organization <code>Organization/FOO</code>
* but FOO is not a valid ID for an organization on the server, the operation will be blocked unless
* this propery has been set to <code>false</code>
* </p>
* <p>
* This property can cause confusing results for clients of the server since searches, includes,
* and other FHIR features may not behave as expected when referential integrity is not
* preserved. Use this feature with caution.
* </p>
*/
public void setEnforceReferentialIntegrityOnWrite(boolean theEnforceReferentialIntegrityOnWrite) {
myEnforceReferentialIntegrityOnWrite = theEnforceReferentialIntegrityOnWrite;
}
/** /**
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion * If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.jpa.dao.data;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.springframework.data.jpa.repository.JpaRepository;
import ca.uhn.fhir.jpa.entity.ResourceLink;
public interface IResourceLinkDao extends JpaRepository<ResourceLink, Long> {
// nothing
}

View File

@ -31,6 +31,7 @@ import javax.persistence.Query;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.dstu3.model.Subscription;
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType;
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -67,6 +68,7 @@ import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum; import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
@ -333,6 +335,21 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\""); throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
} }
if (theResource.getChannel().getType() == null) {
throw new UnprocessableEntityException("Subscription.channel.type must be populated");
} else if (theResource.getChannel().getType() == SubscriptionChannelType.RESTHOOK) {
if (isBlank(theResource.getChannel().getPayload())) {
throw new UnprocessableEntityException("Subscription.channel.payload must be populated for rest-hook subscriptions");
}
if (EncodingEnum.forContentType(theResource.getChannel().getPayload()) == null){
throw new UnprocessableEntityException("Invalid value for Subscription.channel.payload: " + theResource.getChannel().getPayload());
}
if (isBlank(theResource.getChannel().getEndpoint())){
throw new UnprocessableEntityException("Rest-hook subscriptions must have Subscription.channel.endpoint defined");
}
}
RuntimeResourceDefinition resDef; RuntimeResourceDefinition resDef;
try { try {
resDef = getContext().getResourceDefinition(resType); resDef = getContext().getResourceDefinition(resType);

View File

@ -70,10 +70,11 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class); private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class);
@Autowired @Autowired
private FhirContext myCtx; private FhirContext myFhirContext;
private boolean myNotifyOnDelete = false;
private boolean myNotifyOnDelete = false;
private final List<Subscription> myRestHookSubscriptions = new ArrayList<Subscription>(); private final List<Subscription> myRestHookSubscriptions = new ArrayList<Subscription>();
@Autowired @Autowired
@Qualifier("mySubscriptionDaoDstu2") @Qualifier("mySubscriptionDaoDstu2")
private IFhirResourceDao<Subscription> mySubscriptionDao; private IFhirResourceDao<Subscription> mySubscriptionDao;
@ -126,7 +127,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
} }
HttpUriRequest request = null; HttpUriRequest request = null;
String resourceName = myCtx.getResourceDefinition(theResource).getName(); String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
String payload = theSubscription.getChannel().getPayload(); String payload = theSubscription.getChannel().getPayload();
String resourceId = theResource.getIdElement().getIdPart(); String resourceId = theResource.getIdElement().getIdPart();
@ -223,7 +224,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
} }
private String getResourceName(IBaseResource theResource) { private String getResourceName(IBaseResource theResource) {
return myCtx.getResourceDefinition(theResource).getName(); return myFhirContext.getResourceDefinition(theResource).getName();
} }
/** /**
@ -308,7 +309,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
@Override @Override
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
IIdType idType = theResource.getIdElement(); IIdType idType = theResource.getIdElement();
ourLog.info("resource created type: {}", theRequest.getResourceName()); ourLog.info("resource created type: {}", getResourceName(theResource));
if (theResource instanceof Subscription) { if (theResource instanceof Subscription) {
Subscription subscription = (Subscription) theResource; Subscription subscription = (Subscription) theResource;
@ -320,7 +321,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
ourLog.info("Subscription was added. Id: " + subscription.getId()); ourLog.info("Subscription was added. Id: " + subscription.getId());
} }
} else { } else {
checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE); checkSubscriptions(idType, getResourceName(theResource), RestOperationTypeEnum.CREATE);
} }
} }
@ -379,7 +380,15 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
} }
} }
public void setFhirContext(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
public void setNotifyOnDelete(boolean notifyOnDelete) { public void setNotifyOnDelete(boolean notifyOnDelete) {
this.myNotifyOnDelete = notifyOnDelete; this.myNotifyOnDelete = notifyOnDelete;
} }
public void setSubscriptionDao(IFhirResourceDao<Subscription> theSubscriptionDao) {
mySubscriptionDao = theSubscriptionDao;
}
} }

View File

@ -68,7 +68,16 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class); private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class);
@Autowired @Autowired
private FhirContext myCtx; private FhirContext myFhirContext;
public void setFhirContext(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
public void setSubscriptionDao(IFhirResourceDao<Subscription> theSubscriptionDao) {
mySubscriptionDao = theSubscriptionDao;
}
@Autowired @Autowired
@Qualifier("mySubscriptionDaoDstu3") @Qualifier("mySubscriptionDaoDstu3")
private IFhirResourceDao<Subscription> mySubscriptionDao; private IFhirResourceDao<Subscription> mySubscriptionDao;
@ -125,7 +134,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
} }
HttpUriRequest request = null; HttpUriRequest request = null;
String resourceName = myCtx.getResourceDefinition(theResource).getName(); String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
String payload = theSubscription.getChannel().getPayload(); String payload = theSubscription.getChannel().getPayload();
String resourceId = theResource.getIdElement().getIdPart(); String resourceId = theResource.getIdElement().getIdPart();
@ -215,7 +224,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
} }
private String getResourceName(IBaseResource theResource) { private String getResourceName(IBaseResource theResource) {
return myCtx.getResourceDefinition(theResource).getName(); return myFhirContext.getResourceDefinition(theResource).getName();
} }
/** /**
@ -301,7 +310,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
@Override @Override
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
IIdType idType = theResource.getIdElement(); IIdType idType = theResource.getIdElement();
ourLog.info("resource created type: {}", theRequest.getResourceName()); ourLog.info("resource created type: {}", getResourceName(theResource));
if (theResource instanceof Subscription) { if (theResource instanceof Subscription) {
Subscription subscription = (Subscription) theResource; Subscription subscription = (Subscription) theResource;
@ -313,7 +322,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
ourLog.info("Subscription was added, id: {} - Have {}", subscription.getIdElement().getIdPart(), myRestHookSubscriptions.size()); ourLog.info("Subscription was added, id: {} - Have {}", subscription.getIdElement().getIdPart(), myRestHookSubscriptions.size());
} }
} else { } else {
checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE); checkSubscriptions(idType, getResourceName(theResource), RestOperationTypeEnum.CREATE);
} }
} }

View File

@ -26,7 +26,6 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
@SuppressWarnings("unused") @SuppressWarnings("unused")

View File

@ -42,7 +42,29 @@ import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.*; import ca.uhn.fhir.model.dstu2.resource.Appointment;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.ConceptMap;
import ca.uhn.fhir.model.dstu2.resource.Device;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticOrder;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu2.resource.Encounter;
import ca.uhn.fhir.model.dstu2.resource.Immunization;
import ca.uhn.fhir.model.dstu2.resource.Location;
import ca.uhn.fhir.model.dstu2.resource.Media;
import ca.uhn.fhir.model.dstu2.resource.Medication;
import ca.uhn.fhir.model.dstu2.resource.MedicationAdministration;
import ca.uhn.fhir.model.dstu2.resource.MedicationOrder;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.resource.Practitioner;
import ca.uhn.fhir.model.dstu2.resource.Questionnaire;
import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse;
import ca.uhn.fhir.model.dstu2.resource.StructureDefinition;
import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.model.dstu2.resource.Substance;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.method.MethodUtil; import ca.uhn.fhir.rest.method.MethodUtil;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;

View File

@ -0,0 +1,99 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.hl7.fhir.dstu3.model.Organization;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.util.TestUtil;
public class FhirResourceDaoDstu3ReferentialIntegrityTest extends BaseJpaDstu3Test {
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After
public void afterResetConfig() {
myDaoConfig.setEnforceReferentialIntegrityOnWrite(new DaoConfig().isEnforceReferentialIntegrityOnWrite());
myDaoConfig.setEnforceReferentialIntegrityOnDelete(new DaoConfig().isEnforceReferentialIntegrityOnDelete());
}
@Test
public void testCreateUnknownReferenceFail() throws Exception {
Patient p = new Patient();
p.setManagingOrganization(new Reference("Organization/AAA"));
try {
myPatientDao.create(p);
fail();
} catch (InvalidRequestException e) {
assertEquals("Resource Organization/AAA not found, specified in path: Patient.managingOrganization", e.getMessage());
}
}
@Test
public void testCreateUnknownReferenceAllow() throws Exception {
myDaoConfig.setEnforceReferentialIntegrityOnWrite(false);
Patient p = new Patient();
p.setManagingOrganization(new Reference("Organization/AAA"));
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
p = myPatientDao.read(id);
assertEquals("Organization/AAA", p.getManagingOrganization().getReference());
}
@Test
public void testDeleteFail() throws Exception {
Organization o = new Organization();
o.setName("FOO");
IIdType oid = myOrganizationDao.create(o).getId().toUnqualifiedVersionless();
Patient p = new Patient();
p.setManagingOrganization(new Reference(oid));
IIdType pid = myPatientDao.create(p).getId().toUnqualifiedVersionless();
try {
myOrganizationDao.delete(oid);
fail();
} catch (ResourceVersionConflictException e) {
assertEquals("Unable to delete Organization/"+oid.getIdPart()+" because at least one resource has a reference to this resource. First reference found was resource Organization/"+oid.getIdPart()+" in path Patient.managingOrganization", e.getMessage());
}
myPatientDao.delete(pid);
myOrganizationDao.delete(oid);
}
@Test
public void testDeleteAllow() throws Exception {
myDaoConfig.setEnforceReferentialIntegrityOnDelete(false);
Organization o = new Organization();
o.setName("FOO");
IIdType oid = myOrganizationDao.create(o).getId().toUnqualifiedVersionless();
Patient p = new Patient();
p.setManagingOrganization(new Reference(oid));
IIdType pid = myPatientDao.create(p).getId().toUnqualifiedVersionless();
myOrganizationDao.delete(oid);
myPatientDao.delete(pid);
}
}

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionFlaggedResourceDataDao; import ca.uhn.fhir.jpa.dao.data.ISubscriptionFlaggedResourceDataDao;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao; import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
import ca.uhn.fhir.jpa.entity.SubscriptionTable; import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
@ -170,6 +171,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
public void testCreateSubscriptionInvalidCriteria() { public void testCreateSubscriptionInvalidCriteria() {
Subscription subs = new Subscription(); Subscription subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED); subs.setStatus(SubscriptionStatus.REQUESTED);
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
subs.setCriteria("Observation"); subs.setCriteria("Observation");
try { try {
mySubscriptionDao.create(subs, mySrd); mySubscriptionDao.create(subs, mySrd);
@ -180,6 +182,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
subs = new Subscription(); subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED); subs.setStatus(SubscriptionStatus.REQUESTED);
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
subs.setCriteria("http://foo.com/Observation?AAA=BBB"); subs.setCriteria("http://foo.com/Observation?AAA=BBB");
try { try {
mySubscriptionDao.create(subs, mySrd); mySubscriptionDao.create(subs, mySrd);
@ -190,6 +193,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
subs = new Subscription(); subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED); subs.setStatus(SubscriptionStatus.REQUESTED);
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
subs.setCriteria("ObservationZZZZ?a=b"); subs.setCriteria("ObservationZZZZ?a=b");
try { try {
mySubscriptionDao.create(subs, mySrd); mySubscriptionDao.create(subs, mySrd);
@ -205,7 +209,42 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
mySubscriptionDao.create(subs, mySrd); mySubscriptionDao.create(subs, mySrd);
fail(); fail();
} catch (UnprocessableEntityException e) { } catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Subscription.channel.type must be populated on this server")); assertThat(e.getMessage(), containsString("Subscription.channel.type must be populated"));
}
subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED);
subs.setCriteria("Observation?identifier=123");
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
try {
mySubscriptionDao.create(subs, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Subscription.channel.payload must be populated for rest-hook subscriptions"));
}
subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED);
subs.setCriteria("Observation?identifier=123");
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
subs.getChannel().setPayload("text/html");
try {
mySubscriptionDao.create(subs, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Invalid value for Subscription.channel.payload: text/html"));
}
subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED);
subs.setCriteria("Observation?identifier=123");
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
subs.getChannel().setPayload("application/fhir+xml");
try {
mySubscriptionDao.create(subs, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Rest-hook subscriptions must have Subscription.channel.endpoint defined"));
} }
subs = new Subscription(); subs = new Subscription();
@ -214,6 +253,14 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
assertTrue(mySubscriptionDao.create(subs, mySrd).getId().hasIdPart()); assertTrue(mySubscriptionDao.create(subs, mySrd).getId().hasIdPart());
subs = new Subscription();
subs.setStatus(SubscriptionStatus.REQUESTED);
subs.setCriteria("Observation?identifier=123");
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
subs.getChannel().setPayload("application/fhir+json");
subs.getChannel().setEndpoint("http://localhost:8080");
assertTrue(mySubscriptionDao.create(subs, mySrd).getId().hasIdPart());
} }
@Test @Test

View File

@ -24,7 +24,6 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.rp.dstu.ObservationResourceProvider; import ca.uhn.fhir.jpa.rp.dstu.ObservationResourceProvider;
import ca.uhn.fhir.jpa.rp.dstu.OrganizationResourceProvider; import ca.uhn.fhir.jpa.rp.dstu.OrganizationResourceProvider;
import ca.uhn.fhir.jpa.rp.dstu.PatientResourceProvider; import ca.uhn.fhir.jpa.rp.dstu.PatientResourceProvider;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.model.dstu.resource.Observation; import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.Organization; import ca.uhn.fhir.model.dstu.resource.Organization;
@ -32,6 +31,7 @@ import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.resource.Questionnaire; import ca.uhn.fhir.model.dstu.resource.Questionnaire;
import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
public class SystemProviderDstu1Test extends BaseJpaTest { public class SystemProviderDstu1Test extends BaseJpaTest {
@ -100,7 +100,7 @@ public class SystemProviderDstu1Test extends BaseJpaTest {
JpaSystemProviderDstu1 systemProv = ourAppCtx.getBean(JpaSystemProviderDstu1.class, "mySystemProviderDstu1"); JpaSystemProviderDstu1 systemProv = ourAppCtx.getBean(JpaSystemProviderDstu1.class, "mySystemProviderDstu1");
restServer.setPlainProviders(systemProv); restServer.setPlainProviders(systemProv);
int myPort = RandomServerPortProvider.findFreePort(); int myPort = PortUtil.findFreePort();
ourServer = new Server(myPort); ourServer = new Server(myPort);
ServletContextHandler proxyHandler = new ServletContextHandler(); ServletContextHandler proxyHandler = new ServletContextHandler();

View File

@ -34,7 +34,6 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor; import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.parser.StrictErrorHandler;
@ -43,6 +42,7 @@ import ca.uhn.fhir.rest.client.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
@ -77,7 +77,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
if (ourServer == null) { if (ourServer == null) {
ourPort = RandomServerPortProvider.findFreePort(); ourPort = PortUtil.findFreePort();
ourRestServer = new RestfulServer(myFhirCtx); ourRestServer = new RestfulServer(myFhirCtx);

View File

@ -9,25 +9,34 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.*; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test; import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Observation; import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.Subscription; import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.model.dstu2.resource.Subscription.Channel; import ca.uhn.fhir.model.dstu2.resource.Subscription.Channel;
import ca.uhn.fhir.model.dstu2.valueset.*; import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.PortUtil;
/** /**
* Test the rest-hook subscriptions * Test the rest-hook subscriptions
@ -250,7 +259,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
@BeforeClass @BeforeClass
public static void startListenerServer() throws Exception { public static void startListenerServer() throws Exception {
ourListenerPort = RandomServerPortProvider.findFreePort(); ourListenerPort = PortUtil.findFreePort();
ourListenerRestServer = new RestfulServer(FhirContext.forDstu2()); ourListenerRestServer = new RestfulServer(FhirContext.forDstu2());
ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context";

View File

@ -3,8 +3,11 @@ package ca.uhn.fhir.jpa.subscription;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
@ -21,6 +24,7 @@ import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
@ -29,12 +33,14 @@ import ca.uhn.fhir.rest.server.RestfulServer;
*/ */
public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
private static List<String> ourContentTypes = new ArrayList<String>();
private static List<Observation> ourCreatedObservations = Lists.newArrayList(); private static List<Observation> ourCreatedObservations = Lists.newArrayList();
private static int ourListenerPort; private static int ourListenerPort;
private static RestfulServer ourListenerRestServer; private static RestfulServer ourListenerRestServer;
private static Server ourListenerServer; private static Server ourListenerServer;
private static String ourListenerServerBase; private static String ourListenerServerBase;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestDstu3Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestDstu3Test.class);
private static List<Observation> ourUpdatedObservations = Lists.newArrayList(); private static List<Observation> ourUpdatedObservations = Lists.newArrayList();
@After @After
@ -57,18 +63,19 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
public void beforeReset() { public void beforeReset() {
ourCreatedObservations.clear(); ourCreatedObservations.clear();
ourUpdatedObservations.clear(); ourUpdatedObservations.clear();
ourContentTypes.clear();
} }
private Subscription createSubscription(String criteria, String payload, String endpoint) { private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) {
Subscription subscription = new Subscription(); Subscription subscription = new Subscription();
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
subscription.setCriteria(criteria); subscription.setCriteria(theCriteria);
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); channel.setType(Subscription.SubscriptionChannelType.RESTHOOK);
channel.setPayload(payload); channel.setPayload(thePayload);
channel.setEndpoint(endpoint); channel.setEndpoint(theEndpoint);
subscription.setChannel(channel); subscription.setChannel(channel);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
@ -96,7 +103,27 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
} }
@Test @Test
public void testRestHookSubscriptionJson() throws Exception { public void testRestHookSubscriptionApplicationFhirJson() throws Exception {
String payload = "application/fhir+json";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
createSubscription(criteria1, payload, ourListenerServerBase);
createSubscription(criteria2, payload, ourListenerServerBase);
sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
Thread.sleep(500);
assertEquals(1, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
}
@Test
public void testRestHookSubscriptionApplicationJson() throws Exception {
String payload = "application/json"; String payload = "application/json";
String code = "1000000050"; String code = "1000000050";
@ -112,6 +139,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
Thread.sleep(500); Thread.sleep(500);
assertEquals(1, ourCreatedObservations.size()); assertEquals(1, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size()); assertEquals(0, ourUpdatedObservations.size());
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
Assert.assertNotNull(subscriptionTemp); Assert.assertNotNull(subscriptionTemp);
@ -169,7 +197,28 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
} }
@Test @Test
public void testRestHookSubscriptionXml() throws Exception { public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
String payload = "application/fhir+xml";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase);
Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase);
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
Thread.sleep(500);
assertEquals(1, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0));
}
@Test
public void testRestHookSubscriptionApplicationXml() throws Exception {
String payload = "application/xml"; String payload = "application/xml";
String code = "1000000050"; String code = "1000000050";
@ -185,6 +234,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
Thread.sleep(500); Thread.sleep(500);
assertEquals(1, ourCreatedObservations.size()); assertEquals(1, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size()); assertEquals(0, ourUpdatedObservations.size());
assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0));
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
Assert.assertNotNull(subscriptionTemp); Assert.assertNotNull(subscriptionTemp);
@ -271,8 +321,9 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
public static class ObservationListener implements IResourceProvider { public static class ObservationListener implements IResourceProvider {
@Create @Create
public MethodOutcome create(@ResourceParam Observation theObservation) { public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
ourLog.info("Received Listener Create"); ourLog.info("Received Listener Create");
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
ourCreatedObservations.add(theObservation); ourCreatedObservations.add(theObservation);
return new MethodOutcome(new IdType("Observation/1"), true); return new MethodOutcome(new IdType("Observation/1"), true);
} }
@ -283,9 +334,10 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
} }
@Update @Update
public MethodOutcome update(@ResourceParam Observation theObservation) { public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
ourLog.info("Received Listener Update"); ourLog.info("Received Listener Update");
ourUpdatedObservations.add(theObservation); ourUpdatedObservations.add(theObservation);
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
return new MethodOutcome(new IdType("Observation/1"), false); return new MethodOutcome(new IdType("Observation/1"), false);
} }

View File

@ -0,0 +1,309 @@
package ca.uhn.fhir.jpa.subscription;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.collect.Lists;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.model.dstu2.resource.Subscription.Channel;
import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.PortUtil;
/**
* Test the rest-hook subscriptions
*/
public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends BaseResourceProviderDstu2Test {
private static List<Observation> ourCreatedObservations = Lists.newArrayList();
private static int ourListenerPort;
private static RestfulServer ourListenerRestServer;
private static Server ourListenerServer;
private static String ourListenerServerBase;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.class);
private static List<Observation> ourUpdatedObservations = Lists.newArrayList();
@After
public void afterUnregisterRestHookListener() {
myDaoConfig.setAllowMultipleDelete(true);
ourLog.info("Deleting all subscriptions");
ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute();
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
myDaoConfig.getInterceptors().remove(ourRestHookSubscriptionInterceptor);
}
@Before
public void beforeRegisterRestHookListener() {
myDaoConfig.getInterceptors().add(ourRestHookSubscriptionInterceptor);
}
@Before
public void beforeReset() {
ourCreatedObservations.clear();
ourUpdatedObservations.clear();
}
private Subscription createSubscription(String criteria, String payload, String endpoint) {
Subscription subscription = new Subscription();
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
subscription.setStatus(SubscriptionStatusEnum.ACTIVE);
subscription.setCriteria(criteria);
Channel channel = new Channel();
channel.setType(SubscriptionChannelTypeEnum.REST_HOOK);
channel.setPayload(payload);
channel.setEndpoint(endpoint);
subscription.setChannel(channel);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
subscription.setId(methodOutcome.getId().getIdPart());
return subscription;
}
private Observation sendObservation(String code, String system) {
Observation observation = new Observation();
CodeableConceptDt codeableConcept = new CodeableConceptDt();
observation.setCode(codeableConcept);
CodingDt coding = codeableConcept.addCoding();
coding.setCode(code);
coding.setSystem(system);
observation.setStatus(ObservationStatusEnum.FINAL);
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute();
String observationId = methodOutcome.getId().getIdPart();
observation.setId(observationId);
return observation;
}
@Test
public void testRestHookSubscriptionJson() throws Exception {
String payload = "application/json";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase);
Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase);
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
Thread.sleep(500);
assertEquals(1, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.setCriteria(criteria1);
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
Observation observation2 = sendObservation(code, "SNOMED-CT");
// Should see two subscription notifications
Thread.sleep(500);
assertEquals(3, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
ourClient.delete().resourceById(new IdDt("Subscription/"+ subscription2.getId())).execute();
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
// Should see only one subscription notification
Thread.sleep(500);
assertEquals(4, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
Observation observation3 = ourClient.read(Observation.class, observationTemp3.getId());
CodeableConceptDt codeableConcept = new CodeableConceptDt();
observation3.setCode(codeableConcept);
CodingDt coding = codeableConcept.addCoding();
coding.setCode(code + "111");
coding.setSystem("SNOMED-CT");
ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
// Should see no subscription notification
Thread.sleep(500);
assertEquals(4, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
Observation observation3a = ourClient.read(Observation.class, observationTemp3.getId());
CodeableConceptDt codeableConcept1 = new CodeableConceptDt();
observation3a.setCode(codeableConcept1);
CodingDt coding1 = codeableConcept1.addCoding();
coding1.setCode(code);
coding1.setSystem("SNOMED-CT");
ourClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
// Should see only one subscription notification
Thread.sleep(500);
assertEquals(4, ourCreatedObservations.size());
assertEquals(1, ourUpdatedObservations.size());
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty());
}
@Test
public void testRestHookSubscriptionXml() throws Exception {
String payload = "application/xml";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase);
Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase);
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
Thread.sleep(500);
assertEquals(1, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.setCriteria(criteria1);
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
Observation observation2 = sendObservation(code, "SNOMED-CT");
// Should see two subscription notifications
Thread.sleep(500);
assertEquals(3, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
ourClient.delete().resourceById(new IdDt("Subscription/"+ subscription2.getId())).execute();
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
// Should see only one subscription notification
Thread.sleep(500);
assertEquals(4, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
Observation observation3 = ourClient.read(Observation.class, observationTemp3.getId());
CodeableConceptDt codeableConcept = new CodeableConceptDt();
observation3.setCode(codeableConcept);
CodingDt coding = codeableConcept.addCoding();
coding.setCode(code + "111");
coding.setSystem("SNOMED-CT");
ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
// Should see no subscription notification
Thread.sleep(500);
assertEquals(4, ourCreatedObservations.size());
assertEquals(0, ourUpdatedObservations.size());
Observation observation3a = ourClient.read(Observation.class, observationTemp3.getId());
CodeableConceptDt codeableConcept1 = new CodeableConceptDt();
observation3a.setCode(codeableConcept1);
CodingDt coding1 = codeableConcept1.addCoding();
coding1.setCode(code);
coding1.setSystem("SNOMED-CT");
ourClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
// Should see only one subscription notification
Thread.sleep(500);
assertEquals(4, ourCreatedObservations.size());
assertEquals(1, ourUpdatedObservations.size());
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty());
}
@BeforeClass
public static void startListenerServer() throws Exception {
ourListenerPort = PortUtil.findFreePort();
ourListenerRestServer = new RestfulServer(FhirContext.forDstu2());
ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context";
ObservationListener obsListener = new ObservationListener();
ourListenerRestServer.setResourceProviders(obsListener);
ourListenerServer = new Server(ourListenerPort);
ServletContextHandler proxyHandler = new ServletContextHandler();
proxyHandler.setContextPath("/");
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(ourListenerRestServer);
proxyHandler.addServlet(servletHolder, "/fhir/context/*");
ourListenerServer.setHandler(proxyHandler);
ourListenerServer.start();
}
@AfterClass
public static void stopListenerServer() throws Exception {
ourListenerServer.stop();
}
public static class ObservationListener implements IResourceProvider {
@Create
public MethodOutcome create(@ResourceParam Observation theObservation) {
ourLog.info("Received Listener Create");
ourCreatedObservations.add(theObservation);
return new MethodOutcome(new IdDt("Observation/1"), true);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return Observation.class;
}
@Update
public MethodOutcome update(@ResourceParam Observation theObservation) {
ourLog.info("Received Listener Update");
ourUpdatedObservations.add(theObservation);
return new MethodOutcome(new IdDt("Observation/1"), false);
}
}
}

View File

@ -125,6 +125,14 @@
Fix XhtmlParser to correctly handle hexadecimal escaped literals. Thanks to Fix XhtmlParser to correctly handle hexadecimal escaped literals. Thanks to
Gijsbert van den Brink for the Pull Request! Gijsbert van den Brink for the Pull Request!
</action> </action>
<action type="add">
JPA server now has configurable properties that allow referential integrity
to be disabled for both writes and deletes. This is useful in some cases
where data integrity is not wanted or not possible. It can also be useful
if you want to delete large amounts of interconnected data quickly.
<![CDATA[<br/><br/>]]>
A corresponding flag has been added to the CLI tool as well.
</action>
</release> </release>
<release version="2.4" date="2017-04-19"> <release version="2.4" date="2017-04-19">
<action type="add"> <action type="add">