diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java index 97bf6e80bc2..a9f9aff9a17 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3; 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_ALLOW_EXTERNAL_REFS = "allow-external-refs"; 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(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, DISABLE_REFERENTIAL_INTEGRITY, false, "If this flag is set, the server will not enforce referential integrity"); return options; } diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/script/hapi-fhir-cli b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/script/hapi-fhir-cli index 299125d0315..2e9a0addfde 100755 --- a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/script/hapi-fhir-cli +++ b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/script/hapi-fhir-cli @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/ContextHolder.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/ContextHolder.java index 9843469cb82..5675424db19 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/ContextHolder.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/ContextHolder.java @@ -9,6 +9,7 @@ public class ContextHolder { private static boolean ourAllowExternalRefs; private static FhirContext ourCtx; + private static boolean ourDisableReferentialIntegrity; private static String ourPath; public static FhirContext getCtx() { @@ -25,6 +26,10 @@ public class ContextHolder { return ourAllowExternalRefs; } + public static boolean isDisableReferentialIntegrity() { + return ourDisableReferentialIntegrity; + } + public static void setAllowExternalRefs(boolean theAllowExternalRefs) { ourAllowExternalRefs = theAllowExternalRefs; } @@ -43,5 +48,9 @@ public class ContextHolder { ourCtx = theCtx; } + + public static void setDisableReferentialIntegrity(boolean theDisableReferentialIntegrity) { + ourDisableReferentialIntegrity = theDisableReferentialIntegrity; + } } diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 031d2de4ade..18ccf90f5e3 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -168,6 +168,8 @@ public class JpaServerDemo extends RestfulServer { DaoConfig daoConfig = myAppCtx.getBean(DaoConfig.class); daoConfig.setAllowExternalReferences(ContextHolder.isAllowExternalRefs()); + daoConfig.setEnforceReferentialIntegrityOnDelete(!ContextHolder.isDisableReferentialIntegrity()); + daoConfig.setEnforceReferentialIntegrityOnWrite(!ContextHolder.isDisableReferentialIntegrity()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java index 4657e0b11c3..ba0818dbb42 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java @@ -122,7 +122,7 @@ public class BaseDstu2Config extends BaseConfig { @Bean @Lazy - public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu3Interceptor() { + public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu2Interceptor() { return new RestHookSubscriptionDstu2Interceptor(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 7dc76dcf415..39edb263f08 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -378,6 +378,9 @@ public abstract class BaseHapiFhirDao implements IDao { try { valueOf = translateForcedIdToPid(typeString, id); } catch (ResourceNotFoundException e) { + if (myConfig.isEnforceReferentialIntegrityOnWrite() == false) { + continue; + } String resName = getContext().getResourceDefinition(type).getName(); throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 4c72a1d9e75..1d6f5edd318 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -43,6 +43,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; 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.ISearchResultDao; import ca.uhn.fhir.jpa.entity.*; @@ -79,6 +80,8 @@ public abstract class BaseHapiFhirResourceDao extends B protected PlatformTransactionManager myPlatformTransactionManager; @Autowired private IResourceHistoryTableDao myResourceHistoryTableDao; + @Autowired + private IResourceLinkDao myResourceLinkDao; private String myResourceName; @Autowired protected IResourceTableDao myResourceTableDao; @@ -87,8 +90,10 @@ public abstract class BaseHapiFhirResourceDao extends B protected IFulltextSearchSvc mySearchDao; @Autowired() protected ISearchResultDao mySearchResultDao; + private String mySecondaryPrimaryKeyParamName; + @Override public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { StopWatch w = new StopWatch(); @@ -117,13 +122,12 @@ public abstract class BaseHapiFhirResourceDao extends B ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() }); } - @Override public DaoMethodOutcome create(final T theResource) { return create(theResource, null, true, null); } - + @Override public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) { return create(theResource, null, true, theRequestDetails); @@ -175,7 +179,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override - public DaoMethodOutcome delete(IIdType theId, List deleteConflicts, RequestDetails theRequestDetails) { + public DaoMethodOutcome delete(IIdType theId, List theDeleteConflicts, RequestDetails theRequestDetails) { if (theId == null || !theId.hasIdPart()) { throw new InvalidRequestException("Can not perform delete, no ID provided"); } @@ -188,7 +192,7 @@ public abstract class BaseHapiFhirResourceDao extends B T resourceToDelete = toResource(myResourceType, entity, false); - validateOkToDelete(deleteConflicts, entity); + validateOkToDelete(theDeleteConflicts, entity); preDelete(resourceToDelete, entity); @@ -311,7 +315,7 @@ public abstract class BaseHapiFhirResourceDao extends B retVal.setOperationOutcome(oo); return retVal; } - + @Override public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) { List deleteConflicts = new ArrayList(); @@ -322,7 +326,7 @@ public abstract class BaseHapiFhirResourceDao extends B return outcome; } - + @PostConstruct public void detectSearchDaoDisabled() { if (mySearchDao != null && mySearchDao.isDisabled()) { @@ -615,6 +619,7 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } + @Override public MT metaGetOperation(Class theType, IIdType theId, RequestDetails theRequestDetails) { // Notify interceptors @@ -636,7 +641,6 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } - @Override public MT metaGetOperation(Class theType, RequestDetails theRequestDetails) { // Notify interceptors @@ -851,7 +855,7 @@ public abstract class BaseHapiFhirResourceDao extends B public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) { removeTag(theId, theTagType, theScheme, theTerm, null); } - + @Override public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequestDetails) { // Notify interceptors @@ -885,7 +889,7 @@ public abstract class BaseHapiFhirResourceDao extends B ourLog.info("Processed remove tag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId.getValue(), w.getMillisAndRestart() }); } - + @Transactional(propagation=Propagation.SUPPORTS) @Override public IBundleProvider search(final SearchParameterMap theParams) { @@ -911,6 +915,9 @@ public abstract class BaseHapiFhirResourceDao extends B return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName()); } + + + @Override public Set searchForIds(SearchParameterMap theParams) { @@ -929,9 +936,6 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } - - - @SuppressWarnings("unchecked") @Required public void setResourceType(Class theTableType) { @@ -1157,13 +1161,13 @@ public abstract class BaseHapiFhirResourceDao extends B ourLog.info(msg); return outcome; } - + + @Override public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails); } - - + @Override public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { return update(theResource, theMatchUrl, true, theRequestDetails); @@ -1200,7 +1204,7 @@ public abstract class BaseHapiFhirResourceDao extends B } } } - + protected void validateOkToDelete(List theDeleteConflicts, ResourceTable theEntity) { TypedQuery query = myEntityManager.createQuery("SELECT l FROM ResourceLink l WHERE l.myTargetResourcePid = :target_pid", ResourceLink.class); query.setParameter("target_pid", theEntity.getId()); @@ -1210,6 +1214,12 @@ public abstract class BaseHapiFhirResourceDao extends B 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); IdDt targetId = theEntity.getIdDt(); IdDt sourceId = link.getSourceResource().getIdDt(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 67feeaada4d..067a07e32a1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -1,6 +1,11 @@ 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.Validate; @@ -78,11 +83,14 @@ public class DaoConfig { private boolean myDeleteStaleSearches = true; + private boolean myEnforceReferentialIntegrityOnDelete = true; + + private boolean myEnforceReferentialIntegrityOnWrite = true; + // *** // update setter javadoc if default changes // *** private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR; - private int myHardTagListLimit = 1000; private int myIncludeLimit = 2000; @@ -90,7 +98,6 @@ public class DaoConfig { // update setter javadoc if default changes // *** private boolean myIndexContainedResources = true; - private List myInterceptors; // *** // update setter javadoc if default changes @@ -323,6 +330,39 @@ public class DaoConfig { return myDefaultSearchParamsCanBeOverridden; } + /** + * If set to false (default is true) resources will be permitted to be + * deleted even if other resources currently contain references to them. + *

+ * 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. + *

+ * + * @return + */ + public boolean isEnforceReferentialIntegrityOnDelete() { + return myEnforceReferentialIntegrityOnDelete; + } + + /** + * If set to false (default is true) resources will be permitted to be + * created or updated even if they contain references to local resources that do not exist. + *

+ * For example, if a patient contains a reference to managing organization Organization/FOO + * 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 false + *

+ *

+ * 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. + *

+ */ + public boolean isEnforceReferentialIntegrityOnWrite() { + return myEnforceReferentialIntegrityOnWrite; + } + /** * If this is set to false (default is true) the stale search deletion * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. @@ -356,7 +396,7 @@ public class DaoConfig { /** * If set to {@literal true} (default is true), if a client performs an update which does not actually - * result in any chance to a given resource (e.g. an update where the resource body matches the + * result in any chance to a given resource (e.g. an update where the resource body matches the * existing resource body in the database) the operation will succeed but a new version (and corresponding history * entry) will not actually be created. The existing resource version will be returned to the client. *

@@ -445,6 +485,37 @@ public class DaoConfig { myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize; } + /** + * If set to false (default is true) resources will be permitted to be + * deleted even if other resources currently contain references to them. + *

+ * 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. + *

+ */ + public void setEnforceReferentialIntegrityOnDelete(boolean theEnforceReferentialIntegrityOnDelete) { + myEnforceReferentialIntegrityOnDelete = theEnforceReferentialIntegrityOnDelete; + } + + /** + * If set to false (default is true) resources will be permitted to be + * created or updated even if they contain references to local resources that do not exist. + *

+ * For example, if a patient contains a reference to managing organization Organization/FOO + * 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 false + *

+ *

+ * 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. + *

+ */ + public void setEnforceReferentialIntegrityOnWrite(boolean theEnforceReferentialIntegrityOnWrite) { + myEnforceReferentialIntegrityOnWrite = theEnforceReferentialIntegrityOnWrite; + } + /** * If this is set to false (default is true) the stale search deletion * task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION. @@ -552,7 +623,7 @@ public class DaoConfig { public void setMaximumSearchResultCountInTransaction(int theMaximumSearchResultCountInTransaction) { myMaximumSearchResultCountInTransaction = theMaximumSearchResultCountInTransaction; } - + public void setResourceEncoding(ResourceEncodingEnum theResourceEncoding) { myResourceEncoding = theResourceEncoding; } @@ -602,7 +673,7 @@ public class DaoConfig { /** * If set to {@literal true} (default is true), if a client performs an update which does not actually - * result in any chance to a given resource (e.g. an update where the resource body matches the + * result in any chance to a given resource (e.g. an update where the resource body matches the * existing resource body in the database) the operation will succeed but a new version (and corresponding history * entry) will not actually be created. The existing resource version will be returned to the client. *

@@ -611,7 +682,7 @@ public class DaoConfig { */ public void setSuppressUpdatesWithNoChange(boolean theSuppressUpdatesWithNoChange) { mySuppressUpdatesWithNoChange = theSuppressUpdatesWithNoChange; - + } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java new file mode 100644 index 00000000000..c9f6ab0cea9 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -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 { + // nothing +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java index 60905ae9d1f..18e859ecbd2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java @@ -31,6 +31,7 @@ import javax.persistence.Query; import org.apache.commons.lang3.time.DateUtils; 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.instance.model.api.IAnyResource; 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.ParamPrefixEnum; 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.exceptions.UnprocessableEntityException; @@ -333,6 +335,21 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 myRestHookSubscriptions = new ArrayList(); + @Autowired @Qualifier("mySubscriptionDaoDstu2") private IFhirResourceDao mySubscriptionDao; @@ -126,7 +127,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp } HttpUriRequest request = null; - String resourceName = myCtx.getResourceDefinition(theResource).getName(); + String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); String payload = theSubscription.getChannel().getPayload(); String resourceId = theResource.getIdElement().getIdPart(); @@ -223,7 +224,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp } 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 public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { IIdType idType = theResource.getIdElement(); - ourLog.info("resource created type: {}", theRequest.getResourceName()); + ourLog.info("resource created type: {}", getResourceName(theResource)); if (theResource instanceof Subscription) { Subscription subscription = (Subscription) theResource; @@ -320,7 +321,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp ourLog.info("Subscription was added. Id: " + subscription.getId()); } } 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) { this.myNotifyOnDelete = notifyOnDelete; } + + public void setSubscriptionDao(IFhirResourceDao theSubscriptionDao) { + mySubscriptionDao = theSubscriptionDao; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java index 5535ad8ec39..ac913113568 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java @@ -68,7 +68,16 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class); @Autowired - private FhirContext myCtx; + private FhirContext myFhirContext; + + public void setFhirContext(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + public void setSubscriptionDao(IFhirResourceDao theSubscriptionDao) { + mySubscriptionDao = theSubscriptionDao; + } + @Autowired @Qualifier("mySubscriptionDaoDstu3") private IFhirResourceDao mySubscriptionDao; @@ -125,7 +134,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp } HttpUriRequest request = null; - String resourceName = myCtx.getResourceDefinition(theResource).getName(); + String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); String payload = theSubscription.getChannel().getPayload(); String resourceId = theResource.getIdElement().getIdPart(); @@ -215,7 +224,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp } 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 public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { IIdType idType = theResource.getIdElement(); - ourLog.info("resource created type: {}", theRequest.getResourceName()); + ourLog.info("resource created type: {}", getResourceName(theResource)); if (theResource instanceof Subscription) { 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()); } } else { - checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE); + checkSubscriptions(idType, getResourceName(theResource), RestOperationTypeEnum.CREATE); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1Test.java index 4f4952780fb..ba86c977d74 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1Test.java @@ -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.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.TestUtil; @SuppressWarnings("unused") diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index ec880449e0e..7537c60e7f1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -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.CodingDt; 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.rest.method.MethodUtil; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ReferentialIntegrityTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ReferentialIntegrityTest.java new file mode 100644 index 00000000000..defdd70af4a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ReferentialIntegrityTest.java @@ -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); + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SubscriptionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SubscriptionTest.java index 205edf9c17c..c24505302e2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SubscriptionTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SubscriptionTest.java @@ -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.ISubscriptionTableDao; 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.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @@ -170,6 +171,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test { public void testCreateSubscriptionInvalidCriteria() { Subscription subs = new Subscription(); subs.setStatus(SubscriptionStatus.REQUESTED); + subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); subs.setCriteria("Observation"); try { mySubscriptionDao.create(subs, mySrd); @@ -180,6 +182,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test { subs = new Subscription(); subs.setStatus(SubscriptionStatus.REQUESTED); + subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); subs.setCriteria("http://foo.com/Observation?AAA=BBB"); try { mySubscriptionDao.create(subs, mySrd); @@ -190,6 +193,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test { subs = new Subscription(); subs.setStatus(SubscriptionStatus.REQUESTED); + subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); subs.setCriteria("ObservationZZZZ?a=b"); try { mySubscriptionDao.create(subs, mySrd); @@ -205,15 +209,58 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test { mySubscriptionDao.create(subs, mySrd); fail(); } 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.setStatus(SubscriptionStatus.REQUESTED); subs.setCriteria("Observation?identifier=123"); subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); 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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java index 9639ec9a27d..1cc9dfa5651 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu1Test.java @@ -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.OrganizationResourceProvider; 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.dstu.resource.Observation; 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.rest.client.IGenericClient; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; public class SystemProviderDstu1Test extends BaseJpaTest { @@ -100,7 +100,7 @@ public class SystemProviderDstu1Test extends BaseJpaTest { JpaSystemProviderDstu1 systemProv = ourAppCtx.getBean(JpaSystemProviderDstu1.class, "mySystemProviderDstu1"); restServer.setPlainProviders(systemProv); - int myPort = RandomServerPortProvider.findFreePort(); + int myPort = PortUtil.findFreePort(); ourServer = new Server(myPort); ServletContextHandler proxyHandler = new ServletContextHandler(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java index cf3fb00d70d..ebd7c5c34fd 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java @@ -34,7 +34,6 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; 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.narrative.DefaultThymeleafNarrativeGenerator; 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.server.RestfulServer; import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; +import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { @@ -77,7 +77,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); if (ourServer == null) { - ourPort = RandomServerPortProvider.findFreePort(); + ourPort = PortUtil.findFreePort(); ourRestServer = new RestfulServer(myFhirCtx); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java index 4d232366a1e..3d7b5b18bac 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java @@ -9,25 +9,34 @@ 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.*; +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.jpa.testutil.RandomServerPortProvider; 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.*; +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.*; +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 @@ -250,7 +259,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { @BeforeClass public static void startListenerServer() throws Exception { - ourListenerPort = RandomServerPortProvider.findFreePort(); + ourListenerPort = PortUtil.findFreePort(); ourListenerRestServer = new RestfulServer(FhirContext.forDstu2()); ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java index bdf7f401a2d..d78a716f4d6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java @@ -3,8 +3,11 @@ package ca.uhn.fhir.jpa.subscription; import static org.junit.Assert.*; +import java.util.ArrayList; import java.util.List; +import javax.servlet.http.HttpServletRequest; + import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; 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.rest.annotation.*; 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.RestfulServer; @@ -29,12 +33,14 @@ import ca.uhn.fhir.rest.server.RestfulServer; */ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { + private static List ourContentTypes = new ArrayList(); private static List 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(RestHookTestDstu3Test.class); + private static List ourUpdatedObservations = Lists.newArrayList(); @After @@ -57,18 +63,19 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { public void beforeReset() { ourCreatedObservations.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.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); - subscription.setCriteria(criteria); + subscription.setCriteria(theCriteria); Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); - channel.setPayload(payload); - channel.setEndpoint(endpoint); + channel.setPayload(thePayload); + channel.setEndpoint(theEndpoint); subscription.setChannel(channel); MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); @@ -94,9 +101,29 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { return observation; } - + @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 code = "1000000050"; @@ -112,6 +139,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { Thread.sleep(500); assertEquals(1, ourCreatedObservations.size()); assertEquals(0, ourUpdatedObservations.size()); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); Assert.assertNotNull(subscriptionTemp); @@ -169,7 +197,28 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { } @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 code = "1000000050"; @@ -185,6 +234,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { Thread.sleep(500); assertEquals(1, ourCreatedObservations.size()); assertEquals(0, ourUpdatedObservations.size()); + assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); Assert.assertNotNull(subscriptionTemp); @@ -240,7 +290,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { Assert.assertFalse(observation1.getId().isEmpty()); Assert.assertFalse(observation2.getId().isEmpty()); } - + @BeforeClass public static void startListenerServer() throws Exception { ourListenerPort = RandomServerPortProvider.findFreePort(); @@ -271,8 +321,9 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { public static class ObservationListener implements IResourceProvider { @Create - public MethodOutcome create(@ResourceParam Observation theObservation) { + public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { ourLog.info("Received Listener Create"); + ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); ourCreatedObservations.add(theObservation); return new MethodOutcome(new IdType("Observation/1"), true); } @@ -283,9 +334,10 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { } @Update - public MethodOutcome update(@ResourceParam Observation theObservation) { + public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { ourLog.info("Received Listener Update"); ourUpdatedObservations.add(theObservation); + ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); return new MethodOutcome(new IdType("Observation/1"), false); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java new file mode 100644 index 00000000000..cf10502e1b5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java @@ -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 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 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 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); + } + + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b2ef4f65cd4..ab7a3354813 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -125,6 +125,14 @@ Fix XhtmlParser to correctly handle hexadecimal escaped literals. Thanks to Gijsbert van den Brink for the Pull Request! + + 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. +
]]> + A corresponding flag has been added to the CLI tool as well. +