diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index d236fe206ea..938dc2627af 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -14,6 +14,7 @@ HAPI FHIR JPA Server + org.apache.commons @@ -264,11 +266,21 @@ xml-apis xml-apis + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + org.hibernate hibernate-entitymanager + + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + + org.hibernate 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 7b95cb667e2..4657e0b11c3 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 @@ -22,6 +22,8 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu2Interceptor; +import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor; import ca.uhn.fhir.jpa.term.HapiTerminologySvcDstu2; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.model.dstu2.composite.MetaDt; @@ -118,4 +120,10 @@ public class BaseDstu2Config extends BaseConfig { return new HapiTerminologySvcDstu2(); } + @Bean + @Lazy + public RestHookSubscriptionDstu2Interceptor restHookSubscriptionDstu3Interceptor() { + return new RestHookSubscriptionDstu2Interceptor(); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java index 54ed5019dd7..7e17c4f3d20 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java @@ -40,6 +40,7 @@ import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3; +import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; import ca.uhn.fhir.jpa.term.HapiTerminologySvcDstu3; import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; @@ -131,5 +132,11 @@ public class BaseDstu3Config extends BaseConfig { public IValidationSupport validationSupportChainDstu3() { return new JpaValidationSupportChainDstu3(); } + + @Bean + @Lazy + public RestHookSubscriptionDstu3Interceptor restHookSubscriptionDstu3Interceptor() { + return new RestHookSubscriptionDstu3Interceptor(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/WebsocketDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/WebsocketDstu3Config.java index a77e9c61a84..88656845d2d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/WebsocketDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/WebsocketDstu3Config.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.config.dstu3; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Controller; @@ -32,7 +33,9 @@ import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.handler.PerConnectionWebSocketHandler; +import ca.uhn.fhir.jpa.interceptor.WebSocketSubscriptionDstu3Interceptor; import ca.uhn.fhir.jpa.subscription.SubscriptionWebsocketHandlerDstu3; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; @Configuration @EnableWebSocket() @@ -59,7 +62,6 @@ public class WebsocketDstu3Config implements WebSocketConfigurer { public void afterPropertiesSet() { super.afterPropertiesSet(); getScheduledThreadPoolExecutor().setExecuteExistingDelayedTasksAfterShutdownPolicy(false); -// getScheduledThreadPoolExecutor().setRemoveOnCancelPolicy(true); getScheduledThreadPoolExecutor().setContinueExistingPeriodicTasksAfterShutdownPolicy(false); } }; @@ -69,4 +71,10 @@ public class WebsocketDstu3Config implements WebSocketConfigurer { return retVal; } + @Bean + public IServerInterceptor webSocketSubscriptionDstu3Interceptor(){ + return new WebSocketSubscriptionDstu3Interceptor(); + } + + } 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 d48cb656c67..7dc76dcf415 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 @@ -540,29 +540,29 @@ public abstract class BaseHapiFhirDao implements IDao { if (!haveParam) { BaseResourceIndexedSearchParam param; switch (type) { - case DATE: - param = new ResourceIndexedSearchParamDate(); - break; - case NUMBER: - param = new ResourceIndexedSearchParamNumber(); - break; - case QUANTITY: - param = new ResourceIndexedSearchParamQuantity(); - break; - case STRING: - param = new ResourceIndexedSearchParamString(); - break; - case TOKEN: - param = new ResourceIndexedSearchParamToken(); - break; - case URI: - param = new ResourceIndexedSearchParamUri(); - break; - case COMPOSITE: - case HAS: - case REFERENCE: - default: - continue; + case DATE: + param = new ResourceIndexedSearchParamDate(); + break; + case NUMBER: + param = new ResourceIndexedSearchParamNumber(); + break; + case QUANTITY: + param = new ResourceIndexedSearchParamQuantity(); + break; + case STRING: + param = new ResourceIndexedSearchParamString(); + break; + case TOKEN: + param = new ResourceIndexedSearchParamToken(); + break; + case URI: + param = new ResourceIndexedSearchParamUri(); + break; + case COMPOSITE: + case HAS: + case REFERENCE: + default: + continue; } param.setResource(theEntity); param.setMissing(true); @@ -842,24 +842,24 @@ public abstract class BaseHapiFhirDao implements IDao { } } } - + ResourceEncodingEnum encoding = myConfig.getResourceEncoding(); - + IParser parser = encoding.newParser(myContext); parser.setDontEncodeElements(EXCLUDE_ELEMENTS_IN_ENCODED); String encoded = parser.encodeResourceToString(theResource); - + theEntity.setEncoding(encoding); theEntity.setFhirVersion(myContext.getVersion().getVersion()); byte[] bytes; switch (encoding) { - case JSON: - bytes = encoded.getBytes(Charsets.UTF_8); - break; - default: - case JSONC: - bytes = GZipUtil.compress(encoded); - break; + case JSON: + bytes = encoded.getBytes(Charsets.UTF_8); + break; + default: + case JSONC: + bytes = GZipUtil.compress(encoded); + break; } boolean changed = false; @@ -872,23 +872,23 @@ public abstract class BaseHapiFhirDao implements IDao { } theEntity.setHashSha256(hashSha256); } - + if (changed == false) { if (theEntity.getResource() == null) { changed = true; - }else { + } else { changed = !Arrays.equals(theEntity.getResource(), bytes); } } - + theEntity.setResource(bytes); - + Set allDefs = new HashSet(); theEntity.setHasTags(false); - Set allTagsOld = getAllTagDefinitions(theEntity); - + Set allTagsOld = getAllTagDefinitions(theEntity); + if (theResource instanceof IResource) { extractTagsHapi((IResource) theResource, theEntity, allDefs); } else { @@ -984,19 +984,19 @@ public abstract class BaseHapiFhirDao implements IDao { List profiles = new ArrayList(); for (BaseTag next : tags) { switch (next.getTag().getTagType()) { - case PROFILE: - profiles.add(new IdDt(next.getTag().getCode())); - break; - case SECURITY_LABEL: - IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt(); - secLabel.setSystem(next.getTag().getSystem()); - secLabel.setCode(next.getTag().getCode()); - secLabel.setDisplay(next.getTag().getDisplay()); - securityLabels.add(secLabel); - break; - case TAG: - tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay())); - break; + case PROFILE: + profiles.add(new IdDt(next.getTag().getCode())); + break; + case SECURITY_LABEL: + IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt(); + secLabel.setSystem(next.getTag().getSystem()); + secLabel.setCode(next.getTag().getCode()); + secLabel.setDisplay(next.getTag().getDisplay()); + securityLabels.add(secLabel); + break; + case TAG: + tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay())); + break; } } if (tagList.size() > 0) { @@ -1052,21 +1052,21 @@ public abstract class BaseHapiFhirDao implements IDao { if (theEntity.isHasTags()) { for (BaseTag next : tags) { switch (next.getTag().getTagType()) { - case PROFILE: - res.getMeta().addProfile(next.getTag().getCode()); - break; - case SECURITY_LABEL: - IBaseCoding sec = res.getMeta().addSecurity(); - sec.setSystem(next.getTag().getSystem()); - sec.setCode(next.getTag().getCode()); - sec.setDisplay(next.getTag().getDisplay()); - break; - case TAG: - IBaseCoding tag = res.getMeta().addTag(); - tag.setSystem(next.getTag().getSystem()); - tag.setCode(next.getTag().getCode()); - tag.setDisplay(next.getTag().getDisplay()); - break; + case PROFILE: + res.getMeta().addProfile(next.getTag().getCode()); + break; + case SECURITY_LABEL: + IBaseCoding sec = res.getMeta().addSecurity(); + sec.setSystem(next.getTag().getSystem()); + sec.setCode(next.getTag().getCode()); + sec.setDisplay(next.getTag().getDisplay()); + break; + case TAG: + IBaseCoding tag = res.getMeta().addTag(); + tag.setSystem(next.getTag().getSystem()); + tag.setCode(next.getTag().getCode()); + tag.setDisplay(next.getTag().getDisplay()); + break; } } } @@ -1164,13 +1164,13 @@ public abstract class BaseHapiFhirDao implements IDao { return false; } -// protected ResourceTable toEntity(IResource theResource) { -// ResourceTable retVal = new ResourceTable(); -// -// populateResourceIntoEntity(theResource, retVal, true); -// -// return retVal; -// } + // protected ResourceTable toEntity(IResource theResource) { + // ResourceTable retVal = new ResourceTable(); + // + // populateResourceIntoEntity(theResource, retVal, true); + // + // return retVal; + // } @Override public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) { @@ -1184,16 +1184,16 @@ public abstract class BaseHapiFhirDao implements IDao { public R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { String resourceText = null; switch (theEntity.getEncoding()) { - case JSON: - try { - resourceText = new String(theEntity.getResource(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new Error("Should not happen", e); - } - break; - case JSONC: - resourceText = GZipUtil.decompress(theEntity.getResource()); - break; + case JSON: + try { + resourceText = new String(theEntity.getResource(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Should not happen", e); + } + break; + case JSONC: + resourceText = GZipUtil.decompress(theEntity.getResource()); + break; } /* @@ -1218,7 +1218,7 @@ public abstract class BaseHapiFhirDao implements IDao { IParser parser = theEntity.getEncoding().newParser(getContext(theEntity.getFhirVersion())); parser.setParserErrorHandler(new LenientErrorHandler(false).setErrorOnInvalidValue(false)); - + R retVal; try { retVal = parser.parseResource(resourceType, resourceText); @@ -1359,7 +1359,7 @@ public abstract class BaseHapiFhirDao implements IDao { theEntity.setContentTextParsedIntoWords(null); theEntity.setHashSha256(null); changed = true; - + } else { theEntity.setDeleted(null); @@ -1918,12 +1918,12 @@ public abstract class BaseHapiFhirDao implements IDao { String qualifier = null; for (int i = 0; i < paramName.length(); i++) { switch (paramName.charAt(i)) { - case '.': - case ':': - qualifier = paramName.substring(i); - paramName = paramName.substring(0, i); - i = Integer.MAX_VALUE - 1; - break; + case '.': + case ':': + qualifier = paramName.substring(i); + paramName = paramName.substring(0, i); + i = Integer.MAX_VALUE - 1; + break; } } 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 842f11c8403..4c72a1d9e75 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 @@ -117,12 +117,13 @@ 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); @@ -310,7 +311,7 @@ public abstract class BaseHapiFhirResourceDao extends B retVal.setOperationOutcome(oo); return retVal; } - + @Override public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) { List deleteConflicts = new ArrayList(); @@ -321,7 +322,7 @@ public abstract class BaseHapiFhirResourceDao extends B return outcome; } - + @PostConstruct public void detectSearchDaoDisabled() { if (mySearchDao != null && mySearchDao.isDisabled()) { @@ -415,23 +416,6 @@ public abstract class BaseHapiFhirResourceDao extends B return outcome; } - private void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) { - IIdType idType = theResourceId; - String newVersion; - long newVersionLong; - if (idType == null || idType.getVersionIdPart() == null) { - newVersion = "1"; - newVersionLong = 1; - } else { - newVersionLong = idType.getVersionIdPartAsLong() + 1; - newVersion = Long.toString(newVersionLong); - } - - IIdType newId = theResourceId.withVersion(newVersion); - theResource.getIdElement().setValue(newId.getValue()); - theSavedEntity.setVersion(newVersionLong); - } - private void doMetaAdd(MT theMetaAdd, BaseHasResource entity) { List tags = toTagList(theMetaAdd); @@ -548,6 +532,23 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } + private void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) { + IIdType idType = theResourceId; + String newVersion; + long newVersionLong; + if (idType == null || idType.getVersionIdPart() == null) { + newVersion = "1"; + newVersionLong = 1; + } else { + newVersionLong = idType.getVersionIdPartAsLong() + 1; + newVersion = Long.toString(newVersionLong); + } + + IIdType newId = theResourceId.withVersion(newVersion); + theResource.getIdElement().setValue(newId.getValue()); + theSavedEntity.setVersion(newVersionLong); + } + @Override public MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) { // Notify interceptors @@ -614,7 +615,6 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } - @Override public MT metaGetOperation(Class theType, IIdType theId, RequestDetails theRequestDetails) { // Notify interceptors @@ -636,6 +636,7 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } + @Override public MT metaGetOperation(Class theType, RequestDetails theRequestDetails) { // Notify interceptors @@ -850,7 +851,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 @@ -884,7 +885,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) { @@ -910,9 +911,6 @@ public abstract class BaseHapiFhirResourceDao extends B return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName()); } - - - @Override public Set searchForIds(SearchParameterMap theParams) { @@ -931,6 +929,9 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } + + + @SuppressWarnings("unchecked") @Required public void setResourceType(Class theTableType) { @@ -1060,12 +1061,6 @@ public abstract class BaseHapiFhirResourceDao extends B return update(theResource, theMatchUrl, null); } - @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, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails) { StopWatch w = new StopWatch(); @@ -1163,11 +1158,37 @@ public abstract class BaseHapiFhirResourceDao extends B 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); } + /** + * Get the resource definition from the criteria which specifies the resource type + * @param criteria + * @return + */ + @Override + public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { + String resourceName; + if(criteria == null || criteria.trim().isEmpty()){ + throw new IllegalArgumentException("Criteria cannot be empty"); + } + if(criteria.contains("?")){ + resourceName = criteria.substring(0, criteria.indexOf("?")); + }else{ + resourceName = criteria; + } + + return getContext().getResourceDefinition(resourceName); + } + private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) { if (entity.getForcedId() != null) { if (theId.isIdPartValidLong()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1.java index 16fe190b762..000f1395590 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu1.java @@ -41,11 +41,6 @@ import ca.uhn.fhir.util.FhirTerser; public class FhirResourceDaoDstu1 extends BaseHapiFhirResourceDao { - @Override - public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { - return null; - } - @Override protected List getIncludeValues(FhirTerser t, Include next, IBaseResource nextResource, RuntimeResourceDefinition def) { List values; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java index e74a5155528..27ec465869c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java @@ -145,11 +145,6 @@ public class FhirResourceDaoDstu2 extends BaseHapiFhirResou } - @Override - public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { - return null; - } - private class IdChecker implements IValidatorModule { private ValidationModeEnum myMode; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java index f36162cec94..6d2be5c3aa5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java @@ -171,7 +171,6 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2 extends BaseHapiFhirRe } - /** - * Get the resource definition from the criteria which specifies the resource type - * @param criteria - * @return - */ - @Override - public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { - String resourceName; - if(criteria == null || criteria.trim().isEmpty()){ - throw new IllegalArgumentException("Criteria cannot be empty"); - } - if(criteria.contains("?")){ - resourceName = criteria.substring(0, criteria.indexOf("?")); - }else{ - resourceName = criteria; - } - - return fhirContext.getResourceDefinition(resourceName); - } - private class IdChecker implements IValidatorModule { private ValidationModeEnum myMode; 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 84d17bdc396..60905ae9d1f 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 @@ -10,7 +10,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; * 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 + * 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, @@ -57,7 +57,6 @@ import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource; import ca.uhn.fhir.jpa.entity.SubscriptionTable; -import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; @@ -66,11 +65,12 @@ import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; 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.IBundleProvider; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3implements IFhirResourceDaoSubscription { +public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 implements IFhirResourceDaoSubscription { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoSubscriptionDstu3.class); @@ -102,7 +102,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 getUndeliveredResourcesAndPurge(Long theSubscriptionPid) { List retVal = new ArrayList(); @@ -120,12 +120,12 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 subscriptions = txTemplate.execute(new TransactionCallback>() { @Override public Collection doInTransaction(TransactionStatus theStatus) { @@ -149,31 +149,31 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 dao = getDao(resourceDef.getImplementingClass()); @@ -199,7 +199,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 flags = new ArrayList(); Date mostRecentMatch = null; for (IBaseResource nextBase : results.getResources(0, results.size())) { - IAnyResource next = (IAnyResource)nextBase; + IAnyResource next = (IAnyResource) nextBase; Date updated = next.getMeta().getLastUpdated(); if (mostRecentMatch == null) { @@ -214,9 +214,9 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 toPurge = txTemplate.execute(new TransactionCallback>() { @Override public Collection doInTransaction(TransactionStatus theStatus) { @@ -275,7 +275,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 mySubscriptionDao; + private static volatile ExecutorService executor; + private final static int MAX_THREADS = 1; - private static volatile ExecutorService executor; - private FhirResourceDaoSubscriptionDstu2 myResourceSubscriptionDao; + private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class); - private static final Logger logger = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class); - private final List restHookSubscriptions = new ArrayList(); - private boolean notifyOnDelete = false; + @Autowired + private FhirContext myCtx; + private boolean myNotifyOnDelete = false; - private final static int MAX_THREADS = 1; + private final List myRestHookSubscriptions = new ArrayList(); + @Autowired + @Qualifier("mySubscriptionDaoDstu2") + private IFhirResourceDao mySubscriptionDao; - @PostConstruct - public void postConstruct() { - try { - executor = Executors.newFixedThreadPool(MAX_THREADS); - myResourceSubscriptionDao = SpringObjectCaster.getTargetObject(mySubscriptionDao, FhirResourceDaoSubscriptionDstu2.class); - } catch (Exception e) { - throw new RuntimeException("Unable to get DAO from PROXY"); - } - } + /** + * Check subscriptions and send notifications or payload + * + * @param idType + * @param resourceType + * @param theOperation + */ + private void checkSubscriptions(IIdType idType, String resourceType, RestOperationTypeEnum theOperation) { + for (Subscription subscription : myRestHookSubscriptions) { + // see if the criteria matches the created object + ourLog.info("subscription for " + resourceType + " with criteria " + subscription.getCriteria()); + if (resourceType != null && subscription.getCriteria() != null && !subscription.getCriteria().startsWith(resourceType)) { + ourLog.info("Skipping subscription search for " + resourceType + " because it does not match the criteria " + subscription.getCriteria()); + continue; + } + // run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource + String criteria = subscription.getCriteria(); + criteria += "&_id=" + idType.getResourceType() + "/" + idType.getIdPart(); + criteria = TMinusService.parseCriteria(criteria); - /** - * Read the existing subscriptions from the database - */ - public void initSubscriptions() { - SearchParameterMap map = new SearchParameterMap(); - map.add(Subscription.SP_TYPE, new TokenParam(null, SubscriptionChannelTypeEnum.REST_HOOK.getCode())); - map.add(Subscription.SP_STATUS, new TokenParam(null, SubscriptionStatusEnum.ACTIVE.getCode())); + IBundleProvider results = getBundleProvider(criteria); - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); + if (results.size() == 0) { + continue; + } - IBundleProvider subscriptionBundleList = mySubscriptionDao.search(map, req); - List resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); + // should just be one resource as it was filtered by the id + for (IBaseResource nextBase : results.getResources(0, results.size())) { + IResource next = (IResource) nextBase; + ourLog.info("Found match: queueing rest-hook notification for resource: {}", next.getIdElement()); + HttpUriRequest request = createRequest(subscription, next, theOperation); + if (request != null) { + executor.submit(new HttpRequestDstu2Job(request, subscription)); + } + } + } + } - for (IBaseResource resource : resourceList) { - restHookSubscriptions.add((Subscription) resource); - } - } + /** + * Creates an HTTP Post for a subscription + */ + private HttpUriRequest createRequest(Subscription theSubscription, IResource theResource, RestOperationTypeEnum theOperation) { + String url = theSubscription.getChannel().getEndpoint(); + while (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } - /** - * Handles incoming resources. If the resource is a rest-hook subscription, it adds - * it to the rest-hook subscription list. Otherwise it checks to see if the resource - * matches any rest-hook subscriptions. - * - * @param theDetails The request details - * @param theResourceTable The actual created entity - */ - @Override - public void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - String resourceType = theDetails.getResourceType(); - IIdType idType = theDetails.getId(); - logger.info("resource created type: " + resourceType); - if (resourceType.equals(Subscription.class.getSimpleName())) { - Subscription subscription = (Subscription) theDetails.getResource(); - if (subscription.getChannel() != null - && subscription.getChannel().getType().equals(SubscriptionChannelTypeEnum.REST_HOOK.getCode()) - && subscription.getStatus().equals(SubscriptionStatusEnum.REQUESTED.getCode())) { - subscription.setStatus(SubscriptionStatusEnum.ACTIVE); - mySubscriptionDao.update(subscription); - restHookSubscriptions.add(subscription); - logger.info("Subscription was added. Id: " + subscription.getId()); - } - } else { - checkSubscriptions(idType, resourceType); - } - } + HttpUriRequest request = null; + String resourceName = myCtx.getResourceDefinition(theResource).getName(); - /** - * Checks for updates to subscriptions or if an update to a resource matches - * a rest-hook subscription - * - * @param theDetails The request details - * @param theResourceTable The actual updated entity - */ - @Override - public void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - String resourceType = theDetails.getResourceType(); - IIdType idType = theDetails.getId(); + String payload = theSubscription.getChannel().getPayload(); + String resourceId = theResource.getIdElement().getIdPart(); - logger.info("resource updated type: " + resourceType); + // HTTP put + if (theOperation == RestOperationTypeEnum.UPDATE && EncodingEnum.XML.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("XML payload found"); + StringEntity entity = getStringEntity(EncodingEnum.XML, theResource); + HttpPut putRequest = new HttpPut(url + "/" + resourceName + "/" + resourceId); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_XML); + putRequest.setEntity(entity); - if (resourceType.equals(Subscription.class.getSimpleName())) { - Subscription subscription = (Subscription) theDetails.getResource(); - if (subscription.getChannel() != null && subscription.getChannel().getType().equals(SubscriptionChannelTypeEnum.REST_HOOK.getCode())) { - removeLocalSubscription(subscription.getId().getIdPart()); + request = putRequest; + } + // HTTP put + else if (theOperation == RestOperationTypeEnum.UPDATE && EncodingEnum.JSON.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("JSON payload found"); + StringEntity entity = getStringEntity(EncodingEnum.JSON, theResource); + HttpPut putRequest = new HttpPut(url + "/" + resourceName + "/" + resourceId); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON); + putRequest.setEntity(entity); - if (subscription.getStatus().equals(SubscriptionStatusEnum.ACTIVE.getCode())) { - restHookSubscriptions.add(subscription); - logger.info("Subscription was updated. Id: " + subscription.getId()); - } - } - } else { - checkSubscriptions(idType, resourceType); - } - } + request = putRequest; + } + // HTTP POST + else if (theOperation == RestOperationTypeEnum.CREATE && EncodingEnum.XML.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("XML payload found"); - /** - * Check subscriptions to see if there is a matching subscription when there is delete - * - * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the - * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been - * pulled out of the {@link HttpServletRequest servlet request}. - * @param theRequest The incoming request - * @param theResponse The response. Note that interceptors may choose to provide a response (i.e. by calling - * {@link HttpServletResponse#getWriter()}) but in that case it is important to return false - * to indicate that the server itself should not also provide a response. - * @return - * @throws AuthenticationException - */ - @Override - public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { - if (theRequestDetails.getRestOperationType().equals(RestOperationTypeEnum.DELETE)) { - String resourceType = theRequestDetails.getResourceName(); - IIdType idType = theRequestDetails.getId(); + IdDt id = theResource.getId(); + theResource.setId(new IdDt()); + StringEntity entity = getStringEntity(EncodingEnum.XML, theResource); + theResource.setId(id); + HttpPost putRequest = new HttpPost(url + "/" + resourceName); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_XML); + putRequest.setEntity(entity); - if (resourceType.equals(Subscription.class.getSimpleName())) { - String id = idType.getIdPart(); - removeLocalSubscription(id); - } else { - if (notifyOnDelete) { - checkSubscriptions(idType, resourceType); - } - } - } + request = putRequest; + } + // HTTP POST + else if (theOperation == RestOperationTypeEnum.CREATE && EncodingEnum.JSON.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("JSON payload found"); + IdDt id = theResource.getId(); + theResource.setId(new IdDt()); + StringEntity entity = getStringEntity(EncodingEnum.JSON, theResource); + theResource.setId(id); + HttpPost putRequest = new HttpPost(url + "/" + resourceName); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON); + putRequest.setEntity(entity); - return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); - } + request = putRequest; + } - /** - * Check subscriptions and send notifications or payload - * - * @param idType - * @param resourceType - */ - private void checkSubscriptions(IIdType idType, String resourceType) { - for (Subscription subscription : restHookSubscriptions) { - //see if the criteria matches the created object - logger.info("subscription for " + resourceType + " with criteria " + subscription.getCriteria()); - if (resourceType != null && subscription.getCriteria() != null && !subscription.getCriteria().startsWith(resourceType)) { - logger.info("Skipping subscription search for " + resourceType + " because it does not match the criteria " + subscription.getCriteria()); - continue; - } - //run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource - String criteria = subscription.getCriteria(); - criteria += "&_id=" + idType.getResourceType() + "/" + idType.getIdPart(); - IBundleProvider results = getBundleProvider(criteria); + // request.addHeader("User-Agent", USER_AGENT); + return request; + } - if (results.size() == 0) { - continue; - } - Observation aa; - //should just be one resource as it was filtered by the id - for (IBaseResource nextBase : results.getResources(0, results.size())) { - IResource next = (IResource) nextBase; - logger.info("Found match: queueing rest-hook notification for resource: {}", next.getIdElement()); - HttpUriRequest request = createRequest(subscription, next); - executor.submit(new HttpRequestDstu2Job(request, subscription)); - } - } - } + /** + * Search based on a query criteria + * + * @param criteria + * @return + */ + private IBundleProvider getBundleProvider(String criteria) { + RuntimeResourceDefinition responseResourceDef = mySubscriptionDao.validateCriteriaAndReturnResourceDefinition(criteria); + SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(mySubscriptionDao, mySubscriptionDao.getContext(), criteria, responseResourceDef); - /** - * Creates an HTTP Post for a subscription - * - * @param subscription - * @param resource - * @return - */ - private HttpUriRequest createRequest(Subscription subscription, IResource resource) { - String url = subscription.getChannel().getEndpoint(); - HttpUriRequest request = null; - String payload = subscription.getChannel().getPayload(); - //HTTP post - if (payload == null || payload.trim().length() == 0) { - //return an empty response as there is no payload - logger.info("No payload found, returning an empty notification"); - request = new HttpPost(url); - } - //HTTP put - else if (payload.equals("application/xml") || payload.equals("application/fhir+xml")) { - logger.info("XML payload found"); - StringEntity entity = getStringEntity(EncodingEnum.XML, resource); - HttpPut putRequest = new HttpPut(url); - putRequest.setEntity(entity); + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); - request = putRequest; - } - //HTTP put - else if (payload.equals("application/json") || payload.equals("application/fhir+json")) { - logger.info("JSON payload found"); - StringEntity entity = getStringEntity(EncodingEnum.JSON, resource); - HttpPut putRequest = new HttpPut(url); - putRequest.setEntity(entity); + IFhirResourceDao responseDao = mySubscriptionDao.getDao(responseResourceDef.getImplementingClass()); + IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); + return responseResults; + } - request = putRequest; - } - //HTTP post - else if (payload.startsWith("application/fhir+query/")) { //custom payload that is a FHIR query - logger.info("Custom query payload found"); - String responseCriteria = subscription.getChannel().getPayload().substring(23); - //get the encoding type from payload which is a FHIR query with &_format= - EncodingEnum encoding = getEncoding(responseCriteria); - IBundleProvider responseResults = getBundleProvider(responseCriteria); - if (responseResults.size() != 0) { - List resourcelist = responseResults.getResources(0, responseResults.size()); - Bundle bundle = createBundle(resourcelist); - StringEntity bundleEntity = getStringEntity(encoding, bundle); - HttpPost postRequest = new HttpPost(url); - postRequest.setEntity(bundleEntity); + /** + * Get subscription from cache + * + * @param id + * @return + */ + private Subscription getLocalSubscription(String id) { + if (id != null && !id.trim().isEmpty()) { + int size = myRestHookSubscriptions.size(); + if (size > 0) { + for (Subscription restHookSubscription : myRestHookSubscriptions) { + if (id.equals(restHookSubscription.getIdElement().getIdPart())) { + return restHookSubscription; + } + } + } + } - request = postRequest; - } else { - Bundle bundle = new Bundle(); - bundle.setTotal(0); - StringEntity bundleEntity = getStringEntity(encoding, bundle); - HttpPost postRequest = new HttpPost(url); - postRequest.setEntity(bundleEntity); + return null; + } - request = postRequest; - } + /** + * Convert a resource into a string entity + * + * @param encoding + * @param anyResource + * @return + */ + private StringEntity getStringEntity(EncodingEnum encoding, IResource anyResource) { + String encoded = encoding.newParser(mySubscriptionDao.getContext()).encodeResourceToString(anyResource); - } else { - logger.warn("Unsupported payload " + payload + ". Returning an empty notification"); - request = new HttpPost(url); - } + StringEntity entity; + if (encoded.equalsIgnoreCase(EncodingEnum.JSON.name())) { + entity = new StringEntity(encoded, ContentType.APPLICATION_JSON); + } else { + entity = new StringEntity(encoded, ContentType.APPLICATION_XML); + } - //request.addHeader("User-Agent", USER_AGENT); - return request; - } + return entity; + } - /** - * Get the encoding from the criteria or return JSON encoding if its not found - * - * @param criteria - * @return - */ - private EncodingEnum getEncoding(String criteria) { - //check criteria - String params = criteria.substring(criteria.indexOf('?') + 1); - List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); - for (NameValuePair nameValuePair : paramValues) { - if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { - return EncodingEnum.forContentType(nameValuePair.getValue()); - } - } - return EncodingEnum.JSON; - } + /** + * Read the existing subscriptions from the database + */ + public void initSubscriptions() { + SearchParameterMap map = new SearchParameterMap(); + map.add(Subscription.SP_TYPE, new TokenParam(null, SubscriptionChannelTypeEnum.REST_HOOK.getCode())); + map.add(Subscription.SP_STATUS, new TokenParam(null, SubscriptionStatusEnum.ACTIVE.getCode())); - /** - * Search based on a query criteria - * - * @param criteria - * @return - */ - private IBundleProvider getBundleProvider(String criteria) { - Subscription subscription = new Subscription(); - subscription.setCriteria(criteria); + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); - RuntimeResourceDefinition responseResourceDef = myResourceSubscriptionDao.validateCriteriaAndReturnResourceDefinition(subscription); - SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(myResourceSubscriptionDao, myResourceSubscriptionDao.getContext(), criteria, responseResourceDef); + IBundleProvider subscriptionBundleList = mySubscriptionDao.search(map, req); + List resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); + for (IBaseResource resource : resourceList) { + myRestHookSubscriptions.add((Subscription) resource); + } + } - IFhirResourceDao responseDao = myResourceSubscriptionDao.getDao(responseResourceDef.getImplementingClass()); - IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); - return responseResults; - } + public boolean isNotifyOnDelete() { + return myNotifyOnDelete; + } - /** - * Create a bundle to return to the client - * - * @param resourcelist - * @return - */ - private Bundle createBundle(List resourcelist) { - Bundle bundle = new Bundle(); - for (IBaseResource resource : resourcelist) { - Bundle.Entry entry = bundle.addEntry(); - entry.setResource((IResource) resource); - } - bundle.setTotal(resourcelist.size()); + @PostConstruct + public void postConstruct() { + try { + executor = Executors.newFixedThreadPool(MAX_THREADS); + } catch (Exception e) { + throw new RuntimeException("Unable to get DAO from PROXY"); + } + } - return bundle; - } + /** + * Remove subscription from cache + * + * @param subscriptionId + */ + private void removeLocalSubscription(String subscriptionId) { + Subscription localSubscription = getLocalSubscription(subscriptionId); + if (localSubscription != null) { + myRestHookSubscriptions.remove(localSubscription); + ourLog.info("Subscription removed: " + subscriptionId); + } else { + ourLog.info("Subscription not found in local list. Subscription id: " + subscriptionId); + } + } - /** - * Convert a resource into a string entity - * - * @param encoding - * @param anyResource - * @return - */ - private StringEntity getStringEntity(EncodingEnum encoding, IResource anyResource) { - String encoded = encoding.newParser(mySubscriptionDao.getContext()).encodeResourceToString(anyResource); + /** + * Handles incoming resources. If the resource is a rest-hook subscription, it adds + * it to the rest-hook subscription list. Otherwise it checks to see if the resource + * matches any rest-hook subscriptions. + */ + @Override + public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { + IIdType idType = theResource.getIdElement(); + ourLog.info("resource created type: {}", theRequest.getResourceName()); - StringEntity entity; - if (encoded.equalsIgnoreCase(EncodingEnum.JSON.name())) { - entity = new StringEntity(encoded, ContentType.APPLICATION_JSON); - } else { - entity = new StringEntity(encoded, ContentType.APPLICATION_XML); - } + if (theResource instanceof Subscription) { + Subscription subscription = (Subscription) theResource; + if (subscription.getChannel() != null + && subscription.getChannel().getTypeElement().getValueAsEnum() == SubscriptionChannelTypeEnum.REST_HOOK + && subscription.getStatusElement().getValueAsEnum() == SubscriptionStatusEnum.REQUESTED) { + subscription.setStatus(SubscriptionStatusEnum.ACTIVE); + mySubscriptionDao.update(subscription); + myRestHookSubscriptions.add(subscription); + ourLog.info("Subscription was added. Id: " + subscription.getId()); + } + } else { + checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE); + } + } - return entity; - } + /** + * Check subscriptions to see if there is a matching subscription when there is delete + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the {@link HttpServletRequest servlet request}. + * @param theRequest + * The incoming request + * @param theResponse + * The response. Note that interceptors may choose to provide a response (i.e. by calling + * {@link HttpServletResponse#getWriter()}) but in that case it is important to return false + * to indicate that the server itself should not also provide a response. + */ + @Override + public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { + String resourceType = theRequest.getResourceName(); + IIdType idType = theResource.getIdElement(); - @Override - public void resourceDeleted(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - } + if (resourceType.equals(Subscription.class.getSimpleName())) { + String id = idType.getIdPart(); + removeLocalSubscription(id); + } else { + if (myNotifyOnDelete) { + checkSubscriptions(idType, resourceType, RestOperationTypeEnum.DELETE); + } + } + } - /** - * Remove subscription from cache - * - * @param subscriptionId - */ - private void removeLocalSubscription(String subscriptionId) { - Subscription localSubscription = getLocalSubscription(subscriptionId); - if (localSubscription != null) { - restHookSubscriptions.remove(localSubscription); - logger.info("Subscription removed: " + subscriptionId); - } else { - logger.info("Subscription not found in local list. Subscription id: " + subscriptionId); - } - } + /** + * Checks for updates to subscriptions or if an update to a resource matches + * a rest-hook subscription + */ + @Override + public void resourceUpdated(RequestDetails theRequest, IBaseResource theResource) { + String resourceType = theRequest.getResourceName(); + IIdType idType = theResource.getIdElement(); - /** - * Get subscription from cache - * - * @param id - * @return - */ - private Subscription getLocalSubscription(String id) { - if (id != null && !id.trim().isEmpty()) { - int size = restHookSubscriptions.size(); - if (size > 0) { - for (Subscription restHookSubscription : restHookSubscriptions) { - if (id.equals(restHookSubscription.getId().getIdPart())) { - return restHookSubscription; - } - } - } - } + ourLog.info("resource updated type: " + resourceType); - return null; - } + if (theResource instanceof Subscription) { + Subscription subscription = (Subscription) theResource; + if (subscription.getChannel() != null && subscription.getChannel().getTypeElement().getValueAsEnum() == SubscriptionChannelTypeEnum.REST_HOOK) { + removeLocalSubscription(subscription.getIdElement().getIdPart()); - public boolean isNotifyOnDelete() { - return notifyOnDelete; - } + if (subscription.getStatusElement().getValueAsEnum() == SubscriptionStatusEnum.ACTIVE) { + myRestHookSubscriptions.add(subscription); + ourLog.info("Subscription was updated. Id: " + subscription.getId()); + } + } + } else { + checkSubscriptions(idType, resourceType, RestOperationTypeEnum.UPDATE); + } + } - public void setNotifyOnDelete(boolean notifyOnDelete) { - this.notifyOnDelete = notifyOnDelete; - } + public void setNotifyOnDelete(boolean notifyOnDelete) { + this.myNotifyOnDelete = notifyOnDelete; + } } - 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 141769cf661..deee460ddd0 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 @@ -1,43 +1,32 @@ /* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). * - * 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 + * 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 + * 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. + * 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. * - * @author Jeff Chung + * @author Jeff Chung */ package ca.uhn.fhir.jpa.interceptor; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoSubscriptionDstu3; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; -import ca.uhn.fhir.jpa.service.TMinusService; -import ca.uhn.fhir.jpa.thread.HttpRequestDstu3Job; -import ca.uhn.fhir.jpa.util.SpringObjectCaster; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.method.RequestDetails; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.param.TokenParam; -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.AuthenticationException; -import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.apache.http.NameValuePair; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; @@ -45,9 +34,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; -import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Observation; -import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -57,390 +44,364 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import javax.annotation.PostConstruct; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.service.TMinusService; +import ca.uhn.fhir.jpa.thread.HttpRequestDstu3Job; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.param.TokenParam; +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.interceptor.IServerOperationInterceptor; +import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; -public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter implements IJpaServerInterceptor { +public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter implements IServerOperationInterceptor { + + private static volatile ExecutorService executor; + private static final Logger ourLog = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class); + + private final static int MAX_THREADS = 1; - @Autowired - @Qualifier("mySubscriptionDaoDstu3") - private IFhirResourceDao mySubscriptionDao; @Autowired @Qualifier("myObservationDaoDstu3") private IFhirResourceDao myObservationDao; + @Autowired + @Qualifier("mySubscriptionDaoDstu3") + private IFhirResourceDao mySubscriptionDao; + @Autowired + private FhirContext myCtx; + + private boolean notifyOnDelete = false; - private static volatile ExecutorService executor; + private final List restHookSubscriptions = new ArrayList(); - private static final Logger logger = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class); - private final List restHookSubscriptions = new ArrayList(); - private boolean notifyOnDelete = false; + /** + * Check subscriptions and send notifications or payload + * + * @param idType + * @param resourceType + * @param theOperation + */ + private void checkSubscriptions(IIdType idType, String resourceType, RestOperationTypeEnum theOperation) { + /* + * SearchParameterMap map = new SearchParameterMap(); + * // map.add("_id", new StringParam("Observation/" + idType.getIdPart())); + * map.add("code", new TokenParam("SNOMED-CT", "1000000050")); + * //map.setLoadSynchronous(true); + * // Include include = new Include("nothing"); + * // map.addInclude(include); + * + * RequestDetails req = new ServletSubRequestDetails(); + * req.setSubRequest(true); + * + * IBundleProvider myBundle = myObservationDao.search(map, req); + * Observation myObservation = myObservationDao.read(idType); + * + * int mysize = myBundle.size(); + * List result = myBundle.getResources(0, myBundle.size()); + */ + for (Subscription subscription : restHookSubscriptions) { + // see if the criteria matches the created object + ourLog.info("subscription for " + resourceType + " with criteria " + subscription.getCriteria()); + if (resourceType != null && subscription.getCriteria() != null && !subscription.getCriteria().startsWith(resourceType)) { + ourLog.info("Skipping subscription search for " + resourceType + " because it does not match the criteria " + subscription.getCriteria()); + continue; + } + // run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource + String criteria = subscription.getCriteria(); + criteria += "&_id=" + idType.getResourceType() + "/" + idType.getIdPart(); + criteria = TMinusService.parseCriteria(criteria); - private final static int MAX_THREADS = 1; + IBundleProvider results = getBundleProvider(criteria); - @PostConstruct - public void postConstruct() { - try { - executor = Executors.newFixedThreadPool(MAX_THREADS); - } catch (Exception e) { - throw new RuntimeException("Unable to get DAO from PROXY"); - } - } + if (results.size() == 0) { + continue; + } - /** - * Read the existing subscriptions from the database - */ - public void initSubscriptions() { - SearchParameterMap map = new SearchParameterMap(); - map.add(Subscription.SP_TYPE, new TokenParam(null, Subscription.SubscriptionChannelType.RESTHOOK.toCode())); - map.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())); + // should just be one resource as it was filtered by the id + for (IBaseResource nextBase : results.getResources(0, results.size())) { + IAnyResource next = (IAnyResource) nextBase; + ourLog.info("Found match: queueing rest-hook notification for resource: {}", next.getIdElement()); + HttpUriRequest request = createRequest(subscription, next, theOperation); + if (request != null) { + executor.submit(new HttpRequestDstu3Job(request, subscription)); + } + } + } + } - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); + /** + * Creates an HTTP Post for a subscription + */ + private HttpUriRequest createRequest(Subscription theSubscription, IAnyResource theResource, RestOperationTypeEnum theOperation) { + String url = theSubscription.getChannel().getEndpoint(); + while (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + + HttpUriRequest request = null; + String resourceName = myCtx.getResourceDefinition(theResource).getName(); - IBundleProvider subscriptionBundleList = mySubscriptionDao.search(map, req); - List resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); + String payload = theSubscription.getChannel().getPayload(); + String resourceId = theResource.getIdElement().getIdPart(); + + // HTTP put + if (theOperation == RestOperationTypeEnum.UPDATE && EncodingEnum.XML.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("XML payload found"); + StringEntity entity = getStringEntity(EncodingEnum.XML, theResource); + HttpPut putRequest = new HttpPut(url + "/" + resourceName + "/" + resourceId); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_XML_NEW); + putRequest.setEntity(entity); - for (IBaseResource resource : resourceList) { - restHookSubscriptions.add((Subscription) resource); - } - } + request = putRequest; + } + // HTTP put + else if (theOperation == RestOperationTypeEnum.UPDATE && EncodingEnum.JSON.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("JSON payload found"); + StringEntity entity = getStringEntity(EncodingEnum.JSON, theResource); + HttpPut putRequest = new HttpPut(url + "/" + resourceName + "/" + resourceId); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW); + putRequest.setEntity(entity); - /** - * Handles incoming resources. If the resource is a rest-hook subscription, it adds - * it to the rest-hook subscription list. Otherwise it checks to see if the resource - * matches any rest-hook subscriptions. - * - * @param theDetails The request details - * @param theResourceTable The actual created entity - */ - @Override - public void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - String resourceType = theDetails.getResourceType(); - IIdType idType = theDetails.getId(); - logger.info("resource created type: " + resourceType); - if (resourceType.equals(Subscription.class.getSimpleName())) { - Subscription subscription = (Subscription) theDetails.getResource(); - if (subscription.getChannel() != null - && subscription.getChannel().getType() == Subscription.SubscriptionChannelType.RESTHOOK - && subscription.getStatus() == Subscription.SubscriptionStatus.REQUESTED) { - subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); - mySubscriptionDao.update(subscription); - restHookSubscriptions.add(subscription); - logger.info("Subscription was added. Id: " + subscription.getId()); - } - } else { - checkSubscriptions(idType, resourceType); - } - } + request = putRequest; + } + // HTTP POST + else if (theOperation == RestOperationTypeEnum.CREATE && EncodingEnum.XML.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("XML payload found"); + StringEntity entity = getStringEntity(EncodingEnum.XML, theResource); + HttpPost putRequest = new HttpPost(url + "/" + resourceName); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_XML_NEW); + putRequest.setEntity(entity); - /** - * Checks for updates to subscriptions or if an update to a resource matches - * a rest-hook subscription - * - * @param theDetails The request details - * @param theResourceTable The actual updated entity - */ - @Override - public void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - String resourceType = theDetails.getResourceType(); - IIdType idType = theDetails.getId(); + request = putRequest; + } + // HTTP POST + else if (theOperation == RestOperationTypeEnum.CREATE && EncodingEnum.JSON.equals(EncodingEnum.forContentType(payload))) { + ourLog.info("JSON payload found"); + StringEntity entity = getStringEntity(EncodingEnum.JSON, theResource); + HttpPost putRequest = new HttpPost(url + "/" + resourceName); + putRequest.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW); + putRequest.setEntity(entity); - logger.info("resource updated type: " + resourceType); + request = putRequest; + } - if (resourceType.equals(Subscription.class.getSimpleName())) { - Subscription subscription = (Subscription) theDetails.getResource(); - if (subscription.getChannel() != null && subscription.getChannel().getType() == Subscription.SubscriptionChannelType.RESTHOOK) { - removeLocalSubscription(subscription.getIdElement().getIdPart()); + // request.addHeader("User-Agent", USER_AGENT); + return request; + } - if (subscription.getStatus() == Subscription.SubscriptionStatus.ACTIVE) { - restHookSubscriptions.add(subscription); - logger.info("Subscription was updated. Id: " + subscription.getId()); - } - } - } else { - checkSubscriptions(idType, resourceType); - } - } + /** + * Search based on a query criteria + * + * @param criteria + * @return + */ + private IBundleProvider getBundleProvider(String criteria) { + RuntimeResourceDefinition responseResourceDef = mySubscriptionDao.validateCriteriaAndReturnResourceDefinition(criteria); + SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(mySubscriptionDao, mySubscriptionDao.getContext(), criteria, responseResourceDef); + + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); + + IFhirResourceDao responseDao = mySubscriptionDao.getDao(responseResourceDef.getImplementingClass()); + IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); + return responseResults; + } + + /** + * Get the encoding from the criteria or return JSON encoding if its not found + * + * @param criteria + * @return + */ + private EncodingEnum getEncoding(String criteria) { + // check criteria + String params = criteria.substring(criteria.indexOf('?') + 1); + List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); + for (NameValuePair nameValuePair : paramValues) { + if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { + return EncodingEnum.forContentType(nameValuePair.getValue()); + } + } + return EncodingEnum.JSON; + } + + /** + * Get subscription from cache + * + * @param id + * @return + */ + private Subscription getLocalSubscription(String id) { + if (id != null && !id.trim().isEmpty()) { + int size = restHookSubscriptions.size(); + if (size > 0) { + for (Subscription restHookSubscription : restHookSubscriptions) { + if (id.equals(restHookSubscription.getIdElement().getIdPart())) { + return restHookSubscription; + } + } + } + } + + return null; + } + + /** + * Convert a resource into a string entity + * + * @param encoding + * @param anyResource + * @return + */ + private StringEntity getStringEntity(EncodingEnum encoding, IAnyResource anyResource) { + String encoded = encoding.newParser(mySubscriptionDao.getContext()).encodeResourceToString(anyResource); + + StringEntity entity; + if (encoded.equalsIgnoreCase(EncodingEnum.JSON.name())) { + entity = new StringEntity(encoded, ContentType.APPLICATION_JSON); + } else { + entity = new StringEntity(encoded, ContentType.APPLICATION_XML); + } + + return entity; + } - /** - * Check subscriptions to see if there is a matching subscription when there is delete - * - * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the - * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been - * pulled out of the {@link HttpServletRequest servlet request}. - * @param theRequest The incoming request - * @param theResponse The response. Note that interceptors may choose to provide a response (i.e. by calling - * {@link HttpServletResponse#getWriter()}) but in that case it is important to return false - * to indicate that the server itself should not also provide a response. - * @return - * @throws AuthenticationException - */ - @Override - public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { - if (theRequestDetails.getRestOperationType().equals(RestOperationTypeEnum.DELETE)) { - String resourceType = theRequestDetails.getResourceName(); - IIdType idType = theRequestDetails.getId(); + /** + * Read the existing subscriptions from the database + */ + public void initSubscriptions() { + SearchParameterMap map = new SearchParameterMap(); + map.add(Subscription.SP_TYPE, new TokenParam(null, Subscription.SubscriptionChannelType.RESTHOOK.toCode())); + map.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())); - if (resourceType.equals(Subscription.class.getSimpleName())) { - String id = idType.getIdPart(); - removeLocalSubscription(id); - } else { - if (notifyOnDelete) { - checkSubscriptions(idType, resourceType); - } - } - } + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); - return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); - } + IBundleProvider subscriptionBundleList = mySubscriptionDao.search(map, req); + List resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); - /** - * Check subscriptions and send notifications or payload - * - * @param idType - * @param resourceType - */ - private void checkSubscriptions(IIdType idType, String resourceType) { - /* - SearchParameterMap map = new SearchParameterMap(); -// map.add("_id", new StringParam("Observation/" + idType.getIdPart())); - map.add("code", new TokenParam("SNOMED-CT", "1000000050")); - //map.setLoadSynchronous(true); -// Include include = new Include("nothing"); -// map.addInclude(include); + for (IBaseResource resource : resourceList) { + restHookSubscriptions.add((Subscription) resource); + } + } - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); + public boolean isNotifyOnDelete() { + return notifyOnDelete; + } - IBundleProvider myBundle = myObservationDao.search(map, req); - Observation myObservation = myObservationDao.read(idType); + @PostConstruct + public void postConstruct() { + try { + executor = Executors.newFixedThreadPool(MAX_THREADS); + } catch (Exception e) { + throw new RuntimeException("Unable to get DAO from PROXY"); + } + } - int mysize = myBundle.size(); - List result = myBundle.getResources(0, myBundle.size()); -*/ - for (Subscription subscription : restHookSubscriptions) { - //see if the criteria matches the created object - logger.info("subscription for " + resourceType + " with criteria " + subscription.getCriteria()); - if (resourceType != null && subscription.getCriteria() != null && !subscription.getCriteria().startsWith(resourceType)) { - logger.info("Skipping subscription search for " + resourceType + " because it does not match the criteria " + subscription.getCriteria()); - continue; - } - //run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource - String criteria = subscription.getCriteria(); - criteria += "&_id=" + idType.getResourceType() + "/" + idType.getIdPart(); - criteria = TMinusService.parseCriteria(criteria); + /** + * Remove subscription from cache + * + * @param subscriptionId + */ + private void removeLocalSubscription(String subscriptionId) { + Subscription localSubscription = getLocalSubscription(subscriptionId); + if (localSubscription != null) { + restHookSubscriptions.remove(localSubscription); + ourLog.info("Subscription removed: " + subscriptionId); + } else { + ourLog.info("Subscription not found in local list. Subscription id: " + subscriptionId); + } + } - IBundleProvider results = getBundleProvider(criteria); + /** + * Handles incoming resources. If the resource is a rest-hook subscription, it adds + * it to the rest-hook subscription list. Otherwise it checks to see if the resource + * matches any rest-hook subscriptions. + */ + @Override + public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { + IIdType idType = theResource.getIdElement(); + ourLog.info("resource created type: {}", theRequest.getResourceName()); - if (results.size() == 0) { - continue; - } + if (theResource instanceof Subscription) { + Subscription subscription = (Subscription) theResource; + if (subscription.getChannel() != null + && subscription.getChannel().getType() == Subscription.SubscriptionChannelType.RESTHOOK + && subscription.getStatus() == Subscription.SubscriptionStatus.REQUESTED) { + subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); + mySubscriptionDao.update(subscription); + restHookSubscriptions.add(subscription); + ourLog.info("Subscription was added. Id: " + subscription.getId()); + } + } else { + checkSubscriptions(idType, theRequest.getResourceName(), RestOperationTypeEnum.CREATE); + } + } - //should just be one resource as it was filtered by the id - for (IBaseResource nextBase : results.getResources(0, results.size())) { - IAnyResource next = (IAnyResource) nextBase; - logger.info("Found match: queueing rest-hook notification for resource: {}", next.getIdElement()); - HttpUriRequest request = createRequest(subscription, next); - executor.submit(new HttpRequestDstu3Job(request, subscription)); - } - } - } + /** + * Check subscriptions to see if there is a matching subscription when there is delete + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the {@link HttpServletRequest servlet request}. + * @param theRequest + * The incoming request + * @param theResponse + * The response. Note that interceptors may choose to provide a response (i.e. by calling + * {@link HttpServletResponse#getWriter()}) but in that case it is important to return false + * to indicate that the server itself should not also provide a response. + */ + @Override + public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { + String resourceType = theRequest.getResourceName(); + IIdType idType = theResource.getIdElement(); - /** - * Creates an HTTP Post for a subscription - * - * @param subscription - * @param resource - * @return - */ - private HttpUriRequest createRequest(Subscription subscription, IAnyResource resource) { - String url = subscription.getChannel().getEndpoint(); - HttpUriRequest request = null; + if (resourceType.equals(Subscription.class.getSimpleName())) { + String id = idType.getIdPart(); + removeLocalSubscription(id); + } else { + if (notifyOnDelete) { + checkSubscriptions(idType, resourceType, RestOperationTypeEnum.DELETE); + } + } + } - String payload = subscription.getChannel().getPayload(); - //HTTP post - if (payload == null || payload.trim().length() == 0) { - //return an empty response as there is no payload - logger.info("No payload found, returning an empty notification"); - request = new HttpPost(url); - } - //HTTP put - else if (EncodingEnum.XML.equals(EncodingEnum.forContentType(payload))) { - logger.info("XML payload found"); - StringEntity entity = getStringEntity(EncodingEnum.XML, resource); - HttpPut putRequest = new HttpPut(url); - putRequest.setEntity(entity); + /** + * Checks for updates to subscriptions or if an update to a resource matches + * a rest-hook subscription + */ + @Override + public void resourceUpdated(RequestDetails theRequest, IBaseResource theResource) { + String resourceType = theRequest.getResourceName(); + IIdType idType = theResource.getIdElement(); - request = putRequest; - } - //HTTP put - else if (EncodingEnum.JSON.equals(EncodingEnum.forContentType(payload))) { - logger.info("JSON payload found"); - StringEntity entity = getStringEntity(EncodingEnum.JSON, resource); - HttpPut putRequest = new HttpPut(url); - putRequest.setEntity(entity); + ourLog.info("resource updated type: " + resourceType); - request = putRequest; - } - //HTTP post - else if (payload.startsWith("application/fhir+query/")) { //custom payload that is a FHIR query - logger.info("Custom query payload found"); - String responseCriteria = subscription.getChannel().getPayload().substring(23); - responseCriteria = TMinusService.parseCriteria(responseCriteria); + if (theResource instanceof Subscription) { + Subscription subscription = (Subscription) theResource; + if (subscription.getChannel() != null && subscription.getChannel().getType() == Subscription.SubscriptionChannelType.RESTHOOK) { + removeLocalSubscription(subscription.getIdElement().getIdPart()); - //get the encoding type from payload which is a FHIR query with &_format= - EncodingEnum encoding = getEncoding(responseCriteria); - IBundleProvider responseResults = getBundleProvider(responseCriteria); - if (responseResults.size() != 0) { - List resourcelist = responseResults.getResources(0, responseResults.size()); - Bundle bundle = createBundle(resourcelist); - StringEntity bundleEntity = getStringEntity(encoding, bundle); - HttpPost postRequest = new HttpPost(url); - postRequest.setEntity(bundleEntity); + if (subscription.getStatus() == Subscription.SubscriptionStatus.ACTIVE) { + restHookSubscriptions.add(subscription); + ourLog.info("Subscription was updated. Id: " + subscription.getId()); + } + } + } else { + checkSubscriptions(idType, resourceType, RestOperationTypeEnum.UPDATE); + } + } - request = postRequest; - } else { - Bundle bundle = new Bundle(); - bundle.setTotal(0); - StringEntity bundleEntity = getStringEntity(encoding, bundle); - HttpPost postRequest = new HttpPost(url); - postRequest.setEntity(bundleEntity); - - request = postRequest; - } - } else { - logger.warn("Unsupported payload " + payload + ". Returning an empty notification"); - request = new HttpPost(url); - } - - //request.addHeader("User-Agent", USER_AGENT); - return request; - } - - /** - * Get the encoding from the criteria or return JSON encoding if its not found - * - * @param criteria - * @return - */ - private EncodingEnum getEncoding(String criteria) { - //check criteria - String params = criteria.substring(criteria.indexOf('?') + 1); - List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); - for (NameValuePair nameValuePair : paramValues) { - if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { - return EncodingEnum.forContentType(nameValuePair.getValue()); - } - } - return EncodingEnum.JSON; - } - - /** - * Search based on a query criteria - * - * @param criteria - * @return - */ - private IBundleProvider getBundleProvider(String criteria) { - RuntimeResourceDefinition responseResourceDef = mySubscriptionDao.validateCriteriaAndReturnResourceDefinition(criteria); - SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(mySubscriptionDao, mySubscriptionDao.getContext(), criteria, responseResourceDef); - - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); - - IFhirResourceDao responseDao = mySubscriptionDao.getDao(responseResourceDef.getImplementingClass()); - IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); - return responseResults; - } - - /** - * Create a bundle to return to the client - * - * @param resourcelist - * @return - */ - private Bundle createBundle(List resourcelist) { - Bundle bundle = new Bundle(); - for (IBaseResource resource : resourcelist) { - Bundle.BundleEntryComponent entry = bundle.addEntry(); - entry.setResource((Resource) resource); - } - bundle.setTotal(resourcelist.size()); - - return bundle; - } - - /** - * Convert a resource into a string entity - * - * @param encoding - * @param anyResource - * @return - */ - private StringEntity getStringEntity(EncodingEnum encoding, IAnyResource anyResource) { - String encoded = encoding.newParser(mySubscriptionDao.getContext()).encodeResourceToString(anyResource); - - StringEntity entity; - if (encoded.equalsIgnoreCase(EncodingEnum.JSON.name())) { - entity = new StringEntity(encoded, ContentType.APPLICATION_JSON); - } else { - entity = new StringEntity(encoded, ContentType.APPLICATION_XML); - } - - return entity; - } - - @Override - public void resourceDeleted(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - } - - /** - * Remove subscription from cache - * - * @param subscriptionId - */ - private void removeLocalSubscription(String subscriptionId) { - Subscription localSubscription = getLocalSubscription(subscriptionId); - if (localSubscription != null) { - restHookSubscriptions.remove(localSubscription); - logger.info("Subscription removed: " + subscriptionId); - } else { - logger.info("Subscription not found in local list. Subscription id: " + subscriptionId); - } - } - - /** - * Get subscription from cache - * - * @param id - * @return - */ - private Subscription getLocalSubscription(String id) { - if (id != null && !id.trim().isEmpty()) { - int size = restHookSubscriptions.size(); - if (size > 0) { - for (Subscription restHookSubscription : restHookSubscriptions) { - if (id.equals(restHookSubscription.getIdElement().getIdPart())) { - return restHookSubscription; - } - } - } - } - - return null; - } - - public boolean isNotifyOnDelete() { - return notifyOnDelete; - } - - public void setNotifyOnDelete(boolean notifyOnDelete) { - this.notifyOnDelete = notifyOnDelete; - } + public void setNotifyOnDelete(boolean notifyOnDelete) { + this.notifyOnDelete = notifyOnDelete; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu3Interceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu3Interceptor.java index e80488b95a9..40044a92e2b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu3Interceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu3Interceptor.java @@ -1,19 +1,19 @@ /* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). * - * 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 + * 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 + * 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. + * 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. * - * @author Jeff Chung + * @author Jeff Chung */ package ca.uhn.fhir.jpa.interceptor; @@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -37,65 +38,70 @@ import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class WebSocketSubscriptionDstu3Interceptor extends InterceptorAdapter implements IJpaServerInterceptor { +public class WebSocketSubscriptionDstu3Interceptor extends InterceptorAdapter implements IServerOperationInterceptor { - private static final Logger logger = LoggerFactory.getLogger(WebSocketSubscriptionDstu3Interceptor.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(WebSocketSubscriptionDstu3Interceptor.class); - @Autowired - @Qualifier("mySubscriptionDaoDstu3") - private IFhirResourceDao reference; + private IFhirResourceDaoSubscription mySubscriptionDaoCasted; - private IFhirResourceDaoSubscription casted; + @Autowired + @Qualifier("mySubscriptionDaoDstu3") + private IFhirResourceDao mySubscriptionDao; - @PostConstruct - public void postConstruct(){ - casted = (IFhirResourceDaoSubscription) reference; - } + @Override + public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { + if (theRequestDetails.getRestOperationType().equals(RestOperationTypeEnum.DELETE)) { + mySubscriptionDaoCasted.pollForNewUndeliveredResources(theRequestDetails.getResourceName()); + } - @Override - public void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - } + return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); + } - @Override - public void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - } + /** + * Checks for websocket subscriptions + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the {@link HttpServletRequest servlet request}. + * @param theResponseObject + * The actual object which is being streamed to the client as a response + * @return + */ + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { + if (theRequestDetails.getResourceName() == null || + theRequestDetails.getResourceName().isEmpty() || + theRequestDetails.getResourceName().equals("Subscription")) { + return super.outgoingResponse(theRequestDetails, theResponseObject); + } - @Override - public void resourceDeleted(ActionRequestDetails theDetails, ResourceTable theResourceTable) { - } + if (theRequestDetails.getRequestType().equals(RequestTypeEnum.POST) || theRequestDetails.getRequestType().equals(RequestTypeEnum.PUT)) { + ourLog.info("Found POST or PUT for a non-subscription resource"); + mySubscriptionDaoCasted.pollForNewUndeliveredResources(theRequestDetails.getResourceName()); + } - /** - * Checks for websocket subscriptions - * @param theRequestDetails - * A bean containing details about the request that is about to be processed, including details such as the - * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been - * pulled out of the {@link HttpServletRequest servlet request}. - * @param theResponseObject - * The actual object which is being streamed to the client as a response - * @return - */ - @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { - if (theRequestDetails.getResourceName() == null || - theRequestDetails.getResourceName().isEmpty() || - theRequestDetails.getResourceName().equals("Subscription")) { - return super.outgoingResponse(theRequestDetails, theResponseObject); - } + return super.outgoingResponse(theRequestDetails, theResponseObject); + } - if (theRequestDetails.getRequestType().equals(RequestTypeEnum.POST) || theRequestDetails.getRequestType().equals(RequestTypeEnum.PUT)) { - logger.info("Found POST or PUT for a non-subscription resource"); - casted.pollForNewUndeliveredResources(theRequestDetails.getResourceName()); - } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @PostConstruct + public void postConstruct() { + mySubscriptionDaoCasted = (IFhirResourceDaoSubscription) mySubscriptionDao; + } - return super.outgoingResponse(theRequestDetails, theResponseObject); - } + @Override + public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { + // nothing + } - @Override - public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { - if (theRequestDetails.getRestOperationType().equals(RestOperationTypeEnum.DELETE)) { - casted.pollForNewUndeliveredResources(theRequestDetails.getResourceName()); - } + @Override + public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { + // nothing + } - return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); - } + @Override + public void resourceUpdated(RequestDetails theRequest, IBaseResource theResource) { + // nothing + } } \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java index 6ecc7e288be..96673f89ef2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java @@ -24,6 +24,7 @@ import org.springframework.web.servlet.DispatcherServlet; import ca.uhn.fhir.jpa.config.WebsocketDstu2Config; import ca.uhn.fhir.jpa.config.WebsocketDstu2DispatcherConfig; import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test; +import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu2Interceptor; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.BundleEntry; @@ -43,50 +44,16 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test { protected static IGenericClient ourClient; protected static CloseableHttpClient ourHttpClient; protected static int ourPort; + protected static RestfulServer ourRestServer; private static Server ourServer; protected static String ourServerBase; private static GenericWebApplicationContext ourWebApplicationContext; - protected static RestfulServer ourRestServer; - - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - + protected static RestHookSubscriptionDstu2Interceptor ourRestHookSubscriptionInterceptor; public BaseResourceProviderDstu2Test() { super(); } - protected List toIdListUnqualifiedVersionless(Bundle found) { - List list = new ArrayList(); - for (BundleEntry next : found.getEntries()) { - list.add(next.getResource().getId().toUnqualifiedVersionless()); - } - return list; - } - - protected List toNameList(Bundle resp) { - List names = new ArrayList(); - for (BundleEntry next : resp.getEntries()) { - Patient nextPt = (Patient) next.getResource(); - String nextStr = nextPt.getNameFirstRep().getGivenAsSingleString() + " " + nextPt.getNameFirstRep().getFamilyAsSingleString(); - if (isNotBlank(nextStr)) { - names.add(nextStr); - } - } - return names; - } - - @AfterClass - public static void afterClass() throws Exception { - ourServer.stop(); - ourHttpClient.close(); - ourServer = null; - ourHttpClient = null; - ourWebApplicationContext.close(); - ourWebApplicationContext = null; - } @After public void after() throws Exception { @@ -131,13 +98,18 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test { ourWebApplicationContext.setParent(myAppCtx); ourWebApplicationContext.refresh(); + ourRestHookSubscriptionInterceptor = ourWebApplicationContext.getBean(RestHookSubscriptionDstu2Interceptor.class); + proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext); DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class); ServletHolder subsServletHolder = new ServletHolder(); subsServletHolder.setServlet(dispatcherServlet); - subsServletHolder.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, WebsocketDstu2Config.class.getName() + "\n" + WebsocketDstu2DispatcherConfig.class.getName()); + subsServletHolder.setInitParameter( + ContextLoader.CONFIG_LOCATION_PARAM, + WebsocketDstu2Config.class.getName() + "\n" + + WebsocketDstu2DispatcherConfig.class.getName()); proxyHandler.addServlet(subsServletHolder, "/*"); @@ -156,4 +128,35 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test { } } + protected List toIdListUnqualifiedVersionless(Bundle found) { + List list = new ArrayList(); + for (BundleEntry next : found.getEntries()) { + list.add(next.getResource().getId().toUnqualifiedVersionless()); + } + return list; + } + + protected List toNameList(Bundle resp) { + List names = new ArrayList(); + for (BundleEntry next : resp.getEntries()) { + Patient nextPt = (Patient) next.getResource(); + String nextStr = nextPt.getNameFirstRep().getGivenAsSingleString() + " " + nextPt.getNameFirstRep().getFamilyAsSingleString(); + if (isNotBlank(nextStr)) { + names.add(nextStr); + } + } + return names; + } + + @AfterClass + public static void afterClassClearContextBaseResourceProviderDstu3Test() throws Exception { + ourServer.stop(); + ourHttpClient.close(); + ourServer = null; + ourHttpClient = null; + ourWebApplicationContext.close(); + ourWebApplicationContext = null; + TestUtil.clearAllStaticFieldsForUnitTest(); + } + } \ No newline at end of file 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 5d56b229e72..e7801fb2954 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 @@ -31,6 +31,7 @@ import ca.uhn.fhir.jpa.config.dstu3.WebsocketDstu3Config; import ca.uhn.fhir.jpa.config.dstu3.WebsocketDstu3DispatcherConfig; import ca.uhn.fhir.jpa.dao.data.ISearchDao; 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; @@ -55,6 +56,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { protected static String ourServerBase; private static GenericWebApplicationContext ourWebApplicationContext; private TerminologyUploaderProviderDstu3 myTerminologyUploaderProvider; + protected static RestHookSubscriptionDstu3Interceptor ourRestHookSubscriptionInterceptor; protected static ISearchDao mySearchEntityDao; protected static ISearchCoordinatorSvc mySearchCoordinatorSvc; @@ -117,7 +119,10 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { dispatcherServlet.setContextClass(AnnotationConfigWebApplicationContext.class); ServletHolder subsServletHolder = new ServletHolder(); subsServletHolder.setServlet(dispatcherServlet); - subsServletHolder.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, WebsocketDstu3Config.class.getName() + "\n" + WebsocketDstu3DispatcherConfig.class.getName()); + subsServletHolder.setInitParameter( + ContextLoader.CONFIG_LOCATION_PARAM, + WebsocketDstu3Config.class.getName() + "\n" + + WebsocketDstu3DispatcherConfig.class.getName()); proxyHandler.addServlet(subsServletHolder, "/*"); // Register a CORS filter @@ -143,6 +148,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { myValidationSupport = wac.getBean(JpaValidationSupportChainDstu3.class); mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class); mySearchEntityDao = wac.getBean(ISearchDao.class); + ourRestHookSubscriptionInterceptor = wac.getBean(RestHookSubscriptionDstu3Interceptor.class); ourClient = myFhirCtx.newRestfulGenericClient(ourServerBase); ourClient.registerInterceptor(new LoggingInterceptor(true)); diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu2Util.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirDstu2Util.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu2Util.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirDstu2Util.java index dc294f517fc..de0a6d18013 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu2Util.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirDstu2Util.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.composite.IdentifierDt; diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu3Util.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirDstu3Util.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu3Util.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirDstu3Util.java index 6b608645c5b..e5184f9939c 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu3Util.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirDstu3Util.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.IGenericClient; diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirServiceUtil.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirServiceUtil.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirServiceUtil.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirServiceUtil.java index 9accc677f7b..a31ee1546f4 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirServiceUtil.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirServiceUtil.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.MethodOutcome; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithCriteriaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithCriteriaDstu2Test.java new file mode 100644 index 00000000000..b18e04dad67 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithCriteriaDstu2Test.java @@ -0,0 +1,193 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ +package ca.uhn.fhir.jpa.subscription; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; + +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.composite.ResourceReferenceDt; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Patient; +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.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.EncodingEnum; + +/** + * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the + * subscription + *

+ * Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3Test for + * a test that returns the xml of the observation + *

+ * To execute the following test, execute it the following way: + * 0. execute 'clean' test + * 1. Execute the 'createSubscription' test + * 2. Update the subscription id in the 'attachWebSocket' test + * 3. Execute the 'attachWebSocket' test + * 4. Execute the 'sendObservation' test + * 5. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id + */ +public class FhirSubscriptionWithCriteriaDstu2Test extends BaseResourceProviderDstu2Test { + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithCriteriaDstu2Test.class); + + private String myPatientId; + private String mySubscriptionId; + private WebSocketClient myWebSocketClient; + private SocketImplementation mySocketImplementation; + + @After + public void after() throws Exception { + super.after(); + myDaoConfig.setSubscriptionEnabled(new DaoConfig().isSubscriptionEnabled()); + myDaoConfig.setSubscriptionPollDelay(new DaoConfig().getSubscriptionPollDelay()); + } + + @Before + public void before() throws Exception { + super.before(); + + myDaoConfig.setSubscriptionEnabled(true); + myDaoConfig.setSubscriptionPollDelay(0L); + + /* + * Create patient + */ + + Patient patient = FhirDstu2Util.getPatient(); + MethodOutcome methodOutcome = ourClient.create().resource(patient).execute(); + myPatientId = methodOutcome.getId().getIdPart(); + + /* + * Create subscription + */ + Subscription subscription = new Subscription(); + subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); + subscription.setStatus(SubscriptionStatusEnum.ACTIVE); + // subscription.setCriteria("Observation?subject=Patient/" + PATIENT_ID); + subscription.setCriteria("Observation?code=SNOMED-CT|82313006&_format=xml"); + + Channel channel = new Channel(); + channel.setType(SubscriptionChannelTypeEnum.WEBSOCKET); + channel.setPayload("application/json"); + subscription.setChannel(channel); + + methodOutcome = ourClient.create().resource(subscription).execute(); + mySubscriptionId = methodOutcome.getId().getIdPart(); + + /* + * Attach websocket + */ + + myWebSocketClient = new WebSocketClient(); + mySocketImplementation = new SocketImplementation(mySubscriptionId, EncodingEnum.JSON); + + myWebSocketClient.start(); + URI echoUri = new URI("ws://localhost:" + ourPort + "/websocket/dstu2"); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + ourLog.info("Connecting to : {}", echoUri); + Future connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request); + Session session = connection.get(2, TimeUnit.SECONDS); + + ourLog.info("Connected to WS: {}", session.isOpen()); + } + + @After + public void afterCloseWebsocket() throws Exception { + ourLog.info("Shutting down websocket client"); + myWebSocketClient.stop(); + } + + @Test + public void createObservation() throws Exception { + Observation observation = new Observation(); + CodeableConceptDt cc = new CodeableConceptDt(); + observation.setCode(cc); + CodingDt coding = cc.addCoding(); + coding.setCode("82313006"); + coding.setSystem("SNOMED-CT"); + ResourceReferenceDt reference = new ResourceReferenceDt(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(ObservationStatusEnum.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(1, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId, "ping " + mySubscriptionId)); + } + + @Test + public void createObservationThatDoesNotMatch() throws Exception { + Observation observation = new Observation(); + CodeableConceptDt cc = new CodeableConceptDt(); + observation.setCode(cc); + CodingDt coding = cc.addCoding(); + coding.setCode("8231"); + coding.setSystem("SNOMED-CT"); + ResourceReferenceDt reference = new ResourceReferenceDt(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(ObservationStatusEnum.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(0, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId)); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithCriteriaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithCriteriaDstu3Test.java new file mode 100644 index 00000000000..17c932adde6 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithCriteriaDstu3Test.java @@ -0,0 +1,192 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ +package ca.uhn.fhir.jpa.subscription; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.Subscription; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.EncodingEnum; + +/** + * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the + * subscription + *

+ * Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3Test for + * a test that returns the xml of the observation + *

+ * To execute the following test, execute it the following way: + * 0. execute 'clean' test + * 1. Execute the 'createPatient' test + * 2. Update the patient id static variable + * 3. Execute the 'createSubscription' test + * 4. Update the subscription id static variable + * 5. Execute the 'attachWebSocket' test + * 6. Execute the 'sendObservation' test + * 7. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id + */ +public class FhirSubscriptionWithCriteriaDstu3Test extends BaseResourceProviderDstu3Test { + + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithCriteriaDstu3Test.class); + + private String myPatientId; + private String mySubscriptionId; + private WebSocketClient myWebSocketClient; + private SocketImplementation mySocketImplementation; + + @After + public void after() throws Exception { + super.after(); + myDaoConfig.setSubscriptionEnabled(new DaoConfig().isSubscriptionEnabled()); + myDaoConfig.setSubscriptionPollDelay(new DaoConfig().getSubscriptionPollDelay()); + } + + @Before + public void before() throws Exception { + super.before(); + + myDaoConfig.setSubscriptionEnabled(true); + myDaoConfig.setSubscriptionPollDelay(0L); + + /* + * Create patient + */ + + Patient patient = FhirDstu3Util.getPatient(); + MethodOutcome methodOutcome = ourClient.create().resource(patient).execute(); + myPatientId = methodOutcome.getId().getIdPart(); + + /* + * Create subscription + */ + 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("Observation?subject=Patient/" + PATIENT_ID); + subscription.setCriteria("Observation?code=SNOMED-CT|82313006&_format=xml"); + + Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); + channel.setType(Subscription.SubscriptionChannelType.WEBSOCKET); + channel.setPayload("application/json"); + subscription.setChannel(channel); + + methodOutcome = ourClient.create().resource(subscription).execute(); + mySubscriptionId = methodOutcome.getId().getIdPart(); + + /* + * Attach websocket + */ + + myWebSocketClient = new WebSocketClient(); + mySocketImplementation = new SocketImplementation(mySubscriptionId, EncodingEnum.JSON); + + myWebSocketClient.start(); + URI echoUri = new URI("ws://localhost:" + ourPort + "/websocket/dstu3"); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + ourLog.info("Connecting to : {}", echoUri); + Future connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request); + Session session = connection.get(2, TimeUnit.SECONDS); + + ourLog.info("Connected to WS: {}", session.isOpen()); + } + + @After + public void afterCloseWebsocket() throws Exception { + ourLog.info("Shutting down websocket client"); + myWebSocketClient.stop(); + } + + @Test + public void createObservation() throws Exception { + Observation observation = new Observation(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode("82313006"); + coding.setSystem("SNOMED-CT"); + Reference reference = new Reference(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(Observation.ObservationStatus.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(1, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId, "ping " + mySubscriptionId)); + } + + @Test + public void createObservationThatDoesNotMatch() throws Exception { + Observation observation = new Observation(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode("8231"); + coding.setSystem("SNOMED-CT"); + Reference reference = new Reference(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(Observation.ObservationStatus.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(0, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId)); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithSubscriptionIdDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithSubscriptionIdDstu2Test.java new file mode 100644 index 00000000000..9dc0aea095e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithSubscriptionIdDstu2Test.java @@ -0,0 +1,193 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ +package ca.uhn.fhir.jpa.subscription; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; + +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.composite.ResourceReferenceDt; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Patient; +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.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.EncodingEnum; + +/** + * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the + * subscription + *

+ * Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3Test for + * a test that returns the xml of the observation + *

+ * To execute the following test, execute it the following way: + * 0. execute 'clean' test + * 1. Execute the 'createSubscription' test + * 2. Update the subscription id in the 'attachWebSocket' test + * 3. Execute the 'attachWebSocket' test + * 4. Execute the 'sendObservation' test + * 5. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id + */ +public class FhirSubscriptionWithSubscriptionIdDstu2Test extends BaseResourceProviderDstu2Test { + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu2Test.class); + + private String myPatientId; + private String mySubscriptionId; + private WebSocketClient myWebSocketClient; + private SocketImplementation mySocketImplementation; + + @After + public void after() throws Exception { + super.after(); + myDaoConfig.setSubscriptionEnabled(new DaoConfig().isSubscriptionEnabled()); + myDaoConfig.setSubscriptionPollDelay(new DaoConfig().getSubscriptionPollDelay()); + } + + @Before + public void before() throws Exception { + super.before(); + + myDaoConfig.setSubscriptionEnabled(true); + myDaoConfig.setSubscriptionPollDelay(0L); + + /* + * Create patient + */ + + Patient patient = FhirDstu2Util.getPatient(); + MethodOutcome methodOutcome = ourClient.create().resource(patient).execute(); + myPatientId = methodOutcome.getId().getIdPart(); + + /* + * Create subscription + */ + Subscription subscription = new Subscription(); + subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); + subscription.setStatus(SubscriptionStatusEnum.ACTIVE); + // subscription.setCriteria("Observation?subject=Patient/" + PATIENT_ID); + subscription.setCriteria("Observation?code=SNOMED-CT|82313006"); + + Channel channel = new Channel(); + channel.setType(SubscriptionChannelTypeEnum.WEBSOCKET); + channel.setPayload("application/json"); + subscription.setChannel(channel); + + methodOutcome = ourClient.create().resource(subscription).execute(); + mySubscriptionId = methodOutcome.getId().getIdPart(); + + /* + * Attach websocket + */ + + myWebSocketClient = new WebSocketClient(); + mySocketImplementation = new SocketImplementation(mySubscriptionId, EncodingEnum.JSON); + + myWebSocketClient.start(); + URI echoUri = new URI("ws://localhost:" + ourPort + "/websocket/dstu2"); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + ourLog.info("Connecting to : {}", echoUri); + Future connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request); + Session session = connection.get(2, TimeUnit.SECONDS); + + ourLog.info("Connected to WS: {}", session.isOpen()); + } + + @After + public void afterCloseWebsocket() throws Exception { + ourLog.info("Shutting down websocket client"); + myWebSocketClient.stop(); + } + + @Test + public void createObservation() throws Exception { + Observation observation = new Observation(); + CodeableConceptDt cc = new CodeableConceptDt(); + observation.setCode(cc); + CodingDt coding = cc.addCoding(); + coding.setCode("82313006"); + coding.setSystem("SNOMED-CT"); + ResourceReferenceDt reference = new ResourceReferenceDt(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(ObservationStatusEnum.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(1, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId, "ping " + mySubscriptionId)); + } + + @Test + public void createObservationThatDoesNotMatch() throws Exception { + Observation observation = new Observation(); + CodeableConceptDt cc = new CodeableConceptDt(); + observation.setCode(cc); + CodingDt coding = cc.addCoding(); + coding.setCode("8231"); + coding.setSystem("SNOMED-CT"); + ResourceReferenceDt reference = new ResourceReferenceDt(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(ObservationStatusEnum.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(0, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId)); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithSubscriptionIdDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithSubscriptionIdDstu3Test.java new file mode 100644 index 00000000000..26224bed1e1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirSubscriptionWithSubscriptionIdDstu3Test.java @@ -0,0 +1,192 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ +package ca.uhn.fhir.jpa.subscription; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.Subscription; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.EncodingEnum; + +/** + * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the + * subscription + *

+ * Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3Test for + * a test that returns the xml of the observation + *

+ * To execute the following test, execute it the following way: + * 0. execute 'clean' test + * 1. Execute the 'createPatient' test + * 2. Update the patient id static variable + * 3. Execute the 'createSubscription' test + * 4. Update the subscription id static variable + * 5. Execute the 'attachWebSocket' test + * 6. Execute the 'sendObservation' test + * 7. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id + */ +public class FhirSubscriptionWithSubscriptionIdDstu3Test extends BaseResourceProviderDstu3Test { + + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3Test.class); + + private String myPatientId; + private String mySubscriptionId; + private WebSocketClient myWebSocketClient; + private SocketImplementation mySocketImplementation; + + @After + public void after() throws Exception { + super.after(); + myDaoConfig.setSubscriptionEnabled(new DaoConfig().isSubscriptionEnabled()); + myDaoConfig.setSubscriptionPollDelay(new DaoConfig().getSubscriptionPollDelay()); + } + + @Before + public void before() throws Exception { + super.before(); + + myDaoConfig.setSubscriptionEnabled(true); + myDaoConfig.setSubscriptionPollDelay(0L); + + /* + * Create patient + */ + + Patient patient = FhirDstu3Util.getPatient(); + MethodOutcome methodOutcome = ourClient.create().resource(patient).execute(); + myPatientId = methodOutcome.getId().getIdPart(); + + /* + * Create subscription + */ + 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("Observation?subject=Patient/" + PATIENT_ID); + subscription.setCriteria("Observation?code=SNOMED-CT|82313006"); + + Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); + channel.setType(Subscription.SubscriptionChannelType.WEBSOCKET); + channel.setPayload("application/json"); + subscription.setChannel(channel); + + methodOutcome = ourClient.create().resource(subscription).execute(); + mySubscriptionId = methodOutcome.getId().getIdPart(); + + /* + * Attach websocket + */ + + myWebSocketClient = new WebSocketClient(); + mySocketImplementation = new SocketImplementation(mySubscriptionId, EncodingEnum.JSON); + + myWebSocketClient.start(); + URI echoUri = new URI("ws://localhost:" + ourPort + "/websocket/dstu3"); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + ourLog.info("Connecting to : {}", echoUri); + Future connection = myWebSocketClient.connect(mySocketImplementation, echoUri, request); + Session session = connection.get(2, TimeUnit.SECONDS); + + ourLog.info("Connected to WS: {}", session.isOpen()); + } + + @After + public void afterCloseWebsocket() throws Exception { + ourLog.info("Shutting down websocket client"); + myWebSocketClient.stop(); + } + + @Test + public void createObservation() throws Exception { + Observation observation = new Observation(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode("82313006"); + coding.setSystem("SNOMED-CT"); + Reference reference = new Reference(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(Observation.ObservationStatus.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(1, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId, "ping " + mySubscriptionId)); + } + + @Test + public void createObservationThatDoesNotMatch() throws Exception { + Observation observation = new Observation(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode("8231"); + coding.setSystem("SNOMED-CT"); + Reference reference = new Reference(); + reference.setReference("Patient/" + myPatientId); + observation.setSubject(reference); + observation.setStatus(Observation.ObservationStatus.FINAL); + + MethodOutcome methodOutcome2 = ourClient.create().resource(observation).execute(); + String observationId = methodOutcome2.getId().getIdPart(); + observation.setId(observationId); + + ourLog.info("Observation id generated by server is: " + observationId); + + int changes = mySubscriptionDao.pollForNewUndeliveredResources(); + ourLog.info("Polling showed {}", changes); + assertEquals(0, changes); + + Thread.sleep(2000); + + ourLog.info("WS Messages: {}", mySocketImplementation.getMessages()); + assertThat(mySocketImplementation.getMessages(), contains("bound " + mySubscriptionId)); + } +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu2TestIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RemoveDstu2TestIT.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu2TestIT.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RemoveDstu2TestIT.java index e082dd81fb2..ec17d2ac019 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu2TestIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RemoveDstu2TestIT.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Observation; diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu3TestIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RemoveDstu3TestIT.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu3TestIT.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RemoveDstu3TestIT.java index 34e556d3523..9ef524572cb 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu3TestIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RemoveDstu3TestIT.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.gclient.IQuery; diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu2IT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2IT.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu2IT.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2IT.java index 336326a35ac..bb6665a7b35 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu2IT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2IT.java @@ -16,7 +16,7 @@ * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt; @@ -39,7 +39,7 @@ import org.slf4j.Logger; @Ignore public class RestHookTestDstu2IT { - private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3IT.class); + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3Test.class); private static String code = "1000000012"; private IGenericClient client = FhirServiceUtil.getFhirDstu2Client(); 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 new file mode 100644 index 00000000000..44b7bda1345 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java @@ -0,0 +1,318 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ + +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.*; + +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.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; + +/** + * Test the rest-hook subscriptions + */ +public class RestHookTestDstu2Test 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(RestHookTestDstu2Test.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()); + + ourRestServer.unregisterInterceptor(ourRestHookSubscriptionInterceptor); + } + + @Before + public void beforeRegisterRestHookListener() { +// ourRestHookSubscriptionInterceptor.set + ourRestServer.registerInterceptor(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.REQUESTED); + 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 = RandomServerPortProvider.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/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 new file mode 100644 index 00000000000..21e7d6234d9 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java @@ -0,0 +1,312 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ + +package ca.uhn.fhir.jpa.subscription; + +import static org.junit.Assert.*; + +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.dstu3.model.*; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.*; + +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.dstu3.BaseResourceProviderDstu3Test; +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.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; + +/** + * Test the rest-hook subscriptions + */ +public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { + + 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 + 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()); + + ourRestServer.unregisterInterceptor(ourRestHookSubscriptionInterceptor); + } + + @Before + public void beforeRegisterRestHookListener() { +// ourRestHookSubscriptionInterceptor.set + ourRestServer.registerInterceptor(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(Subscription.SubscriptionStatus.REQUESTED); + subscription.setCriteria(criteria); + + Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); + channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); + 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(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(code); + coding.setSystem(system); + + observation.setStatus(Observation.ObservationStatus.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()); + CodeableConcept codeableConcept = new CodeableConcept(); + observation3.setCode(codeableConcept); + Coding 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()); + + CodeableConcept codeableConcept1 = new CodeableConcept(); + observation3a.setCode(codeableConcept1); + Coding 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()); + CodeableConcept codeableConcept = new CodeableConcept(); + observation3.setCode(codeableConcept); + Coding 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()); + + CodeableConcept codeableConcept1 = new CodeableConcept(); + observation3a.setCode(codeableConcept1); + Coding 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 = RandomServerPortProvider.findFreePort(); + ourListenerRestServer = new RestfulServer(FhirContext.forDstu3()); + 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 IdType("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 IdType("Observation/1"), false); + } + + } + +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java similarity index 98% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java index 23b0419546b..d1c4ed3442d 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java @@ -16,7 +16,7 @@ * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.MethodOutcome; @@ -36,7 +36,7 @@ import org.slf4j.Logger; @Ignore public class RestHookTestDstu3WithSubscriptionResponseCriteriaIT { - private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3IT.class); + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3Test.class); @Test public void testRestHookSubscription() { diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu2TestsIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/ResthookSubscriptionDstu2TestsIT.java similarity index 99% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu2TestsIT.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/ResthookSubscriptionDstu2TestsIT.java index 3e5288200dc..61e2f6e40c0 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu2TestsIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/ResthookSubscriptionDstu2TestsIT.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.model.dstu2.resource.Observation; import ca.uhn.fhir.model.dstu2.resource.Subscription; diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu3TestsIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/ResthookSubscriptionDstu3TestsIT.java similarity index 99% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu3TestsIT.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/ResthookSubscriptionDstu3TestsIT.java index b143a61fd36..f2dcfd61067 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu3TestsIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/ResthookSubscriptionDstu3TestsIT.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.rest.client.IGenericClient; import org.hl7.fhir.dstu3.model.DateTimeType; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SocketImplementation.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SocketImplementation.java new file mode 100644 index 00000000000..f7b4da78748 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SocketImplementation.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + * + * @author Jeff Chung + */ + +package ca.uhn.fhir.jpa.subscription; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.slf4j.Logger; + +import ca.uhn.fhir.rest.server.EncodingEnum; + +@WebSocket +public class SocketImplementation { + + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(SocketImplementation.class); + private String myCriteria; + protected String myError; + protected boolean myGotBound; + private List myMessages = new ArrayList(); + protected int myPingCount; + protected String mySubsId; + private Session session; + + public SocketImplementation(String theCriteria, EncodingEnum theEncoding) { + myCriteria = theCriteria; + } + + public List getMessages() { + return myMessages; + } + + public void keepAlive() { + if (this.session != null) { + try { + session.getRemote().sendString("keep alive"); + } catch (Throwable t) { + ourLog.error("Failure", t); + } + } + } + + /** + * This method is executed when the client is connecting to the server. + * In this case, we are sending a message to create the subscription dynamiclly + * + * @param session + */ + @OnWebSocketConnect + public void onConnect(Session session) { + ourLog.info("Got connect: {}", session); + this.session = session; + try { + String sending = "bind " + myCriteria; + ourLog.info("Sending: {}", sending); + session.getRemote().sendString(sending); + + ourLog.info("Connection: DONE"); + } catch (Throwable t) { + t.printStackTrace(); + ourLog.error("Failure", t); + } + } + + /** + * This is the message handler for the client + * + * @param theMsg + */ + @OnWebSocketMessage + public void onMessage(String theMsg) { + ourLog.info("Got msg: " + theMsg); + myMessages.add(theMsg); + + if (theMsg.startsWith("bound ")) { + myGotBound = true; + mySubsId = (theMsg.substring("bound ".length())); + } else if (myGotBound && theMsg.startsWith("add " + mySubsId + "\n")) { + String text = theMsg.substring(("add " + mySubsId + "\n").length()); + ourLog.info("text: " + text); + } else { + myError = "Unexpected message: " + theMsg; + } + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/TminusTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/TminusTest.java similarity index 99% rename from hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/TminusTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/TminusTest.java index 0650cd6a7da..318c8940f56 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/TminusTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/TminusTest.java @@ -15,7 +15,7 @@ * * @author Jeff Chung */ -package ca.uhn.fhir.jpa.demo.subscription; +package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.jpa.service.TMinusService; import org.hl7.fhir.dstu3.model.DateTimeType; diff --git a/hapi-fhir-jpaserver-example/pom.xml b/hapi-fhir-jpaserver-example/pom.xml index 29ad78582b8..3ff8d4578e2 100644 --- a/hapi-fhir-jpaserver-example/pom.xml +++ b/hapi-fhir-jpaserver-example/pom.xml @@ -206,6 +206,15 @@ + + + javax.interceptor + javax.interceptor-api + provided + + diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index 5db5a32247b..90f791c0919 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -11,23 +11,23 @@ import org.hibernate.jpa.HibernatePersistenceProvider; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; -import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; -import ca.uhn.fhir.jpa.config.WebsocketDstu2Config; +import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2; +import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; +/** + * This is the primary configuration file for the example server + */ @Configuration @EnableTransactionManagement() -//@Import(WebsocketDstu2Config.class) -public class FhirServerConfig extends BaseJavaConfigDstu2 { +public class FhirServerConfig extends BaseJavaConfigDstu3 { /** * Configure FHIR properties around the the JPA server via this bean @@ -83,6 +83,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); +// extraProperties.put("hibernate.search.default.worker.execution", "async"); return extraProperties; } @@ -110,7 +111,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { @Bean(autowire = Autowire.BY_TYPE) public IServerInterceptor subscriptionSecurityInterceptor() { - SubscriptionsRequireManualActivationInterceptorDstu2 retVal = new SubscriptionsRequireManualActivationInterceptorDstu2(); + SubscriptionsRequireManualActivationInterceptorDstu3 retVal = new SubscriptionsRequireManualActivationInterceptorDstu3(); return retVal; } diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java deleted file mode 100644 index cc3fb3679b1..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java +++ /dev/null @@ -1,129 +0,0 @@ -package ca.uhn.fhir.jpa.demo; - -import java.util.Properties; - -import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; - -import org.apache.commons.dbcp2.BasicDataSource; -import org.apache.commons.lang3.time.DateUtils; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Autowire; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; - -/** - * This class isn't used by default by the example, but - * you can use it as a config if you want to support DSTU3 - * instead of DSTU2 in your server. - * - * See https://github.com/jamesagnew/hapi-fhir/issues/278 - */ -@Configuration -@EnableTransactionManagement() -public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 { - - /** - * Configure FHIR properties around the the JPA server via this bean - */ - @Bean() - public DaoConfig daoConfig() { - DaoConfig retVal = new DaoConfig(); - retVal.setSubscriptionEnabled(true); - retVal.setSubscriptionPollDelay(5000); - retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); - retVal.setAllowMultipleDelete(true); - return retVal; - } - - /** - * The following bean configures the database connection. The 'url' property value of "jdbc:derby:directory:jpaserver_derby_files;create=true" indicates that the server should save resources in a - * directory called "jpaserver_derby_files". - * - * A URL to a remote database could also be placed here, along with login credentials and other properties supported by BasicDataSource. - */ - @Bean(destroyMethod = "close") - public DataSource dataSource() { - BasicDataSource retVal = new BasicDataSource(); - retVal.setDriver(new org.apache.derby.jdbc.EmbeddedDriver()); - retVal.setUrl("jdbc:derby:directory:target/jpaserver_derby_files;create=true"); - retVal.setUsername(""); - retVal.setPassword(""); - return retVal; - } - - @Bean() - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); - retVal.setPersistenceUnitName("HAPI_PU"); - retVal.setDataSource(dataSource()); - retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); - retVal.setJpaProperties(jpaProperties()); - return retVal; - } - - private Properties jpaProperties() { - Properties extraProperties = new Properties(); - extraProperties.put("hibernate.dialect", org.hibernate.dialect.DerbyTenSevenDialect.class.getName()); - extraProperties.put("hibernate.format_sql", "true"); - extraProperties.put("hibernate.show_sql", "false"); - extraProperties.put("hibernate.hbm2ddl.auto", "update"); - extraProperties.put("hibernate.jdbc.batch_size", "20"); - extraProperties.put("hibernate.cache.use_query_cache", "false"); - extraProperties.put("hibernate.cache.use_second_level_cache", "false"); - extraProperties.put("hibernate.cache.use_structured_entries", "false"); - extraProperties.put("hibernate.cache.use_minimal_puts", "false"); - extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); - extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); - extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); -// extraProperties.put("hibernate.search.default.worker.execution", "async"); - return extraProperties; - } - - /** - * Do some fancy logging to create a nice access log that has details about each incoming request. - */ - public IServerInterceptor loggingInterceptor() { - LoggingInterceptor retVal = new LoggingInterceptor(); - retVal.setLoggerName("fhirtest.access"); - retVal.setMessageFormat( - "Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}]"); - retVal.setLogExceptions(true); - retVal.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}"); - return retVal; - } - - /** - * This interceptor adds some pretty syntax highlighting in responses when a browser is detected - */ - @Bean(autowire = Autowire.BY_TYPE) - public IServerInterceptor responseHighlighterInterceptor() { - ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); - return retVal; - } - - @Bean(autowire = Autowire.BY_TYPE) - public IServerInterceptor subscriptionSecurityInterceptor() { - SubscriptionsRequireManualActivationInterceptorDstu3 retVal = new SubscriptionsRequireManualActivationInterceptorDstu3(); - return retVal; - } - - @Bean() - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager retVal = new JpaTransactionManager(); - retVal.setEntityManagerFactory(entityManagerFactory); - return retVal; - } - -} diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3WSocket.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3WSocket.java deleted file mode 100644 index f42b73b3834..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3WSocket.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ - -package ca.uhn.fhir.jpa.demo; - -import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu3Interceptor; -import ca.uhn.fhir.jpa.interceptor.WebSocketSubscriptionDstu3Interceptor; -import ca.uhn.fhir.jpa.subscription.SubscriptionWebsocketReturnResourceHandlerDstu3; -import ca.uhn.fhir.jpa.util.SpringObjectCaster; -import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; -import org.apache.commons.dbcp2.BasicDataSource; -import org.apache.commons.lang3.time.DateUtils; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Autowire; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Lazy; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import org.springframework.web.socket.handler.PerConnectionWebSocketHandler; - -import javax.persistence.EntityManagerFactory; -import javax.persistence.PersistenceException; -import javax.sql.DataSource; -import java.util.List; -import java.util.Properties; - -/** - * This class isn't used by default by the example, but - * you can use it as a config if you want to support DSTU3 - * instead of DSTU2 in your server as well as rest-hook subscriptions, - * event driven web-socket subscriptions, and a mysql database. - * - * See https://github.com/jamesagnew/hapi-fhir/issues/278 - */ -@Configuration -@EnableWebSocket() -@EnableTransactionManagement() -public class FhirServerConfigDstu3WSocket extends BaseJavaConfigDstu3 implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) { - theRegistry.addHandler(subscriptionWebSocketHandler(), "/websocket/dstu3"); - } - - @Bean(autowire = Autowire.BY_TYPE) - public WebSocketHandler subscriptionWebSocketHandler() { - PerConnectionWebSocketHandler retVal = new PerConnectionWebSocketHandler(SubscriptionWebsocketReturnResourceHandlerDstu3.class); - return retVal; - } - - @Bean(destroyMethod="destroy") - public TaskScheduler websocketTaskScheduler() { - final ThreadPoolTaskScheduler retVal = new ThreadPoolTaskScheduler() { - private static final long serialVersionUID = 1L; - - @Override - public void afterPropertiesSet() { - super.afterPropertiesSet(); - getScheduledThreadPoolExecutor().setExecuteExistingDelayedTasksAfterShutdownPolicy(false); -// getScheduledThreadPoolExecutor().setRemoveOnCancelPolicy(true); - getScheduledThreadPoolExecutor().setContinueExistingPeriodicTasksAfterShutdownPolicy(false); - } - }; - retVal.setThreadNamePrefix("ws-dstu3-"); - retVal.setPoolSize(5); - - return retVal; - } - - @Bean - @Lazy - public IServerInterceptor webSocketSubscriptionDstu3Interceptor(){ - return new WebSocketSubscriptionDstu3Interceptor(); - } - - @Bean - @Lazy - public IServerInterceptor restHookSubscriptionDstu3Interceptor(){ - return new RestHookSubscriptionDstu3Interceptor(); - } - - /** - * Configure FHIR properties around the the JPA server via this bean - */ - @Bean() - public DaoConfig daoConfig() { - DaoConfig retVal = new DaoConfig(); - retVal.setSubscriptionEnabled(true); - retVal.setSubscriptionPollDelay(-1000); - retVal.setSchedulingDisabled(true); - retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); - retVal.setAllowMultipleDelete(true); - retVal.setAllowExternalReferences(true); - return retVal; - } - - /** - * Loads the rest-hook and websocket interceptors after the DaoConfig bean has been - * initialized to avoid cyclical dependency errors - * @param daoConfig - * @return - */ - @Bean(name = "subscriptionInterceptors") - @DependsOn("daoConfig") - public List afterDaoConfig(DaoConfig daoConfig){ - IServerInterceptor webSocketInterceptor = webSocketSubscriptionDstu3Interceptor(); - IServerInterceptor restHookInterceptor = restHookSubscriptionDstu3Interceptor(); - - try { - RestHookSubscriptionDstu3Interceptor restHook = SpringObjectCaster.getTargetObject(restHookInterceptor, RestHookSubscriptionDstu3Interceptor.class); - restHook.setNotifyOnDelete(true); - restHook.initSubscriptions(); - }catch(PersistenceException e){ - throw new RuntimeException("Persistence error in setting up resthook subscriptions:" + e.getMessage()); - }catch(Exception e){ - throw new RuntimeException("Unable to cast from proxy"); - } - - daoConfig.getInterceptors().add(restHookInterceptor); - daoConfig.getInterceptors().add(webSocketInterceptor); - - return daoConfig.getInterceptors(); - } - - /** - * The following bean configures the database connection. The 'url' property value of "jdbc:derby:directory:jpaserver_derby_files;create=true" indicates that the server should save resources in a - * directory called "jpaserver_derby_files". - * - * A URL to a remote database could also be placed here, along with login credentials and other properties supported by BasicDataSource. - */ - @Bean(destroyMethod = "close") - public DataSource dataSource() { - BasicDataSource retVal = new BasicDataSource(); - retVal.setDriver(new org.apache.derby.jdbc.EmbeddedDriver()); - retVal.setUrl("jdbc:derby:directory:target/jpaserver_derby_files;create=true"); - retVal.setUsername(""); - retVal.setPassword(""); - return retVal; - } - - @Bean() - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); - retVal.setPersistenceUnitName("HAPI_PU"); - retVal.setDataSource(dataSource()); - retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); - retVal.setJpaProperties(jpaProperties()); - return retVal; - } - - private Properties jpaProperties() { - Properties extraProperties = new Properties(); - extraProperties.put("hibernate.dialect", org.hibernate.dialect.DerbyTenSevenDialect.class.getName()); - extraProperties.put("hibernate.format_sql", "true"); - extraProperties.put("hibernate.show_sql", "false"); - extraProperties.put("hibernate.hbm2ddl.auto", "update"); - extraProperties.put("hibernate.jdbc.batch_size", "20"); - extraProperties.put("hibernate.cache.use_query_cache", "false"); - extraProperties.put("hibernate.cache.use_second_level_cache", "false"); - extraProperties.put("hibernate.cache.use_structured_entries", "false"); - extraProperties.put("hibernate.cache.use_minimal_puts", "false"); - extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); - extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); - extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); - return extraProperties; - } - - /** - * Do some fancy logging to create a nice access log that has details about each incoming request. - */ - public IServerInterceptor loggingInterceptor() { - LoggingInterceptor retVal = new LoggingInterceptor(); - retVal.setLoggerName("fhirtest.access"); - retVal.setMessageFormat( - "Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}]"); - retVal.setLogExceptions(true); - retVal.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}"); - return retVal; - } - - /** - * This interceptor adds some pretty syntax highlighting in responses when a browser is detected - */ - @Bean(autowire = Autowire.BY_TYPE) - public IServerInterceptor responseHighlighterInterceptor() { - ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); - return retVal; - } - - @Bean(autowire = Autowire.BY_TYPE) - public IServerInterceptor subscriptionSecurityInterceptor() { - SubscriptionsRequireManualActivationInterceptorDstu3 retVal = new SubscriptionsRequireManualActivationInterceptorDstu3(); - return retVal; - } - - @Bean() - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager retVal = new JpaTransactionManager(); - retVal.setEntityManagerFactory(entityManagerFactory); - return retVal; - } - -} diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigWSocket.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigWSocket.java deleted file mode 100644 index 0a82d56d092..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigWSocket.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ - -package ca.uhn.fhir.jpa.demo; - -import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.interceptor.RestHookSubscriptionDstu2Interceptor; -import ca.uhn.fhir.jpa.interceptor.WebSocketSubscriptionDstu2Interceptor; -import ca.uhn.fhir.jpa.subscription.SubscriptionWebsocketReturnResourceHandlerDstu2; -import ca.uhn.fhir.jpa.util.SpringObjectCaster; -import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; -import org.apache.commons.dbcp2.BasicDataSource; -import org.apache.commons.lang3.time.DateUtils; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Autowire; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Lazy; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import org.springframework.web.socket.handler.PerConnectionWebSocketHandler; - -import javax.persistence.EntityManagerFactory; -import javax.persistence.PersistenceException; -import javax.sql.DataSource; -import java.util.List; -import java.util.Properties; - -/** - * This class isn't used by default by the example, but - * you can use it as a config if you want to support DSTU2 rest-hook subscriptions, - * event driven web-socket subscriptions, and a mysql database. - */ -@Configuration -@EnableWebSocket() -@EnableTransactionManagement() -public class FhirServerConfigWSocket extends BaseJavaConfigDstu2 implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) { - theRegistry.addHandler(subscriptionWebSocketHandler(), "/websocket/dstu2"); - } - - @Bean(autowire = Autowire.BY_TYPE) - public WebSocketHandler subscriptionWebSocketHandler() { - //return new PerConnectionWebSocketHandler(SubscriptionWebsocketHandlerDstu2.class); - return new PerConnectionWebSocketHandler(SubscriptionWebsocketReturnResourceHandlerDstu2.class); - } - - @Bean - public TaskScheduler websocketTaskScheduler() { - ThreadPoolTaskScheduler retVal = new ThreadPoolTaskScheduler(); - retVal.setPoolSize(5); - return retVal; - } - - /** - * Configure FHIR properties around the the JPA server via this bean - */ - @Bean() - public DaoConfig daoConfig() { - DaoConfig retVal = new DaoConfig(); - retVal.setSubscriptionEnabled(true); - retVal.setSubscriptionPollDelay(-1000); - retVal.setSchedulingDisabled(true); - retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); - retVal.setAllowMultipleDelete(true); - retVal.setAllowExternalReferences(true); - return retVal; - } - - /** - * Loads the rest-hook and websocket interceptors after the DaoConfig bean has been - * initialized to avoid cyclical dependency errors - * @param daoConfig - * @return - */ - @Bean(name = "subscriptionInterceptors") - @DependsOn("daoConfig") - public List afterDaoConfig(DaoConfig daoConfig){ - IServerInterceptor webSocketInterceptor = webSocketSubscriptionDstu2Interceptor(); - IServerInterceptor restHookInterceptor = restHookSubscriptionDstu2Interceptor(); - - try { - RestHookSubscriptionDstu2Interceptor restHook = SpringObjectCaster.getTargetObject(restHookInterceptor, RestHookSubscriptionDstu2Interceptor.class); - restHook.setNotifyOnDelete(true); - restHook.initSubscriptions(); - }catch(PersistenceException e){ - throw new RuntimeException("Persistence error in setting up resthook subscriptions:" + e.getMessage()); - }catch(Exception e){ - throw new RuntimeException("Unable to cast from proxy"); - } - - daoConfig.getInterceptors().add(restHookInterceptor); - daoConfig.getInterceptors().add(webSocketInterceptor); - return daoConfig.getInterceptors(); - } - - /** - * The following bean configures the database connection. The 'url' property value of "jdbc:derby:directory:jpaserver_derby_files;create=true" indicates that the server should save resources in a - * directory called "jpaserver_derby_files". - * - * A URL to a remote database could also be placed here, along with login credentials and other properties supported by BasicDataSource. - */ - @Bean(destroyMethod = "close") - public DataSource dataSource() { - BasicDataSource retVal = new BasicDataSource(); - retVal.setDriver(new org.apache.derby.jdbc.EmbeddedDriver()); - retVal.setUrl("jdbc:derby:directory:target/jpaserver_derby_files;create=true"); - retVal.setUsername(""); - retVal.setPassword(""); - return retVal; - } - - @Bean() - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); - retVal.setPersistenceUnitName("HAPI_PU"); - retVal.setDataSource(dataSource()); - retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); - retVal.setJpaProperties(jpaProperties()); - return retVal; - } - - private Properties jpaProperties() { - Properties extraProperties = new Properties(); - extraProperties.put("hibernate.dialect", org.hibernate.dialect.DerbyTenSevenDialect.class.getName()); - extraProperties.put("hibernate.format_sql", "true"); - extraProperties.put("hibernate.show_sql", "false"); - extraProperties.put("hibernate.hbm2ddl.auto", "update"); - extraProperties.put("hibernate.jdbc.batch_size", "20"); - extraProperties.put("hibernate.cache.use_query_cache", "false"); - extraProperties.put("hibernate.cache.use_second_level_cache", "false"); - extraProperties.put("hibernate.cache.use_structured_entries", "false"); - extraProperties.put("hibernate.cache.use_minimal_puts", "false"); - extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); - extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); - extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); - return extraProperties; - - } - - /** - * Do some fancy logging to create a nice access log that has details about each incoming request. - */ - public IServerInterceptor loggingInterceptor() { - LoggingInterceptor retVal = new LoggingInterceptor(); - retVal.setLoggerName("fhirtest.access"); - retVal.setMessageFormat( - "Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}]"); - retVal.setLogExceptions(true); - retVal.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}"); - return retVal; - } - - /** - * This interceptor adds some pretty syntax highlighting in responses when a browser is detected - */ - @Bean(autowire = Autowire.BY_TYPE) - public IServerInterceptor responseHighlighterInterceptor() { - ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); - return retVal; - } - - @Bean(autowire = Autowire.BY_TYPE) - public IServerInterceptor subscriptionSecurityInterceptor() { - SubscriptionsRequireManualActivationInterceptorDstu2 retVal = new SubscriptionsRequireManualActivationInterceptorDstu2(); - return retVal; - } - - @Bean() - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager retVal = new JpaTransactionManager(); - retVal.setEntityManagerFactory(entityManagerFactory); - return retVal; - } - - @Bean - @Lazy - public IServerInterceptor webSocketSubscriptionDstu2Interceptor(){ - return new WebSocketSubscriptionDstu2Interceptor(); - } - - @Bean - @Lazy - public IServerInterceptor restHookSubscriptionDstu2Interceptor(){ - return new RestHookSubscriptionDstu2Interceptor(); - } -} diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java index 947c46f9fa4..3d260917aac 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java @@ -24,15 +24,15 @@ public class FhirTesterConfig { /** * This bean tells the testing webpage which servers it should configure itself * to communicate with. In this example we configure it to talk to the local - * server, as well as one public server. If you are creating a project to - * deploy somewhere else, you might choose to only put your own server's + * server, as well as one public server. If you are creating a project to + * deploy somewhere else, you might choose to only put your own server's * address here. - * + * * Note the use of the ${serverBase} variable below. This will be replaced with * the base URL as reported by the server itself. Often for a simple Tomcat * (or other container) installation, this will end up being something * like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are - * deploying your server to a place with a fully qualified domain name, + * deploying your server to a place with a fully qualified domain name, * you might want to use that instead of using the variable. */ @Bean @@ -41,16 +41,16 @@ public class FhirTesterConfig { retVal .addServer() .withId("home") - .withFhirVersion(FhirVersionEnum.DSTU2) - .withBaseUrl("${serverBase}/baseDstu2") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("${serverBase}/baseDstu3") .withName("Local Tester") .addServer() .withId("hapi") - .withFhirVersion(FhirVersionEnum.DSTU2) - .withBaseUrl("http://fhirtest.uhn.ca/baseDstu2") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("http://fhirtest.uhn.ca/baseDstu3") .withName("Public HAPI Test Server"); return retVal; } - + } //@formatter:on diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu3.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu3.java deleted file mode 100644 index 714b8f10cd6..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu3.java +++ /dev/null @@ -1,56 +0,0 @@ -package ca.uhn.fhir.jpa.demo; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.to.FhirTesterMvcConfig; -import ca.uhn.fhir.to.TesterConfig; - -//@formatter:off -/** - * This spring config file configures the web testing module. It serves two - * purposes: - * 1. It imports FhirTesterMvcConfig, which is the spring config for the - * tester itself - * 2. It tells the tester which server(s) to talk to, via the testerConfig() - * method below - */ -@Configuration -@Import(FhirTesterMvcConfig.class) -public class FhirTesterConfigDstu3 { - - /** - * This bean tells the testing webpage which servers it should configure itself - * to communicate with. In this example we configure it to talk to the local - * server, as well as one public server. If you are creating a project to - * deploy somewhere else, you might choose to only put your own server's - * address here. - * - * Note the use of the ${serverBase} variable below. This will be replaced with - * the base URL as reported by the server itself. Often for a simple Tomcat - * (or other container) installation, this will end up being something - * like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are - * deploying your server to a place with a fully qualified domain name, - * you might want to use that instead of using the variable. - */ - @Bean - public TesterConfig testerConfig() { - TesterConfig retVal = new TesterConfig(); - retVal - .addServer() - .withId("home") - .withFhirVersion(FhirVersionEnum.DSTU3) - .withBaseUrl("${serverBase}/baseDstu3") - .withName("Local Tester") - .addServer() - .withId("hapi") - .withFhirVersion(FhirVersionEnum.DSTU3) - .withBaseUrl("http://fhirtest.uhn.ca/baseDstu3") - .withName("Public HAPI Test Server"); - return retVal; - } - -} -//@formatter:on diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index cf1b8071e1a..430c9546914 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -1,16 +1,21 @@ +/* + * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). + * + * 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. + */ + package ca.uhn.fhir.jpa.demo; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import javax.servlet.ServletException; - -import org.hl7.fhir.dstu3.model.Meta; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.cors.CorsConfiguration; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.dao.DaoConfig; @@ -21,19 +26,23 @@ import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu1; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; -import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu2.composite.MetaDt; -import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; -import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Meta; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; + +import javax.servlet.ServletException; +import java.util.Collection; +import java.util.List; public class JpaServerDemo extends RestfulServer { @@ -46,12 +55,11 @@ public class JpaServerDemo extends RestfulServer { protected void initialize() throws ServletException { super.initialize(); - /* + /* * We want to support FHIR DSTU2 format. This means that the server * will use the DSTU2 bundle format and other DSTU2 encoding changes. * - * If you want to use DSTU1 instead, change the following line, and - * change the 2 occurrences of dstu2 in web.xml to dstu1 + * If you want to use DSTU1 instead, change the following line, and change the 2 occurrences of dstu2 in web.xml to dstu1 */ FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU3; setFhirContext(new FhirContext(fhirVersion)); @@ -59,7 +67,7 @@ public class JpaServerDemo extends RestfulServer { // Get the spring context from the web container (it's declared in web.xml) myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); - /* + /* * The BaseJavaConfigDstu2.java class is a spring configuration * file which is automatically generated as a part of hapi-fhir-jpaserver-base and * contains bean definitions for a resource provider for each resource type @@ -76,8 +84,8 @@ public class JpaServerDemo extends RestfulServer { } List beans = myAppCtx.getBean(resourceProviderBeanName, List.class); setResourceProviders(beans); - - /* + + /* * The system provider implements non-resource-type methods, such as * transaction, and global history. */ @@ -104,13 +112,13 @@ public class JpaServerDemo extends RestfulServer { confProvider.setImplementationDescription("Example Server"); setServerConformanceProvider(confProvider); } else if (fhirVersion == FhirVersionEnum.DSTU2) { - IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class); + IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class); JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, myAppCtx.getBean(DaoConfig.class)); confProvider.setImplementationDescription("Example Server"); setServerConformanceProvider(confProvider); } else if (fhirVersion == FhirVersionEnum.DSTU3) { - IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); + IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, myAppCtx.getBean(DaoConfig.class)); confProvider.setImplementationDescription("Example Server"); @@ -145,24 +153,6 @@ public class JpaServerDemo extends RestfulServer { */ setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); - /* - * Enable CORS - */ - CorsConfiguration config = new CorsConfiguration(); - CorsInterceptor corsInterceptor = new CorsInterceptor(config); - config.addAllowedHeader("Origin"); - config.addAllowedHeader("Accept"); - config.addAllowedHeader("Prefer"); - config.addAllowedHeader("X-Requested-With"); - config.addAllowedHeader("Content-Type"); - config.addAllowedHeader("Access-Control-Request-Method"); - config.addAllowedHeader("Access-Control-Request-Headers"); - config.addAllowedOrigin("*"); - config.addExposedHeader("Location"); - config.addExposedHeader("Content-Location"); - config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); - registerInterceptor(corsInterceptor); - /* * Load interceptors for the server from Spring (these are defined in FhirServerConfig.java) */ @@ -172,15 +162,15 @@ public class JpaServerDemo extends RestfulServer { } /* - * If you are hosting this server at a specific DNS name, the server will try to + * If you are hosting this server at a specific DNS name, the server will try to * figure out the FHIR base URL based on what the web container tells it, but * this doesn't always work. If you are setting links in your search bundles that * just refer to "localhost", you might want to use a server address strategy: */ //setServerAddressStrategy(new HardcodedServerAddressStrategy("http://mydomain.com/fhir/baseDstu2")); - + /* - * If you are using DSTU3+, you may want to add a terminology uploader, which allows + * If you are using DSTU3+, you may want to add a terminology uploader, which allows * uploading of external terminologies such as Snomed CT. Note that this uploader * does not have any security attached (any anonymous user may use it by default) * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java deleted file mode 100644 index 92cc71dfd04..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java +++ /dev/null @@ -1,190 +0,0 @@ -package ca.uhn.fhir.jpa.demo; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu1; -import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; -import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu1; -import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; -import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; -import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; -import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.dstu2.composite.MetaDt; -import ca.uhn.fhir.model.dstu2.resource.Bundle; -import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; -import ca.uhn.fhir.rest.server.ETagSupportEnum; -import ca.uhn.fhir.rest.server.EncodingEnum; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import org.hl7.fhir.dstu3.model.Meta; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.cors.CorsConfiguration; - -import javax.servlet.ServletException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -public class JpaServerDemoDstu2 extends RestfulServer { - - private static final long serialVersionUID = 1L; - - private WebApplicationContext myAppCtx; - - @SuppressWarnings("unchecked") - @Override - protected void initialize() throws ServletException { - super.initialize(); - - /* - * We want to support FHIR DSTU2 format. This means that the server - * will use the DSTU2 bundle format and other DSTU2 encoding changes. - * - * If you want to use DSTU1 instead, change the following line, and - * change the 2 occurrences of dstu2 in web.xml to dstu1 - */ - FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU2; - setFhirContext(new FhirContext(fhirVersion)); - - // Get the spring context from the web container (it's declared in web.xml) - myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); - - /* - * The BaseJavaConfigDstu2.java class is a spring configuration - * file which is automatically generated as a part of hapi-fhir-jpaserver-base and - * contains bean definitions for a resource provider for each resource type - */ - String resourceProviderBeanName; - if (fhirVersion == FhirVersionEnum.DSTU1) { - resourceProviderBeanName = "myResourceProvidersDstu1"; - } else if (fhirVersion == FhirVersionEnum.DSTU2) { - resourceProviderBeanName = "myResourceProvidersDstu2"; - } else if (fhirVersion == FhirVersionEnum.DSTU3) { - resourceProviderBeanName = "myResourceProvidersDstu3"; - } else { - throw new IllegalStateException(); - } - List beans = myAppCtx.getBean(resourceProviderBeanName, List.class); - setResourceProviders(beans); - - /* - * The system provider implements non-resource-type methods, such as - * transaction, and global history. - */ - Object systemProvider; - if (fhirVersion == FhirVersionEnum.DSTU1) { - systemProvider = myAppCtx.getBean("mySystemProviderDstu1", JpaSystemProviderDstu1.class); - } else if (fhirVersion == FhirVersionEnum.DSTU2) { - systemProvider = myAppCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class); - } else if (fhirVersion == FhirVersionEnum.DSTU3) { - systemProvider = myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class); - } else { - throw new IllegalStateException(); - } - setPlainProviders(systemProvider); - - /* - * The conformance provider exports the supported resources, search parameters, etc for - * this server. The JPA version adds resource counts to the exported statement, so it - * is a nice addition. - */ - if (fhirVersion == FhirVersionEnum.DSTU1) { - IFhirSystemDao, MetaDt> systemDao = myAppCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class); - JpaConformanceProviderDstu1 confProvider = new JpaConformanceProviderDstu1(this, systemDao); - confProvider.setImplementationDescription("Example Server"); - setServerConformanceProvider(confProvider); - } else if (fhirVersion == FhirVersionEnum.DSTU2) { - IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class); - JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, - myAppCtx.getBean(DaoConfig.class)); - confProvider.setImplementationDescription("Example Server"); - setServerConformanceProvider(confProvider); - } else if (fhirVersion == FhirVersionEnum.DSTU3) { - IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); - JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, - myAppCtx.getBean(DaoConfig.class)); - confProvider.setImplementationDescription("Example Server"); - setServerConformanceProvider(confProvider); - } else { - throw new IllegalStateException(); - } - - /* - * Enable ETag Support (this is already the default) - */ - setETagSupport(ETagSupportEnum.ENABLED); - - /* - * This server tries to dynamically generate narratives - */ - FhirContext ctx = getFhirContext(); - ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); - - /* - * Default to JSON and pretty printing - */ - setDefaultPrettyPrint(true); - setDefaultResponseEncoding(EncodingEnum.JSON); - - /* - * -- New in HAPI FHIR 1.5 -- - * This configures the server to page search results to and from - * the database, instead of only paging them to memory. This may mean - * a performance hit when performing searches that return lots of results, - * but makes the server much more scalable. - */ - setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); - - /* - * Enable CORS - */ - CorsConfiguration config = new CorsConfiguration(); - CorsInterceptor corsInterceptor = new CorsInterceptor(config); - config.addAllowedHeader("Origin"); - config.addAllowedHeader("Accept"); - config.addAllowedHeader("Prefer"); - config.addAllowedHeader("X-Requested-With"); - config.addAllowedHeader("Content-Type"); - config.addAllowedHeader("Access-Control-Request-Method"); - config.addAllowedHeader("Access-Control-Request-Headers"); - config.addAllowedOrigin("*"); - config.addExposedHeader("Location"); - config.addExposedHeader("Content-Location"); - config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); - registerInterceptor(corsInterceptor); - - /* - * Load interceptors for the server from Spring (these are defined in FhirServerConfig.java) - */ - Collection interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values(); - for (IServerInterceptor interceptor : interceptorBeans) { - this.registerInterceptor(interceptor); - } - - /* - * If you are hosting this server at a specific DNS name, the server will try to - * figure out the FHIR base URL based on what the web container tells it, but - * this doesn't always work. If you are setting links in your search bundles that - * just refer to "localhost", you might want to use a server address strategy: - */ - //setServerAddressStrategy(new HardcodedServerAddressStrategy("http://mydomain.com/fhir/baseDstu2")); - - /* - * If you are using DSTU3+, you may want to add a terminology uploader, which allows - * uploading of external terminologies such as Snomed CT. Note that this uploader - * does not have any security attached (any anonymous user may use it by default) - * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor - * with this feature. - */ - //if (fhirVersion == FhirVersionEnum.DSTU3) { - // registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class)); - //} - } - -} diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu3.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu3.java deleted file mode 100644 index 3ae5f4395da..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu3.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - */ - -package ca.uhn.fhir.jpa.demo; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu1; -import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; -import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu1; -import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; -import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; -import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; -import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.dstu2.composite.MetaDt; -import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; -import ca.uhn.fhir.rest.server.ETagSupportEnum; -import ca.uhn.fhir.rest.server.EncodingEnum; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import org.hl7.fhir.dstu3.model.Bundle; -import org.hl7.fhir.dstu3.model.Meta; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.WebApplicationContext; - -import javax.servlet.ServletException; -import java.util.Collection; -import java.util.List; - -public class JpaServerDemoDstu3 extends RestfulServer { - - private static final long serialVersionUID = 1L; - - private WebApplicationContext myAppCtx; - - @SuppressWarnings("unchecked") - @Override - protected void initialize() throws ServletException { - super.initialize(); - - /* - * We want to support FHIR DSTU2 format. This means that the server - * will use the DSTU2 bundle format and other DSTU2 encoding changes. - * - * If you want to use DSTU1 instead, change the following line, and change the 2 occurrences of dstu2 in web.xml to dstu1 - */ - FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU3; - setFhirContext(new FhirContext(fhirVersion)); - - // Get the spring context from the web container (it's declared in web.xml) - myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); - - /* - * The BaseJavaConfigDstu2.java class is a spring configuration - * file which is automatically generated as a part of hapi-fhir-jpaserver-base and - * contains bean definitions for a resource provider for each resource type - */ - String resourceProviderBeanName; - if (fhirVersion == FhirVersionEnum.DSTU1) { - resourceProviderBeanName = "myResourceProvidersDstu1"; - } else if (fhirVersion == FhirVersionEnum.DSTU2) { - resourceProviderBeanName = "myResourceProvidersDstu2"; - } else if (fhirVersion == FhirVersionEnum.DSTU3) { - resourceProviderBeanName = "myResourceProvidersDstu3"; - } else { - throw new IllegalStateException(); - } - List beans = myAppCtx.getBean(resourceProviderBeanName, List.class); - setResourceProviders(beans); - - /* - * The system provider implements non-resource-type methods, such as - * transaction, and global history. - */ - Object systemProvider; - if (fhirVersion == FhirVersionEnum.DSTU1) { - systemProvider = myAppCtx.getBean("mySystemProviderDstu1", JpaSystemProviderDstu1.class); - } else if (fhirVersion == FhirVersionEnum.DSTU2) { - systemProvider = myAppCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class); - } else if (fhirVersion == FhirVersionEnum.DSTU3) { - systemProvider = myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class); - } else { - throw new IllegalStateException(); - } - setPlainProviders(systemProvider); - - /* - * The conformance provider exports the supported resources, search parameters, etc for - * this server. The JPA version adds resource counts to the exported statement, so it - * is a nice addition. - */ - if (fhirVersion == FhirVersionEnum.DSTU1) { - IFhirSystemDao, MetaDt> systemDao = myAppCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class); - JpaConformanceProviderDstu1 confProvider = new JpaConformanceProviderDstu1(this, systemDao); - confProvider.setImplementationDescription("Example Server"); - setServerConformanceProvider(confProvider); - } else if (fhirVersion == FhirVersionEnum.DSTU2) { - IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class); - JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, - myAppCtx.getBean(DaoConfig.class)); - confProvider.setImplementationDescription("Example Server"); - setServerConformanceProvider(confProvider); - } else if (fhirVersion == FhirVersionEnum.DSTU3) { - IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); - JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, - myAppCtx.getBean(DaoConfig.class)); - confProvider.setImplementationDescription("Example Server"); - setServerConformanceProvider(confProvider); - } else { - throw new IllegalStateException(); - } - - /* - * Enable ETag Support (this is already the default) - */ - setETagSupport(ETagSupportEnum.ENABLED); - - /* - * This server tries to dynamically generate narratives - */ - FhirContext ctx = getFhirContext(); - ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); - - /* - * Default to JSON and pretty printing - */ - setDefaultPrettyPrint(true); - setDefaultResponseEncoding(EncodingEnum.JSON); - - /* - * -- New in HAPI FHIR 1.5 -- - * This configures the server to page search results to and from - * the database, instead of only paging them to memory. This may mean - * a performance hit when performing searches that return lots of results, - * but makes the server much more scalable. - */ - setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); - - /* - * Load interceptors for the server from Spring (these are defined in FhirServerConfig.java) - */ - Collection interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values(); - for (IServerInterceptor interceptor : interceptorBeans) { - this.registerInterceptor(interceptor); - } - - /* - * If you are hosting this server at a specific DNS name, the server will try to - * figure out the FHIR base URL based on what the web container tells it, but - * this doesn't always work. If you are setting links in your search bundles that - * just refer to "localhost", you might want to use a server address strategy: - */ - //setServerAddressStrategy(new HardcodedServerAddressStrategy("http://mydomain.com/fhir/baseDstu2")); - - /* - * If you are using DSTU3+, you may want to add a terminology uploader, which allows - * uploading of external terminologies such as Snomed CT. Note that this uploader - * does not have any security attached (any anonymous user may use it by default) - * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor - * with this feature. - */ - //if (fhirVersion == FhirVersionEnum.DSTU3) { - // registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class)); - //} - } - -} diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml index 8de1fe1e02a..e74f0193383 100644 --- a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml @@ -28,14 +28,14 @@ contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirTesterConfigDstu3 + ca.uhn.fhir.jpa.demo.FhirTesterConfig 2 fhirServlet - ca.uhn.fhir.jpa.demo.JpaServerDemoDstu3 + ca.uhn.fhir.jpa.demo.JpaServerDemo ImplementationDescription FHIR JPA Server diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web.xml index 1bde5e34fcf..39d8f8970a3 100644 --- a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web.xml @@ -13,7 +13,7 @@ contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3WSocket + ca.uhn.fhir.jpa.demo.FhirServerConfig @@ -28,14 +28,14 @@ contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirTesterConfigDstu3 + ca.uhn.fhir.jpa.demo.FhirTesterConfig 2 fhirServlet - ca.uhn.fhir.jpa.demo.JpaServerDemoDstu3 + ca.uhn.fhir.jpa.demo.JpaServerDemo ImplementationDescription FHIR JPA Server diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu2IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu2IT.java deleted file mode 100644 index 13075ed4228..00000000000 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu2IT.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ -package ca.uhn.fhir.jpa.demo.subscription; - -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.valueset.ObservationStatusEnum; -import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum; -import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.IGenericClient; -import ca.uhn.fhir.rest.server.EncodingEnum; -import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; - -import java.net.URI; - -/** - * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the - * subscription - *

- * Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3IT for - * a test that returns the xml of the observation - *

- * To execute the following test, execute it the following way: - * 0. execute 'clean' test - * 1. Execute the 'createSubscription' test - * 2. Update the subscription id in the 'attachWebSocket' test - * 3. Execute the 'attachWebSocket' test - * 4. Execute the 'sendObservation' test - * 5. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id - */ -@Ignore -public class FhirSubscriptionWithSubscriptionIdDstu2IT { - - private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu2IT.class); - private IGenericClient client = FhirServiceUtil.getFhirDstu2Client(); - - @Test - public void clean() { - RemoveDstu2TestIT.deleteResources(Subscription.class, null, client); - RemoveDstu2TestIT.deleteResources(Observation.class, null, client); - } - - @Test - public void createSubscription() { - Subscription subscription = new Subscription(); - subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); - subscription.setStatus(SubscriptionStatusEnum.ACTIVE); - String criteria = "Observation?code=SNOMED-CT|82313006&_format=xml"; - subscription.setCriteria(criteria); - - Subscription.Channel channel = new Subscription.Channel(); - channel.setType(SubscriptionChannelTypeEnum.WEBSOCKET); - channel.setPayload("application/json"); - subscription.setChannel(channel); - - MethodOutcome methodOutcome = client.create().resource(subscription).execute(); - String id = methodOutcome.getId().getIdPart(); - - System.out.println("Subscription id generated by server is: " + id); - } - - @Test - @Ignore - public void attachWebSocket() throws Exception { - String subscriptionId = "1"; - subscriptionId = subscriptionId + ""; - - String target = "ws://localhost:9092/websocket/dstu2"; - - WebSocketClient webSocketClient = new WebSocketClient(); - SocketImplementation socket = new SocketImplementation(subscriptionId, EncodingEnum.JSON); - - try { - webSocketClient.start(); - URI echoUri = new URI(target); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - ourLog.info("Connecting to : {}", echoUri); - webSocketClient.connect(socket, echoUri, request); - - while (true) { - Thread.sleep(500L); - } - - } finally { - try { - ourLog.info("Shutting down websocket client"); - webSocketClient.stop(); - } catch (Exception e) { - ourLog.error("Failure", e); - } - } - } - - @Test - public void createObservation() throws Exception { - Observation observation = new Observation(); - - observation.setStatus(ObservationStatusEnum.FINAL); - CodeableConceptDt codeableConcept = new CodeableConceptDt(); - observation.setCode(codeableConcept); - CodingDt coding = codeableConcept.addCoding(); - coding.setCode("82313006"); - coding.setSystem("SNOMED-CT"); - - MethodOutcome methodOutcome2 = client.create().resource(observation).execute(); - String observationId = methodOutcome2.getId().getIdPart(); - observation.setId(observationId); - - System.out.println("Observation id generated by server is: " + observationId); - } -} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu3IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu3IT.java deleted file mode 100644 index 88b7ba92dfc..00000000000 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu3IT.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ -package ca.uhn.fhir.jpa.demo.subscription; - -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.IGenericClient; -import ca.uhn.fhir.rest.server.EncodingEnum; -import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.hl7.fhir.dstu3.model.*; -import org.hl7.fhir.dstu3.model.Observation; -import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.dstu3.model.Subscription; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; - -import java.net.URI; - -/** - * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the - * subscription - *

- * Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3IT for - * a test that returns the xml of the observation - *

- * To execute the following test, execute it the following way: - * 0. execute 'clean' test - * 1. Execute the 'createPatient' test - * 2. Update the patient id static variable - * 3. Execute the 'createSubscription' test - * 4. Update the subscription id static variable - * 5. Execute the 'attachWebSocket' test - * 6. Execute the 'sendObservation' test - * 7. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id - */ -@Ignore -public class FhirSubscriptionWithSubscriptionIdDstu3IT { - - private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3IT.class); - - public static final String WEBSOCKET_LISTENER_URL = "ws://localhost:9093/websocket/dstu3"; - - public static final String PATIENT_ID = "5102"; - public static final String SUBSCRIPTION_ID = "5103"; - - private IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - @Test - public void clean() { - RemoveDstu3TestIT.deleteResources(Subscription.class, null, client); - RemoveDstu3TestIT.deleteResources(Observation.class, null, client); - } - - @Test - public void createPatient() throws Exception { - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - Patient patient = FhirDstu3Util.getPatient(); - MethodOutcome methodOutcome = client.create().resource(patient).execute(); - String id = methodOutcome.getId().getIdPart(); - patient.setId(id); - System.out.println("Patient id generated by server is: " + id); - } - - @Test - public void createSubscription() { - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - 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("Observation?subject=Patient/" + PATIENT_ID); - subscription.setCriteria("Observation?code=SNOMED-CT|82313006"); - - Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); - channel.setType(Subscription.SubscriptionChannelType.WEBSOCKET); - channel.setPayload("application/json"); - subscription.setChannel(channel); - - MethodOutcome methodOutcome = client.create().resource(subscription).execute(); - String id = methodOutcome.getId().getIdPart(); - - System.out.println("Subscription id generated by server is: " + id); - } - - @Ignore - @Test - public void attachWebSocket() throws Exception { - WebSocketClient webSocketClient = new WebSocketClient(); - SocketImplementation socket = new SocketImplementation(SUBSCRIPTION_ID, EncodingEnum.JSON); - - try { - webSocketClient.start(); - URI echoUri = new URI(WEBSOCKET_LISTENER_URL); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - ourLog.info("Connecting to : {}", echoUri); - webSocketClient.connect(socket, echoUri, request); - - while (true) { - Thread.sleep(500L); - } - - } finally { - try { - ourLog.info("Shutting down websocket client"); - webSocketClient.stop(); - } catch (Exception e) { - ourLog.error("Failure", e); - } - } - } - - @Test - public void createObservation() throws Exception { - Observation observation = new Observation(); - CodeableConcept codeableConcept = new CodeableConcept(); - observation.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode("82313006"); - coding.setSystem("SNOMED-CT"); - Reference reference = new Reference(); - reference.setReference("Patient/" + PATIENT_ID); - observation.setSubject(reference); - observation.setStatus(Observation.ObservationStatus.FINAL); - - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - MethodOutcome methodOutcome2 = client.create().resource(observation).execute(); - String observationId = methodOutcome2.getId().getIdPart(); - observation.setId(observationId); - - System.out.println("Observation id generated by server is: " + observationId); - } - - @Test - public void createObservationThatDoesNotMatch() throws Exception { - Observation observation = new Observation(); - IdDt idDt = new IdDt(); - idDt.setValue("Patient/" + PATIENT_ID); - CodeableConcept codeableConcept = new CodeableConcept(); - observation.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode("8231"); - coding.setSystem("SNOMED-CT"); - observation.setStatus(Observation.ObservationStatus.FINAL); - - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - MethodOutcome methodOutcome2 = client.create().resource(observation).execute(); - String observationId = methodOutcome2.getId().getIdPart(); - observation.setId(observationId); - - System.out.println("Observation id generated by server is: " + observationId); - - } -} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu2IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu2IT.java deleted file mode 100644 index 3eee21bd809..00000000000 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu2IT.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ -package ca.uhn.fhir.jpa.demo.subscription; - -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.valueset.ObservationStatusEnum; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.IGenericClient; -import ca.uhn.fhir.rest.server.EncodingEnum; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.junit.Ignore; -import org.junit.Test; - -import java.net.URI; - -/** - * Adds a FHIR subscription by creating a websocket that includes the subscription criteria. The server will create - * a subscription automatically and return the subscription id - *

- * 0. execute 'clean' test - * 1. Execute the 'attachWebSocket' test - * 2. Execute the 'sendObservation' test - * 3. Look in the 'attachWebSocket' terminal execution and wait for your JSON/XML response - */ -@Ignore -public class FhirSubscriptionWithWebsocketCriteriaDstu2IT { - - public final static String PORT = "9092"; - public final static String WEBSOCKET_PATH = "/websocket/dstu2"; - private IGenericClient client = FhirServiceUtil.getFhirDstu2Client(); - - @Test - public void clean() { - RemoveDstu2TestIT.deleteResources(Subscription.class, null, client); - RemoveDstu2TestIT.deleteResources(Observation.class, null, client); - } - /** - * Attach a websocket to the FHIR server based on a criteria - * - * @throws Exception - */ - @Test - @Ignore - public void attachWebSocket() throws Exception { - String criteria = "Observation?code=SNOMED-CT|82313006&_format=xml"; - - SocketImplementation socket = new SocketImplementation(criteria, EncodingEnum.JSON); - WebSocketClient client = new WebSocketClient(); - - try { - client.start(); - URI echoUri = new URI("ws://localhost:" + PORT + WEBSOCKET_PATH); - client.connect(socket, echoUri); - - Thread.sleep(5000); - } catch (Exception e) { - e.printStackTrace(); - } - - while (true) { - Thread.sleep(60000); - socket.keepAlive(); - } - } - - /** - * Create an observation in the FHIR server - */ - @Test - public void createObservation() { - Observation observation = new Observation(); - observation.setStatus(ObservationStatusEnum.FINAL); - CodeableConceptDt codeableConcept = new CodeableConceptDt(); - observation.setCode(codeableConcept); - CodingDt coding = codeableConcept.addCoding(); - coding.setCode("82313006"); - coding.setSystem("SNOMED-CT"); - - MethodOutcome methodOutcome2 = client.create().resource(observation).execute(); - String observationId = methodOutcome2.getId().getIdPart(); - observation.setId(observationId); - - System.out.println("Observation id generated by server is: " + observationId); - } -} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu3IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu3IT.java deleted file mode 100644 index 400a8ec1dc7..00000000000 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu3IT.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ -package ca.uhn.fhir.jpa.demo.subscription; - -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.IGenericClient; -import ca.uhn.fhir.rest.server.EncodingEnum; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.hl7.fhir.dstu3.model.*; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import java.net.URI; - -/** - * Adds a FHIR subscription by creating a websocket that includes the subscription criteria. The server will create - * a subscription automatically and return the subscription id - *

- * 0. execute 'clean' test - * 1. Execute the 'attachWebSocket' test - * 2. Execute the 'sendObservation' test - * 3. Look in the 'attachWebSocket' terminal execution and wait for your JSON/XML response - */ -@Ignore -public class FhirSubscriptionWithWebsocketCriteriaDstu3IT { - - public final static String PORT = "9093"; - public final static String WEBSOCKET_PATH = "/websocket/dstu3"; - //public final static String WEBSOCKET_PATH = "/baseDstu3"; - private IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - @Test - public void clean() { - RemoveDstu3TestIT.deleteResources(Subscription.class, null, client); - RemoveDstu3TestIT.deleteResources(Observation.class, null, client); - } - - /** - * Attach a websocket to the FHIR server based on a criteria - * - * @throws Exception - */ - @Test - @Ignore - public void attachWebSocket() throws Exception { - String criteria = "Observation?_format=xml"; -// String criteria = "Observation?code=SNOMED-CT|82313006&_format=xml"; - SocketImplementation socket = new SocketImplementation(criteria, EncodingEnum.JSON); - WebSocketClient client = new WebSocketClient(); - - try { - client.start(); - URI echoUri = new URI("ws://localhost:" + PORT + WEBSOCKET_PATH); - - client.connect(socket, echoUri); - - Thread.sleep(5000); - } catch (Exception e) { - e.printStackTrace(); - } - - while (true) { - Thread.sleep(60000); - - socket.keepAlive(); - } - } - - /** - * Create an observation in the FHIR server - */ - @Test - public void createObservation() { - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - Observation observation = new Observation(); - observation.setStatus(Observation.ObservationStatus.FINAL); - CodeableConcept codeableConcept = new CodeableConcept(); - observation.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode("82313006"); - coding.setSystem("SNOMED-CT"); - - MethodOutcome methodOutcome2 = client.create().resource(observation).execute(); - String observationId = methodOutcome2.getId().getIdPart(); - observation.setId(observationId); - - System.out.println("Observation id generated by server is: " + observationId); - } - - @Test - public void deleteObservation(){ - String observationId = "5103"; - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - client.delete().resourceById(Observation.class.getSimpleName(), observationId).execute(); - } -} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3IT.java deleted file mode 100644 index ed15bfd190a..00000000000 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3IT.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ - -package ca.uhn.fhir.jpa.demo.subscription; - -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.IGenericClient; -import org.hl7.fhir.dstu3.model.CodeableConcept; -import org.hl7.fhir.dstu3.model.Coding; -import org.hl7.fhir.dstu3.model.Observation; -import org.hl7.fhir.dstu3.model.Subscription; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; - -/** - * Test the rest-hook subscriptions - */ -@Ignore -public class RestHookTestDstu3IT { - - private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3IT.class); - private IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - @Before - public void clean() { - RemoveDstu3TestIT.deleteResources(Subscription.class, null, client); - RemoveDstu3TestIT.deleteResources(Observation.class, null, client); - } - - @Test - public void testRestHookSubscription() { - IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); - - String payload = "application/json"; - String endpoint = "http://localhost:10080/rest-hook"; - - 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, endpoint, client); - Subscription subscription2 = createSubscription(criteria2, payload, endpoint, client); - - Observation observation1 = sendObservation(code, "SNOMED-CT", client); - //Should see only one subscription notification - - Subscription subscriptionTemp = client.read(Subscription.class, subscription2.getId()); - Assert.assertNotNull(subscriptionTemp); - - subscriptionTemp.setCriteria(criteria1); - client.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); - - Observation observation2 = sendObservation(code, "SNOMED-CT", client); - //Should see two subscription notifications - - client.delete().resourceById(new IdDt("Subscription", subscription2.getId())).execute(); - - Observation observationTemp3 = sendObservation(code, "SNOMED-CT", client); - //Should see only one subscription notification - - Observation observation3 = client.read(Observation.class, observationTemp3.getId()); - CodeableConcept codeableConcept = new CodeableConcept(); - observation3.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code + "111"); - coding.setSystem("SNOMED-CT"); - client.update().resource(observation3).withId(observation3.getIdElement()).execute(); - //Should see no subscription notification - - Observation observation3a = client.read(Observation.class, observationTemp3.getId()); - - CodeableConcept codeableConcept1 = new CodeableConcept(); - observation3a.setCode(codeableConcept1); - Coding coding1 = codeableConcept1.addCoding(); - coding1.setCode(code); - coding1.setSystem("SNOMED-CT"); - client.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); - //Should see only one subscription notification - - Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); - Assert.assertFalse(observation1.getId().isEmpty()); - Assert.assertFalse(observation2.getId().isEmpty()); - } - - public Subscription createSubscription(String criteria, String payload, String endpoint, IGenericClient client) { - Subscription subscription = new Subscription(); - subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); - subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); - subscription.setCriteria(criteria); - - Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); - channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); - channel.setPayload(payload); - channel.setEndpoint(endpoint); - subscription.setChannel(channel); - - MethodOutcome methodOutcome = client.create().resource(subscription).execute(); - subscription.setId(methodOutcome.getId().getIdPart()); - - return subscription; - } - - public Observation sendObservation(String code, String system, IGenericClient client) { - Observation observation = new Observation(); - CodeableConcept codeableConcept = new CodeableConcept(); - observation.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code); - coding.setSystem(system); - - observation.setStatus(Observation.ObservationStatus.FINAL); - - MethodOutcome methodOutcome = client.create().resource(observation).execute(); - - String observationId = methodOutcome.getId().getIdPart(); - observation.setId(observationId); - - return observation; - } -} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/SocketImplementation.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/SocketImplementation.java deleted file mode 100644 index 7b8ad26aaf5..00000000000 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/SocketImplementation.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2017 Cognitive Medical Systems, Inc (http://www.cognitivemedicine.com). - * - * 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. - * - * @author Jeff Chung - */ - -package ca.uhn.fhir.jpa.demo.subscription; - -import ca.uhn.fhir.rest.server.EncodingEnum; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.slf4j.Logger; - -@WebSocket -public class SocketImplementation { - - private String myCriteria; - private Session session; - - protected String myError; - protected boolean myGotBound; - protected int myPingCount; - protected String mySubsId; - - private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(SocketImplementation.class); - - public SocketImplementation(String theCriteria, EncodingEnum theEncoding) { - myCriteria = theCriteria; - } - - /** - * This method is executed when the client is connecting to the server. - * In this case, we are sending a message to create the subscription dynamiclly - * - * @param session - */ - @OnWebSocketConnect - public void onConnect(Session session) { - ourLog.info("Got connect: {}", session); - this.session = session; - try { - String sending = "bind " + myCriteria; - ourLog.info("Sending: {}", sending); - session.getRemote().sendString(sending); - - ourLog.info("Connection: DONE"); - } catch (Throwable t) { - t.printStackTrace(); - ourLog.error("Failure", t); - } - } - - /** - * This is the message handler for the client - * - * @param theMsg - */ - @OnWebSocketMessage - public void onMessage(String theMsg) { - ourLog.info("Got msg: " + theMsg); - - if (theMsg.startsWith("bound ")) { - myGotBound = true; - mySubsId = (theMsg.substring("bound ".length())); - myPingCount++; - } else if (myGotBound && theMsg.startsWith("add " + mySubsId + "\n")) { - String text = theMsg.substring(("add " + mySubsId + "\n").length()); - ourLog.info("text: " + text); - myPingCount++; - } else { - myError = "Unexpected message: " + theMsg; - } - } - - public void keepAlive() { - if (this.session != null) { - try { - session.getRemote().sendString("keep alive"); - } catch (Throwable t) { - ourLog.error("Failure", t); - } - } - } -} \ No newline at end of file