JPA server now has configurable properties that allow referential integrity to be disabled for both writes and deletes. This is useful in some cases where data integrity is not wanted or not possible. It can also be useful if you want to delete large amounts interconnected data quickly.
This commit is contained in:
parent
a834770e38
commit
1ec180628f
|
@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3;
|
||||||
|
|
||||||
public class RunServerCommand extends BaseCommand {
|
public class RunServerCommand extends BaseCommand {
|
||||||
|
|
||||||
|
private static final String DISABLE_REFERENTIAL_INTEGRITY = "disable-referential-integrity";
|
||||||
private static final String OPTION_LOWMEM = "lowmem";
|
private static final String OPTION_LOWMEM = "lowmem";
|
||||||
private static final String OPTION_ALLOW_EXTERNAL_REFS = "allow-external-refs";
|
private static final String OPTION_ALLOW_EXTERNAL_REFS = "allow-external-refs";
|
||||||
private static final int DEFAULT_PORT = 8080;
|
private static final int DEFAULT_PORT = 8080;
|
||||||
|
@ -49,6 +50,7 @@ public class RunServerCommand extends BaseCommand {
|
||||||
options.addOption(OPTION_P, "port", true, "The port to listen on (default is " + DEFAULT_PORT + ")");
|
options.addOption(OPTION_P, "port", true, "The port to listen on (default is " + DEFAULT_PORT + ")");
|
||||||
options.addOption(null, OPTION_LOWMEM, false, "If this flag is set, the server will operate in low memory mode (some features disabled)");
|
options.addOption(null, OPTION_LOWMEM, false, "If this flag is set, the server will operate in low memory mode (some features disabled)");
|
||||||
options.addOption(null, OPTION_ALLOW_EXTERNAL_REFS, false, "If this flag is set, the server will allow resources to be persisted contaning external resource references");
|
options.addOption(null, OPTION_ALLOW_EXTERNAL_REFS, false, "If this flag is set, the server will allow resources to be persisted contaning external resource references");
|
||||||
|
options.addOption(null, DISABLE_REFERENTIAL_INTEGRITY, false, "If this flag is set, the server will not enforce referential integrity");
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
# or more contributor license agreements. See the NOTICE file
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
|
|
@ -9,6 +9,7 @@ public class ContextHolder {
|
||||||
|
|
||||||
private static boolean ourAllowExternalRefs;
|
private static boolean ourAllowExternalRefs;
|
||||||
private static FhirContext ourCtx;
|
private static FhirContext ourCtx;
|
||||||
|
private static boolean ourDisableReferentialIntegrity;
|
||||||
private static String ourPath;
|
private static String ourPath;
|
||||||
|
|
||||||
public static FhirContext getCtx() {
|
public static FhirContext getCtx() {
|
||||||
|
@ -25,6 +26,10 @@ public class ContextHolder {
|
||||||
return ourAllowExternalRefs;
|
return ourAllowExternalRefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDisableReferentialIntegrity() {
|
||||||
|
return ourDisableReferentialIntegrity;
|
||||||
|
}
|
||||||
|
|
||||||
public static void setAllowExternalRefs(boolean theAllowExternalRefs) {
|
public static void setAllowExternalRefs(boolean theAllowExternalRefs) {
|
||||||
ourAllowExternalRefs = theAllowExternalRefs;
|
ourAllowExternalRefs = theAllowExternalRefs;
|
||||||
}
|
}
|
||||||
|
@ -44,4 +49,8 @@ public class ContextHolder {
|
||||||
ourCtx = theCtx;
|
ourCtx = theCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setDisableReferentialIntegrity(boolean theDisableReferentialIntegrity) {
|
||||||
|
ourDisableReferentialIntegrity = theDisableReferentialIntegrity;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,8 @@ public class JpaServerDemo extends RestfulServer {
|
||||||
|
|
||||||
DaoConfig daoConfig = myAppCtx.getBean(DaoConfig.class);
|
DaoConfig daoConfig = myAppCtx.getBean(DaoConfig.class);
|
||||||
daoConfig.setAllowExternalReferences(ContextHolder.isAllowExternalRefs());
|
daoConfig.setAllowExternalReferences(ContextHolder.isAllowExternalRefs());
|
||||||
|
daoConfig.setEnforceReferentialIntegrityOnDelete(!ContextHolder.isDisableReferentialIntegrity());
|
||||||
|
daoConfig.setEnforceReferentialIntegrityOnWrite(!ContextHolder.isDisableReferentialIntegrity());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,7 @@ public class BaseDstu2Config extends BaseConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Lazy
|
@Lazy
|
||||||
public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu3Interceptor() {
|
public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu2Interceptor() {
|
||||||
return new RestHookSubscriptionDstu2Interceptor();
|
return new RestHookSubscriptionDstu2Interceptor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -378,6 +378,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
|
||||||
try {
|
try {
|
||||||
valueOf = translateForcedIdToPid(typeString, id);
|
valueOf = translateForcedIdToPid(typeString, id);
|
||||||
} catch (ResourceNotFoundException e) {
|
} catch (ResourceNotFoundException e) {
|
||||||
|
if (myConfig.isEnforceReferentialIntegrityOnWrite() == false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String resName = getContext().getResourceDefinition(type).getName();
|
String resName = getContext().getResourceDefinition(type).getName();
|
||||||
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
|
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
|
||||||
|
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
|
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
|
||||||
import ca.uhn.fhir.jpa.entity.*;
|
import ca.uhn.fhir.jpa.entity.*;
|
||||||
|
@ -79,6 +80,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
protected PlatformTransactionManager myPlatformTransactionManager;
|
protected PlatformTransactionManager myPlatformTransactionManager;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IResourceHistoryTableDao myResourceHistoryTableDao;
|
private IResourceHistoryTableDao myResourceHistoryTableDao;
|
||||||
|
@Autowired
|
||||||
|
private IResourceLinkDao myResourceLinkDao;
|
||||||
private String myResourceName;
|
private String myResourceName;
|
||||||
@Autowired
|
@Autowired
|
||||||
protected IResourceTableDao myResourceTableDao;
|
protected IResourceTableDao myResourceTableDao;
|
||||||
|
@ -87,8 +90,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
protected IFulltextSearchSvc mySearchDao;
|
protected IFulltextSearchSvc mySearchDao;
|
||||||
@Autowired()
|
@Autowired()
|
||||||
protected ISearchResultDao mySearchResultDao;
|
protected ISearchResultDao mySearchResultDao;
|
||||||
|
|
||||||
private String mySecondaryPrimaryKeyParamName;
|
private String mySecondaryPrimaryKeyParamName;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
|
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
|
||||||
StopWatch w = new StopWatch();
|
StopWatch w = new StopWatch();
|
||||||
|
@ -118,7 +123,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() });
|
ourLog.info("Processed addTag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId, w.getMillisAndRestart() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DaoMethodOutcome create(final T theResource) {
|
public DaoMethodOutcome create(final T theResource) {
|
||||||
return create(theResource, null, true, null);
|
return create(theResource, null, true, null);
|
||||||
|
@ -175,7 +179,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> deleteConflicts, RequestDetails theRequestDetails) {
|
public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theRequestDetails) {
|
||||||
if (theId == null || !theId.hasIdPart()) {
|
if (theId == null || !theId.hasIdPart()) {
|
||||||
throw new InvalidRequestException("Can not perform delete, no ID provided");
|
throw new InvalidRequestException("Can not perform delete, no ID provided");
|
||||||
}
|
}
|
||||||
|
@ -188,7 +192,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
|
|
||||||
T resourceToDelete = toResource(myResourceType, entity, false);
|
T resourceToDelete = toResource(myResourceType, entity, false);
|
||||||
|
|
||||||
validateOkToDelete(deleteConflicts, entity);
|
validateOkToDelete(theDeleteConflicts, entity);
|
||||||
|
|
||||||
preDelete(resourceToDelete, entity);
|
preDelete(resourceToDelete, entity);
|
||||||
|
|
||||||
|
@ -615,6 +619,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequestDetails) {
|
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequestDetails) {
|
||||||
// Notify interceptors
|
// Notify interceptors
|
||||||
|
@ -636,7 +641,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
|
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
|
||||||
// Notify interceptors
|
// Notify interceptors
|
||||||
|
@ -911,6 +915,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName());
|
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Long> searchForIds(SearchParameterMap theParams) {
|
public Set<Long> searchForIds(SearchParameterMap theParams) {
|
||||||
|
|
||||||
|
@ -929,9 +936,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Required
|
@Required
|
||||||
public void setResourceType(Class<? extends IBaseResource> theTableType) {
|
public void setResourceType(Class<? extends IBaseResource> theTableType) {
|
||||||
|
@ -1158,12 +1162,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
|
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
|
||||||
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
|
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
|
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
|
||||||
return update(theResource, theMatchUrl, true, theRequestDetails);
|
return update(theResource, theMatchUrl, true, theRequestDetails);
|
||||||
|
@ -1210,6 +1214,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (myDaoConfig.isEnforceReferentialIntegrityOnDelete() == false) {
|
||||||
|
ourLog.info("Deleting {} resource dependencies which can no longer be satisfied", resultList.size());
|
||||||
|
myResourceLinkDao.delete(resultList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ResourceLink link = resultList.get(0);
|
ResourceLink link = resultList.get(0);
|
||||||
IdDt targetId = theEntity.getIdDt();
|
IdDt targetId = theEntity.getIdDt();
|
||||||
IdDt sourceId = link.getSourceResource().getIdDt();
|
IdDt sourceId = link.getSourceResource().getIdDt();
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
package ca.uhn.fhir.jpa.dao;
|
package ca.uhn.fhir.jpa.dao;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
@ -78,11 +83,14 @@ public class DaoConfig {
|
||||||
|
|
||||||
private boolean myDeleteStaleSearches = true;
|
private boolean myDeleteStaleSearches = true;
|
||||||
|
|
||||||
|
private boolean myEnforceReferentialIntegrityOnDelete = true;
|
||||||
|
|
||||||
|
private boolean myEnforceReferentialIntegrityOnWrite = true;
|
||||||
|
|
||||||
// ***
|
// ***
|
||||||
// update setter javadoc if default changes
|
// update setter javadoc if default changes
|
||||||
// ***
|
// ***
|
||||||
private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR;
|
private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR;
|
||||||
|
|
||||||
private int myHardTagListLimit = 1000;
|
private int myHardTagListLimit = 1000;
|
||||||
|
|
||||||
private int myIncludeLimit = 2000;
|
private int myIncludeLimit = 2000;
|
||||||
|
@ -90,7 +98,6 @@ public class DaoConfig {
|
||||||
// update setter javadoc if default changes
|
// update setter javadoc if default changes
|
||||||
// ***
|
// ***
|
||||||
private boolean myIndexContainedResources = true;
|
private boolean myIndexContainedResources = true;
|
||||||
|
|
||||||
private List<IServerInterceptor> myInterceptors;
|
private List<IServerInterceptor> myInterceptors;
|
||||||
// ***
|
// ***
|
||||||
// update setter javadoc if default changes
|
// update setter javadoc if default changes
|
||||||
|
@ -323,6 +330,39 @@ public class DaoConfig {
|
||||||
return myDefaultSearchParamsCanBeOverridden;
|
return myDefaultSearchParamsCanBeOverridden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
|
||||||
|
* deleted even if other resources currently contain references to them.
|
||||||
|
* <p>
|
||||||
|
* This property can cause confusing results for clients of the server since searches, includes,
|
||||||
|
* and other FHIR features may not behave as expected when referential integrity is not
|
||||||
|
* preserved. Use this feature with caution.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isEnforceReferentialIntegrityOnDelete() {
|
||||||
|
return myEnforceReferentialIntegrityOnDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
|
||||||
|
* created or updated even if they contain references to local resources that do not exist.
|
||||||
|
* <p>
|
||||||
|
* For example, if a patient contains a reference to managing organization <code>Organization/FOO</code>
|
||||||
|
* but FOO is not a valid ID for an organization on the server, the operation will be blocked unless
|
||||||
|
* this propery has been set to <code>false</code>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This property can cause confusing results for clients of the server since searches, includes,
|
||||||
|
* and other FHIR features may not behave as expected when referential integrity is not
|
||||||
|
* preserved. Use this feature with caution.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public boolean isEnforceReferentialIntegrityOnWrite() {
|
||||||
|
return myEnforceReferentialIntegrityOnWrite;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
|
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
|
||||||
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
|
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
|
||||||
|
@ -445,6 +485,37 @@ public class DaoConfig {
|
||||||
myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize;
|
myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
|
||||||
|
* deleted even if other resources currently contain references to them.
|
||||||
|
* <p>
|
||||||
|
* This property can cause confusing results for clients of the server since searches, includes,
|
||||||
|
* and other FHIR features may not behave as expected when referential integrity is not
|
||||||
|
* preserved. Use this feature with caution.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public void setEnforceReferentialIntegrityOnDelete(boolean theEnforceReferentialIntegrityOnDelete) {
|
||||||
|
myEnforceReferentialIntegrityOnDelete = theEnforceReferentialIntegrityOnDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to <code>false</code> (default is <code>true</code>) resources will be permitted to be
|
||||||
|
* created or updated even if they contain references to local resources that do not exist.
|
||||||
|
* <p>
|
||||||
|
* For example, if a patient contains a reference to managing organization <code>Organization/FOO</code>
|
||||||
|
* but FOO is not a valid ID for an organization on the server, the operation will be blocked unless
|
||||||
|
* this propery has been set to <code>false</code>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This property can cause confusing results for clients of the server since searches, includes,
|
||||||
|
* and other FHIR features may not behave as expected when referential integrity is not
|
||||||
|
* preserved. Use this feature with caution.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public void setEnforceReferentialIntegrityOnWrite(boolean theEnforceReferentialIntegrityOnWrite) {
|
||||||
|
myEnforceReferentialIntegrityOnWrite = theEnforceReferentialIntegrityOnWrite;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
|
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
|
||||||
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
|
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR JPA Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2017 University Health Network
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.entity.ResourceLink;
|
||||||
|
|
||||||
|
public interface IResourceLinkDao extends JpaRepository<ResourceLink, Long> {
|
||||||
|
// nothing
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import javax.persistence.Query;
|
||||||
|
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.hl7.fhir.dstu3.model.Subscription;
|
import org.hl7.fhir.dstu3.model.Subscription;
|
||||||
|
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType;
|
||||||
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus;
|
import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus;
|
||||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
@ -67,6 +68,7 @@ import ca.uhn.fhir.rest.param.DateParam;
|
||||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||||
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
||||||
import ca.uhn.fhir.rest.server.Constants;
|
import ca.uhn.fhir.rest.server.Constants;
|
||||||
|
import ca.uhn.fhir.rest.server.EncodingEnum;
|
||||||
import ca.uhn.fhir.rest.server.IBundleProvider;
|
import ca.uhn.fhir.rest.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
|
||||||
|
@ -333,6 +335,21 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (theResource.getChannel().getType() == null) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.channel.type must be populated");
|
||||||
|
} else if (theResource.getChannel().getType() == SubscriptionChannelType.RESTHOOK) {
|
||||||
|
if (isBlank(theResource.getChannel().getPayload())) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.channel.payload must be populated for rest-hook subscriptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EncodingEnum.forContentType(theResource.getChannel().getPayload()) == null){
|
||||||
|
throw new UnprocessableEntityException("Invalid value for Subscription.channel.payload: " + theResource.getChannel().getPayload());
|
||||||
|
}
|
||||||
|
if (isBlank(theResource.getChannel().getEndpoint())){
|
||||||
|
throw new UnprocessableEntityException("Rest-hook subscriptions must have Subscription.channel.endpoint defined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RuntimeResourceDefinition resDef;
|
RuntimeResourceDefinition resDef;
|
||||||
try {
|
try {
|
||||||
resDef = getContext().getResourceDefinition(resType);
|
resDef = getContext().getResourceDefinition(resType);
|
||||||
|
|
|
@ -70,10 +70,11 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FhirContext myCtx;
|
private FhirContext myFhirContext;
|
||||||
private boolean myNotifyOnDelete = false;
|
|
||||||
|
|
||||||
|
private boolean myNotifyOnDelete = false;
|
||||||
private final List<Subscription> myRestHookSubscriptions = new ArrayList<Subscription>();
|
private final List<Subscription> myRestHookSubscriptions = new ArrayList<Subscription>();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("mySubscriptionDaoDstu2")
|
@Qualifier("mySubscriptionDaoDstu2")
|
||||||
private IFhirResourceDao<Subscription> mySubscriptionDao;
|
private IFhirResourceDao<Subscription> mySubscriptionDao;
|
||||||
|
@ -126,7 +127,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpUriRequest request = null;
|
HttpUriRequest request = null;
|
||||||
String resourceName = myCtx.getResourceDefinition(theResource).getName();
|
String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
|
||||||
|
|
||||||
String payload = theSubscription.getChannel().getPayload();
|
String payload = theSubscription.getChannel().getPayload();
|
||||||
String resourceId = theResource.getIdElement().getIdPart();
|
String resourceId = theResource.getIdElement().getIdPart();
|
||||||
|
@ -223,7 +224,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getResourceName(IBaseResource theResource) {
|
private String getResourceName(IBaseResource theResource) {
|
||||||
return myCtx.getResourceDefinition(theResource).getName();
|
return myFhirContext.getResourceDefinition(theResource).getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,7 +309,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
|
||||||
@Override
|
@Override
|
||||||
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
|
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
IIdType idType = theResource.getIdElement();
|
IIdType idType = theResource.getIdElement();
|
||||||
ourLog.info("resource created type: {}", theRequest.getResourceName());
|
ourLog.info("resource created type: {}", getResourceName(theResource));
|
||||||
|
|
||||||
if (theResource instanceof Subscription) {
|
if (theResource instanceof Subscription) {
|
||||||
Subscription subscription = (Subscription) theResource;
|
Subscription subscription = (Subscription) theResource;
|
||||||
|
@ -320,7 +321,7 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
|
||||||
ourLog.info("Subscription was added. Id: " + subscription.getId());
|
ourLog.info("Subscription was added. Id: " + subscription.getId());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE);
|
checkSubscriptions(idType, getResourceName(theResource), RestOperationTypeEnum.CREATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,7 +380,15 @@ public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter imp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFhirContext(FhirContext theFhirContext) {
|
||||||
|
myFhirContext = theFhirContext;
|
||||||
|
}
|
||||||
|
|
||||||
public void setNotifyOnDelete(boolean notifyOnDelete) {
|
public void setNotifyOnDelete(boolean notifyOnDelete) {
|
||||||
this.myNotifyOnDelete = notifyOnDelete;
|
this.myNotifyOnDelete = notifyOnDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSubscriptionDao(IFhirResourceDao<Subscription> theSubscriptionDao) {
|
||||||
|
mySubscriptionDao = theSubscriptionDao;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,16 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FhirContext myCtx;
|
private FhirContext myFhirContext;
|
||||||
|
|
||||||
|
public void setFhirContext(FhirContext theFhirContext) {
|
||||||
|
myFhirContext = theFhirContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubscriptionDao(IFhirResourceDao<Subscription> theSubscriptionDao) {
|
||||||
|
mySubscriptionDao = theSubscriptionDao;
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("mySubscriptionDaoDstu3")
|
@Qualifier("mySubscriptionDaoDstu3")
|
||||||
private IFhirResourceDao<Subscription> mySubscriptionDao;
|
private IFhirResourceDao<Subscription> mySubscriptionDao;
|
||||||
|
@ -125,7 +134,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpUriRequest request = null;
|
HttpUriRequest request = null;
|
||||||
String resourceName = myCtx.getResourceDefinition(theResource).getName();
|
String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
|
||||||
|
|
||||||
String payload = theSubscription.getChannel().getPayload();
|
String payload = theSubscription.getChannel().getPayload();
|
||||||
String resourceId = theResource.getIdElement().getIdPart();
|
String resourceId = theResource.getIdElement().getIdPart();
|
||||||
|
@ -215,7 +224,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getResourceName(IBaseResource theResource) {
|
private String getResourceName(IBaseResource theResource) {
|
||||||
return myCtx.getResourceDefinition(theResource).getName();
|
return myFhirContext.getResourceDefinition(theResource).getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -301,7 +310,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
|
||||||
@Override
|
@Override
|
||||||
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
|
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
IIdType idType = theResource.getIdElement();
|
IIdType idType = theResource.getIdElement();
|
||||||
ourLog.info("resource created type: {}", theRequest.getResourceName());
|
ourLog.info("resource created type: {}", getResourceName(theResource));
|
||||||
|
|
||||||
if (theResource instanceof Subscription) {
|
if (theResource instanceof Subscription) {
|
||||||
Subscription subscription = (Subscription) theResource;
|
Subscription subscription = (Subscription) theResource;
|
||||||
|
@ -313,7 +322,7 @@ public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter imp
|
||||||
ourLog.info("Subscription was added, id: {} - Have {}", subscription.getIdElement().getIdPart(), myRestHookSubscriptions.size());
|
ourLog.info("Subscription was added, id: {} - Have {}", subscription.getIdElement().getIdPart(), myRestHookSubscriptions.size());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE);
|
checkSubscriptions(idType, getResourceName(theResource), RestOperationTypeEnum.CREATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
|
|
@ -42,7 +42,29 @@ import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
|
||||||
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
|
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
|
||||||
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
||||||
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
|
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
|
||||||
import ca.uhn.fhir.model.dstu2.resource.*;
|
import ca.uhn.fhir.model.dstu2.resource.Appointment;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Bundle;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.ConceptMap;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Device;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.DiagnosticOrder;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Encounter;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Immunization;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Location;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Media;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Medication;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.MedicationAdministration;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.MedicationOrder;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Observation;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Organization;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Patient;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Practitioner;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Questionnaire;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.StructureDefinition;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Substance;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
|
||||||
import ca.uhn.fhir.parser.IParser;
|
import ca.uhn.fhir.parser.IParser;
|
||||||
import ca.uhn.fhir.rest.method.MethodUtil;
|
import ca.uhn.fhir.rest.method.MethodUtil;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.dao.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISubscriptionFlaggedResourceDataDao;
|
import ca.uhn.fhir.jpa.dao.data.ISubscriptionFlaggedResourceDataDao;
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
|
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
|
||||||
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
|
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
|
||||||
|
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
|
||||||
import ca.uhn.fhir.rest.server.IBundleProvider;
|
import ca.uhn.fhir.rest.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
@ -170,6 +171,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
|
||||||
public void testCreateSubscriptionInvalidCriteria() {
|
public void testCreateSubscriptionInvalidCriteria() {
|
||||||
Subscription subs = new Subscription();
|
Subscription subs = new Subscription();
|
||||||
subs.setStatus(SubscriptionStatus.REQUESTED);
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
|
||||||
subs.setCriteria("Observation");
|
subs.setCriteria("Observation");
|
||||||
try {
|
try {
|
||||||
mySubscriptionDao.create(subs, mySrd);
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
|
@ -180,6 +182,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
|
||||||
|
|
||||||
subs = new Subscription();
|
subs = new Subscription();
|
||||||
subs.setStatus(SubscriptionStatus.REQUESTED);
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
|
||||||
subs.setCriteria("http://foo.com/Observation?AAA=BBB");
|
subs.setCriteria("http://foo.com/Observation?AAA=BBB");
|
||||||
try {
|
try {
|
||||||
mySubscriptionDao.create(subs, mySrd);
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
|
@ -190,6 +193,7 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
|
||||||
|
|
||||||
subs = new Subscription();
|
subs = new Subscription();
|
||||||
subs.setStatus(SubscriptionStatus.REQUESTED);
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
|
||||||
subs.setCriteria("ObservationZZZZ?a=b");
|
subs.setCriteria("ObservationZZZZ?a=b");
|
||||||
try {
|
try {
|
||||||
mySubscriptionDao.create(subs, mySrd);
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
|
@ -205,7 +209,42 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
|
||||||
mySubscriptionDao.create(subs, mySrd);
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
fail();
|
fail();
|
||||||
} catch (UnprocessableEntityException e) {
|
} catch (UnprocessableEntityException e) {
|
||||||
assertThat(e.getMessage(), containsString("Subscription.channel.type must be populated on this server"));
|
assertThat(e.getMessage(), containsString("Subscription.channel.type must be populated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
subs = new Subscription();
|
||||||
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.setCriteria("Observation?identifier=123");
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
|
||||||
|
try {
|
||||||
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
|
fail();
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertThat(e.getMessage(), containsString("Subscription.channel.payload must be populated for rest-hook subscriptions"));
|
||||||
|
}
|
||||||
|
|
||||||
|
subs = new Subscription();
|
||||||
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.setCriteria("Observation?identifier=123");
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
|
||||||
|
subs.getChannel().setPayload("text/html");
|
||||||
|
try {
|
||||||
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
|
fail();
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertThat(e.getMessage(), containsString("Invalid value for Subscription.channel.payload: text/html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
subs = new Subscription();
|
||||||
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.setCriteria("Observation?identifier=123");
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
|
||||||
|
subs.getChannel().setPayload("application/fhir+xml");
|
||||||
|
try {
|
||||||
|
mySubscriptionDao.create(subs, mySrd);
|
||||||
|
fail();
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertThat(e.getMessage(), containsString("Rest-hook subscriptions must have Subscription.channel.endpoint defined"));
|
||||||
}
|
}
|
||||||
|
|
||||||
subs = new Subscription();
|
subs = new Subscription();
|
||||||
|
@ -214,6 +253,14 @@ public class FhirResourceDaoDstu3SubscriptionTest extends BaseJpaDstu3Test {
|
||||||
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
|
subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET);
|
||||||
assertTrue(mySubscriptionDao.create(subs, mySrd).getId().hasIdPart());
|
assertTrue(mySubscriptionDao.create(subs, mySrd).getId().hasIdPart());
|
||||||
|
|
||||||
|
subs = new Subscription();
|
||||||
|
subs.setStatus(SubscriptionStatus.REQUESTED);
|
||||||
|
subs.setCriteria("Observation?identifier=123");
|
||||||
|
subs.getChannel().setType(SubscriptionChannelType.RESTHOOK);
|
||||||
|
subs.getChannel().setPayload("application/fhir+json");
|
||||||
|
subs.getChannel().setEndpoint("http://localhost:8080");
|
||||||
|
assertTrue(mySubscriptionDao.create(subs, mySrd).getId().hasIdPart());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -24,7 +24,6 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
|
||||||
import ca.uhn.fhir.jpa.rp.dstu.ObservationResourceProvider;
|
import ca.uhn.fhir.jpa.rp.dstu.ObservationResourceProvider;
|
||||||
import ca.uhn.fhir.jpa.rp.dstu.OrganizationResourceProvider;
|
import ca.uhn.fhir.jpa.rp.dstu.OrganizationResourceProvider;
|
||||||
import ca.uhn.fhir.jpa.rp.dstu.PatientResourceProvider;
|
import ca.uhn.fhir.jpa.rp.dstu.PatientResourceProvider;
|
||||||
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
|
|
||||||
import ca.uhn.fhir.model.api.BundleEntry;
|
import ca.uhn.fhir.model.api.BundleEntry;
|
||||||
import ca.uhn.fhir.model.dstu.resource.Observation;
|
import ca.uhn.fhir.model.dstu.resource.Observation;
|
||||||
import ca.uhn.fhir.model.dstu.resource.Organization;
|
import ca.uhn.fhir.model.dstu.resource.Organization;
|
||||||
|
@ -32,6 +31,7 @@ import ca.uhn.fhir.model.dstu.resource.Patient;
|
||||||
import ca.uhn.fhir.model.dstu.resource.Questionnaire;
|
import ca.uhn.fhir.model.dstu.resource.Questionnaire;
|
||||||
import ca.uhn.fhir.rest.client.IGenericClient;
|
import ca.uhn.fhir.rest.client.IGenericClient;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
|
import ca.uhn.fhir.util.PortUtil;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
|
||||||
public class SystemProviderDstu1Test extends BaseJpaTest {
|
public class SystemProviderDstu1Test extends BaseJpaTest {
|
||||||
|
@ -100,7 +100,7 @@ public class SystemProviderDstu1Test extends BaseJpaTest {
|
||||||
JpaSystemProviderDstu1 systemProv = ourAppCtx.getBean(JpaSystemProviderDstu1.class, "mySystemProviderDstu1");
|
JpaSystemProviderDstu1 systemProv = ourAppCtx.getBean(JpaSystemProviderDstu1.class, "mySystemProviderDstu1");
|
||||||
restServer.setPlainProviders(systemProv);
|
restServer.setPlainProviders(systemProv);
|
||||||
|
|
||||||
int myPort = RandomServerPortProvider.findFreePort();
|
int myPort = PortUtil.findFreePort();
|
||||||
ourServer = new Server(myPort);
|
ourServer = new Server(myPort);
|
||||||
|
|
||||||
ServletContextHandler proxyHandler = new ServletContextHandler();
|
ServletContextHandler proxyHandler = new ServletContextHandler();
|
||||||
|
|
|
@ -34,7 +34,6 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
|
||||||
import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor;
|
import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor;
|
||||||
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
||||||
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
|
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
|
||||||
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
|
|
||||||
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
|
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
|
||||||
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
|
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
|
||||||
import ca.uhn.fhir.parser.StrictErrorHandler;
|
import ca.uhn.fhir.parser.StrictErrorHandler;
|
||||||
|
@ -43,6 +42,7 @@ import ca.uhn.fhir.rest.client.ServerValidationModeEnum;
|
||||||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
|
||||||
|
import ca.uhn.fhir.util.PortUtil;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
|
||||||
public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
|
public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
|
||||||
|
@ -77,7 +77,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
|
||||||
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
|
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
|
||||||
|
|
||||||
if (ourServer == null) {
|
if (ourServer == null) {
|
||||||
ourPort = RandomServerPortProvider.findFreePort();
|
ourPort = PortUtil.findFreePort();
|
||||||
|
|
||||||
ourRestServer = new RestfulServer(myFhirCtx);
|
ourRestServer = new RestfulServer(myFhirCtx);
|
||||||
|
|
||||||
|
|
|
@ -9,25 +9,34 @@ import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.junit.*;
|
import org.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
|
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
|
||||||
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
|
|
||||||
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
|
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
|
||||||
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
||||||
import ca.uhn.fhir.model.dstu2.resource.Observation;
|
import ca.uhn.fhir.model.dstu2.resource.Observation;
|
||||||
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
||||||
import ca.uhn.fhir.model.dstu2.resource.Subscription.Channel;
|
import ca.uhn.fhir.model.dstu2.resource.Subscription.Channel;
|
||||||
import ca.uhn.fhir.model.dstu2.valueset.*;
|
import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum;
|
||||||
|
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
|
||||||
|
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.rest.annotation.*;
|
import ca.uhn.fhir.rest.annotation.Create;
|
||||||
|
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||||
|
import ca.uhn.fhir.rest.annotation.Update;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
|
import ca.uhn.fhir.util.PortUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the rest-hook subscriptions
|
* Test the rest-hook subscriptions
|
||||||
|
@ -250,7 +259,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void startListenerServer() throws Exception {
|
public static void startListenerServer() throws Exception {
|
||||||
ourListenerPort = RandomServerPortProvider.findFreePort();
|
ourListenerPort = PortUtil.findFreePort();
|
||||||
ourListenerRestServer = new RestfulServer(FhirContext.forDstu2());
|
ourListenerRestServer = new RestfulServer(FhirContext.forDstu2());
|
||||||
ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context";
|
ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context";
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,11 @@ package ca.uhn.fhir.jpa.subscription;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
@ -21,6 +24,7 @@ import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.rest.annotation.*;
|
import ca.uhn.fhir.rest.annotation.*;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
|
import ca.uhn.fhir.rest.server.Constants;
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
|
|
||||||
|
@ -29,12 +33,14 @@ import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
*/
|
*/
|
||||||
public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
|
|
||||||
|
private static List<String> ourContentTypes = new ArrayList<String>();
|
||||||
private static List<Observation> ourCreatedObservations = Lists.newArrayList();
|
private static List<Observation> ourCreatedObservations = Lists.newArrayList();
|
||||||
private static int ourListenerPort;
|
private static int ourListenerPort;
|
||||||
private static RestfulServer ourListenerRestServer;
|
private static RestfulServer ourListenerRestServer;
|
||||||
private static Server ourListenerServer;
|
private static Server ourListenerServer;
|
||||||
private static String ourListenerServerBase;
|
private static String ourListenerServerBase;
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestDstu3Test.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestDstu3Test.class);
|
||||||
|
|
||||||
private static List<Observation> ourUpdatedObservations = Lists.newArrayList();
|
private static List<Observation> ourUpdatedObservations = Lists.newArrayList();
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -57,18 +63,19 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
public void beforeReset() {
|
public void beforeReset() {
|
||||||
ourCreatedObservations.clear();
|
ourCreatedObservations.clear();
|
||||||
ourUpdatedObservations.clear();
|
ourUpdatedObservations.clear();
|
||||||
|
ourContentTypes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Subscription createSubscription(String criteria, String payload, String endpoint) {
|
private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) {
|
||||||
Subscription subscription = new Subscription();
|
Subscription subscription = new Subscription();
|
||||||
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
||||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||||
subscription.setCriteria(criteria);
|
subscription.setCriteria(theCriteria);
|
||||||
|
|
||||||
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
|
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
|
||||||
channel.setType(Subscription.SubscriptionChannelType.RESTHOOK);
|
channel.setType(Subscription.SubscriptionChannelType.RESTHOOK);
|
||||||
channel.setPayload(payload);
|
channel.setPayload(thePayload);
|
||||||
channel.setEndpoint(endpoint);
|
channel.setEndpoint(theEndpoint);
|
||||||
subscription.setChannel(channel);
|
subscription.setChannel(channel);
|
||||||
|
|
||||||
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
|
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
|
||||||
|
@ -96,7 +103,27 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionJson() throws Exception {
|
public void testRestHookSubscriptionApplicationFhirJson() throws Exception {
|
||||||
|
String payload = "application/fhir+json";
|
||||||
|
|
||||||
|
String code = "1000000050";
|
||||||
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
|
createSubscription(criteria1, payload, ourListenerServerBase);
|
||||||
|
createSubscription(criteria2, payload, ourListenerServerBase);
|
||||||
|
|
||||||
|
sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(1, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationJson() throws Exception {
|
||||||
String payload = "application/json";
|
String payload = "application/json";
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
|
@ -112,6 +139,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
assertEquals(1, ourCreatedObservations.size());
|
assertEquals(1, ourCreatedObservations.size());
|
||||||
assertEquals(0, ourUpdatedObservations.size());
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
|
||||||
|
|
||||||
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
|
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
|
||||||
Assert.assertNotNull(subscriptionTemp);
|
Assert.assertNotNull(subscriptionTemp);
|
||||||
|
@ -169,7 +197,28 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionXml() throws Exception {
|
public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
|
||||||
|
String payload = "application/fhir+xml";
|
||||||
|
|
||||||
|
String code = "1000000050";
|
||||||
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
|
Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase);
|
||||||
|
Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase);
|
||||||
|
|
||||||
|
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(1, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationXml() throws Exception {
|
||||||
String payload = "application/xml";
|
String payload = "application/xml";
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
|
@ -185,6 +234,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
assertEquals(1, ourCreatedObservations.size());
|
assertEquals(1, ourCreatedObservations.size());
|
||||||
assertEquals(0, ourUpdatedObservations.size());
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0));
|
||||||
|
|
||||||
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
|
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
|
||||||
Assert.assertNotNull(subscriptionTemp);
|
Assert.assertNotNull(subscriptionTemp);
|
||||||
|
@ -271,8 +321,9 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
public static class ObservationListener implements IResourceProvider {
|
public static class ObservationListener implements IResourceProvider {
|
||||||
|
|
||||||
@Create
|
@Create
|
||||||
public MethodOutcome create(@ResourceParam Observation theObservation) {
|
public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
||||||
ourLog.info("Received Listener Create");
|
ourLog.info("Received Listener Create");
|
||||||
|
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
|
||||||
ourCreatedObservations.add(theObservation);
|
ourCreatedObservations.add(theObservation);
|
||||||
return new MethodOutcome(new IdType("Observation/1"), true);
|
return new MethodOutcome(new IdType("Observation/1"), true);
|
||||||
}
|
}
|
||||||
|
@ -283,9 +334,10 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
public MethodOutcome update(@ResourceParam Observation theObservation) {
|
public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
||||||
ourLog.info("Received Listener Update");
|
ourLog.info("Received Listener Update");
|
||||||
ourUpdatedObservations.add(theObservation);
|
ourUpdatedObservations.add(theObservation);
|
||||||
|
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
|
||||||
return new MethodOutcome(new IdType("Observation/1"), false);
|
return new MethodOutcome(new IdType("Observation/1"), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,309 @@
|
||||||
|
|
||||||
|
package ca.uhn.fhir.jpa.subscription;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||||
|
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
|
||||||
|
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
|
||||||
|
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Observation;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
||||||
|
import ca.uhn.fhir.model.dstu2.resource.Subscription.Channel;
|
||||||
|
import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum;
|
||||||
|
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
|
||||||
|
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
|
||||||
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
|
import ca.uhn.fhir.rest.annotation.Create;
|
||||||
|
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||||
|
import ca.uhn.fhir.rest.annotation.Update;
|
||||||
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
|
import ca.uhn.fhir.util.PortUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the rest-hook subscriptions
|
||||||
|
*/
|
||||||
|
public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends BaseResourceProviderDstu2Test {
|
||||||
|
|
||||||
|
private static List<Observation> ourCreatedObservations = Lists.newArrayList();
|
||||||
|
private static int ourListenerPort;
|
||||||
|
private static RestfulServer ourListenerRestServer;
|
||||||
|
private static Server ourListenerServer;
|
||||||
|
private static String ourListenerServerBase;
|
||||||
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.class);
|
||||||
|
private static List<Observation> ourUpdatedObservations = Lists.newArrayList();
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterUnregisterRestHookListener() {
|
||||||
|
myDaoConfig.setAllowMultipleDelete(true);
|
||||||
|
ourLog.info("Deleting all subscriptions");
|
||||||
|
ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute();
|
||||||
|
ourLog.info("Done deleting all subscriptions");
|
||||||
|
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
|
||||||
|
|
||||||
|
myDaoConfig.getInterceptors().remove(ourRestHookSubscriptionInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeRegisterRestHookListener() {
|
||||||
|
myDaoConfig.getInterceptors().add(ourRestHookSubscriptionInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeReset() {
|
||||||
|
ourCreatedObservations.clear();
|
||||||
|
ourUpdatedObservations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Subscription createSubscription(String criteria, String payload, String endpoint) {
|
||||||
|
Subscription subscription = new Subscription();
|
||||||
|
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
||||||
|
subscription.setStatus(SubscriptionStatusEnum.ACTIVE);
|
||||||
|
subscription.setCriteria(criteria);
|
||||||
|
|
||||||
|
Channel channel = new Channel();
|
||||||
|
channel.setType(SubscriptionChannelTypeEnum.REST_HOOK);
|
||||||
|
channel.setPayload(payload);
|
||||||
|
channel.setEndpoint(endpoint);
|
||||||
|
subscription.setChannel(channel);
|
||||||
|
|
||||||
|
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
|
||||||
|
subscription.setId(methodOutcome.getId().getIdPart());
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observation sendObservation(String code, String system) {
|
||||||
|
Observation observation = new Observation();
|
||||||
|
CodeableConceptDt codeableConcept = new CodeableConceptDt();
|
||||||
|
observation.setCode(codeableConcept);
|
||||||
|
CodingDt coding = codeableConcept.addCoding();
|
||||||
|
coding.setCode(code);
|
||||||
|
coding.setSystem(system);
|
||||||
|
|
||||||
|
observation.setStatus(ObservationStatusEnum.FINAL);
|
||||||
|
|
||||||
|
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute();
|
||||||
|
|
||||||
|
String observationId = methodOutcome.getId().getIdPart();
|
||||||
|
observation.setId(observationId);
|
||||||
|
|
||||||
|
return observation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionJson() throws Exception {
|
||||||
|
String payload = "application/json";
|
||||||
|
|
||||||
|
String code = "1000000050";
|
||||||
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
|
Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase);
|
||||||
|
Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase);
|
||||||
|
|
||||||
|
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(1, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
|
||||||
|
Assert.assertNotNull(subscriptionTemp);
|
||||||
|
|
||||||
|
subscriptionTemp.setCriteria(criteria1);
|
||||||
|
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
|
||||||
|
|
||||||
|
|
||||||
|
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see two subscription notifications
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(3, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
ourClient.delete().resourceById(new IdDt("Subscription/"+ subscription2.getId())).execute();
|
||||||
|
|
||||||
|
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see only one subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(4, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Observation observation3 = ourClient.read(Observation.class, observationTemp3.getId());
|
||||||
|
CodeableConceptDt codeableConcept = new CodeableConceptDt();
|
||||||
|
observation3.setCode(codeableConcept);
|
||||||
|
CodingDt coding = codeableConcept.addCoding();
|
||||||
|
coding.setCode(code + "111");
|
||||||
|
coding.setSystem("SNOMED-CT");
|
||||||
|
ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
|
||||||
|
|
||||||
|
// Should see no subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(4, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Observation observation3a = ourClient.read(Observation.class, observationTemp3.getId());
|
||||||
|
|
||||||
|
CodeableConceptDt codeableConcept1 = new CodeableConceptDt();
|
||||||
|
observation3a.setCode(codeableConcept1);
|
||||||
|
CodingDt coding1 = codeableConcept1.addCoding();
|
||||||
|
coding1.setCode(code);
|
||||||
|
coding1.setSystem("SNOMED-CT");
|
||||||
|
ourClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
|
||||||
|
|
||||||
|
// Should see only one subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(4, ourCreatedObservations.size());
|
||||||
|
assertEquals(1, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
|
||||||
|
Assert.assertFalse(observation1.getId().isEmpty());
|
||||||
|
Assert.assertFalse(observation2.getId().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionXml() throws Exception {
|
||||||
|
String payload = "application/xml";
|
||||||
|
|
||||||
|
String code = "1000000050";
|
||||||
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
|
Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase);
|
||||||
|
Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase);
|
||||||
|
|
||||||
|
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(1, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId());
|
||||||
|
Assert.assertNotNull(subscriptionTemp);
|
||||||
|
|
||||||
|
subscriptionTemp.setCriteria(criteria1);
|
||||||
|
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
|
||||||
|
|
||||||
|
|
||||||
|
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see two subscription notifications
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(3, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
ourClient.delete().resourceById(new IdDt("Subscription/"+ subscription2.getId())).execute();
|
||||||
|
|
||||||
|
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
|
||||||
|
|
||||||
|
// Should see only one subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(4, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Observation observation3 = ourClient.read(Observation.class, observationTemp3.getId());
|
||||||
|
CodeableConceptDt codeableConcept = new CodeableConceptDt();
|
||||||
|
observation3.setCode(codeableConcept);
|
||||||
|
CodingDt coding = codeableConcept.addCoding();
|
||||||
|
coding.setCode(code + "111");
|
||||||
|
coding.setSystem("SNOMED-CT");
|
||||||
|
ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
|
||||||
|
|
||||||
|
// Should see no subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(4, ourCreatedObservations.size());
|
||||||
|
assertEquals(0, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Observation observation3a = ourClient.read(Observation.class, observationTemp3.getId());
|
||||||
|
|
||||||
|
CodeableConceptDt codeableConcept1 = new CodeableConceptDt();
|
||||||
|
observation3a.setCode(codeableConcept1);
|
||||||
|
CodingDt coding1 = codeableConcept1.addCoding();
|
||||||
|
coding1.setCode(code);
|
||||||
|
coding1.setSystem("SNOMED-CT");
|
||||||
|
ourClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
|
||||||
|
|
||||||
|
// Should see only one subscription notification
|
||||||
|
Thread.sleep(500);
|
||||||
|
assertEquals(4, ourCreatedObservations.size());
|
||||||
|
assertEquals(1, ourUpdatedObservations.size());
|
||||||
|
|
||||||
|
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
|
||||||
|
Assert.assertFalse(observation1.getId().isEmpty());
|
||||||
|
Assert.assertFalse(observation2.getId().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void startListenerServer() throws Exception {
|
||||||
|
ourListenerPort = PortUtil.findFreePort();
|
||||||
|
ourListenerRestServer = new RestfulServer(FhirContext.forDstu2());
|
||||||
|
ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context";
|
||||||
|
|
||||||
|
ObservationListener obsListener = new ObservationListener();
|
||||||
|
ourListenerRestServer.setResourceProviders(obsListener);
|
||||||
|
|
||||||
|
ourListenerServer = new Server(ourListenerPort);
|
||||||
|
|
||||||
|
ServletContextHandler proxyHandler = new ServletContextHandler();
|
||||||
|
proxyHandler.setContextPath("/");
|
||||||
|
|
||||||
|
ServletHolder servletHolder = new ServletHolder();
|
||||||
|
servletHolder.setServlet(ourListenerRestServer);
|
||||||
|
proxyHandler.addServlet(servletHolder, "/fhir/context/*");
|
||||||
|
|
||||||
|
ourListenerServer.setHandler(proxyHandler);
|
||||||
|
ourListenerServer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void stopListenerServer() throws Exception {
|
||||||
|
ourListenerServer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ObservationListener implements IResourceProvider {
|
||||||
|
|
||||||
|
@Create
|
||||||
|
public MethodOutcome create(@ResourceParam Observation theObservation) {
|
||||||
|
ourLog.info("Received Listener Create");
|
||||||
|
ourCreatedObservations.add(theObservation);
|
||||||
|
return new MethodOutcome(new IdDt("Observation/1"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends IBaseResource> getResourceType() {
|
||||||
|
return Observation.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Update
|
||||||
|
public MethodOutcome update(@ResourceParam Observation theObservation) {
|
||||||
|
ourLog.info("Received Listener Update");
|
||||||
|
ourUpdatedObservations.add(theObservation);
|
||||||
|
return new MethodOutcome(new IdDt("Observation/1"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -125,6 +125,14 @@
|
||||||
Fix XhtmlParser to correctly handle hexadecimal escaped literals. Thanks to
|
Fix XhtmlParser to correctly handle hexadecimal escaped literals. Thanks to
|
||||||
Gijsbert van den Brink for the Pull Request!
|
Gijsbert van den Brink for the Pull Request!
|
||||||
</action>
|
</action>
|
||||||
|
<action type="add">
|
||||||
|
JPA server now has configurable properties that allow referential integrity
|
||||||
|
to be disabled for both writes and deletes. This is useful in some cases
|
||||||
|
where data integrity is not wanted or not possible. It can also be useful
|
||||||
|
if you want to delete large amounts of interconnected data quickly.
|
||||||
|
<![CDATA[<br/><br/>]]>
|
||||||
|
A corresponding flag has been added to the CLI tool as well.
|
||||||
|
</action>
|
||||||
</release>
|
</release>
|
||||||
<release version="2.4" date="2017-04-19">
|
<release version="2.4" date="2017-04-19">
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
|
Loading…
Reference in New Issue