From 1ec180628fd7207ef93580b63e754aacac3ba34e Mon Sep 17 00:00:00 2001 From: James Date: Mon, 22 May 2017 15:34:44 -0400 Subject: [PATCH] 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. --- .../ca/uhn/fhir/cli/RunServerCommand.java | 2 + .../src/main/script/hapi-fhir-cli | 2 +- .../ca/uhn/fhir/jpa/demo/ContextHolder.java | 9 + .../ca/uhn/fhir/jpa/demo/JpaServerDemo.java | 2 + .../uhn/fhir/jpa/config/BaseDstu2Config.java | 2 +- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 3 + .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 42 ++- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 85 ++++- .../fhir/jpa/dao/data/IResourceLinkDao.java | 29 ++ .../FhirResourceDaoSubscriptionDstu3.java | 17 + .../RestHookSubscriptionDstu2Interceptor.java | 21 +- .../RestHookSubscriptionDstu3Interceptor.java | 19 +- .../jpa/dao/FhirResourceDaoDstu1Test.java | 1 - .../fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java | 24 +- ...ourceDaoDstu3ReferentialIntegrityTest.java | 99 ++++++ .../FhirResourceDaoDstu3SubscriptionTest.java | 49 ++- .../jpa/provider/SystemProviderDstu1Test.java | 4 +- .../dstu3/BaseResourceProviderDstu3Test.java | 4 +- .../subscription/RestHookTestDstu2Test.java | 19 +- .../subscription/RestHookTestDstu3Test.java | 72 +++- ...rceptorRegisteredToDaoConfigDstu2Test.java | 309 ++++++++++++++++++ src/changes/changes.xml | 8 + 22 files changed, 764 insertions(+), 58 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ReferentialIntegrityTest.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java 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. +