diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java index e19e162e520..a6a46bfe669 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java @@ -28,6 +28,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -48,7 +49,9 @@ public class RuntimeResourceDefinition extends BaseRuntimeElementCompositeDefini private List mySearchParams; private final FhirVersionEnum myStructureVersion; private volatile RuntimeResourceDefinition myBaseDefinition; - + + + public RuntimeResourceDefinition(FhirContext theContext, String theResourceName, Class theClass, ResourceDef theResourceAnnotation, boolean theStandardType, Map, BaseRuntimeElementDefinition> theClassToElementDefinitions) { super(theResourceName, theClass, theStandardType, theContext, theClassToElementDefinitions); myContext = theContext; @@ -68,6 +71,7 @@ public class RuntimeResourceDefinition extends BaseRuntimeElementCompositeDefini } + public void addSearchParam(RuntimeSearchParam theParam) { myNameToSearchParam.put(theParam.getName(), theParam); } diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index d347430a11f..d236fe206ea 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -14,6 +14,28 @@ HAPI FHIR JPA Server + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate4 + ${jackson.version} + + + + org.apache.commons + commons-lang3 + 3.5 + + + net.sf.saxon + Saxon-HE + 9.5.1-5 + + org.apache.commons commons-csv @@ -363,6 +385,7 @@ false + 2.7.1 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 75bb3f76f95..fb7d6c9fc51 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 @@ -886,6 +886,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (theRequestDetails.isSubRequest()) { theParams.setLoadSynchronous(true); + int max = myDaoConfig.getMaximumSearchResultCountInTransaction(); theParams.setLoadSynchronousUpTo(myDaoConfig.getMaximumSearchResultCountInTransaction()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 90c02c78e14..2cb2176ec68 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -180,7 +180,7 @@ public class DaoConfig { @Deprecated public List getInterceptors() { if (myInterceptors == null) { - return Collections.emptyList(); + myInterceptors = new ArrayList(); } return myInterceptors; } 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 000f1395590..16fe190b762 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,6 +41,11 @@ 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 27ec465869c..e74a5155528 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,6 +145,11 @@ 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 0843309893a..f36162cec94 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 @@ -29,6 +29,7 @@ import java.util.List; import javax.persistence.Query; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -116,8 +117,12 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2 extends IDao { */ MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequestDetails); + RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria); + + IFhirResourceDao getDao(Class theType) ; + // /** // * Invoke the everything operation // */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java index 5f333058f2d..c54e5a0c5e6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoSubscription.java @@ -37,4 +37,5 @@ public interface IFhirResourceDaoSubscription extends I void pollForNewUndeliveredResourcesScheduler(); + int pollForNewUndeliveredResources(String resourceType); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index ba202350b21..64eab8a1041 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -1474,7 +1474,8 @@ public class SearchBuilder implements ISearchBuilder { public void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, EntityManager entityManager, FhirContext context, IDao theDao) { if (theIncludePids.isEmpty()) { - return; + ourLog.info("The include pids are empty"); + //return; } // Dupes will cause a crash later anyhow, but this is expensive so only do it diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java index 9da4a0c7c92..3cb3e0b1212 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity; @@ -71,6 +72,9 @@ public class FhirResourceDaoDstu3 extends BaseHapiFhirRe @Qualifier("myInstanceValidatorDstu3") private IValidatorModule myInstanceValidator; + @Autowired + private FhirContext fhirContext; + @Override protected IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode) { OperationOutcome oo = new OperationOutcome(); @@ -163,6 +167,26 @@ public class FhirResourceDaoDstu3 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 a30486818ce..84d17bdc396 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 @@ -119,9 +119,13 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3 mySubscriptionDao; + + private static volatile ExecutorService executor; + private FhirResourceDaoSubscriptionDstu2 myResourceSubscriptionDao; + + private static final Logger logger = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class); + private final List restHookSubscriptions = new ArrayList(); + private boolean notifyOnDelete = false; + + private final static int MAX_THREADS = 1; + + @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"); + } + } + + /** + * 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())); + + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); + + IBundleProvider subscriptionBundleList = mySubscriptionDao.search(map, req); + List resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); + + for (IBaseResource resource : resourceList) { + restHookSubscriptions.add((Subscription) resource); + } + } + + /** + * 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); + } + } + + /** + * 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(); + + logger.info("resource updated 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())) { + removeLocalSubscription(subscription.getId().getIdPart()); + + if (subscription.getStatus().equals(SubscriptionStatusEnum.ACTIVE.getCode())) { + restHookSubscriptions.add(subscription); + logger.info("Subscription was updated. Id: " + subscription.getId()); + } + } + } else { + checkSubscriptions(idType, resourceType); + } + } + + /** + * 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(); + + if (resourceType.equals(Subscription.class.getSimpleName())) { + String id = idType.getIdPart(); + removeLocalSubscription(id); + } else { + if (notifyOnDelete) { + checkSubscriptions(idType, resourceType); + } + } + } + + return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); + } + + /** + * 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); + + 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)); + } + } + } + + /** + * 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); + + 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); + + 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); + + 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) { + Subscription subscription = new Subscription(); + subscription.setCriteria(criteria); + + RuntimeResourceDefinition responseResourceDef = myResourceSubscriptionDao.validateCriteriaAndReturnResourceDefinition(subscription); + SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(myResourceSubscriptionDao, myResourceSubscriptionDao.getContext(), criteria, responseResourceDef); + + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); + + IFhirResourceDao responseDao = myResourceSubscriptionDao.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.Entry entry = bundle.addEntry(); + entry.setResource((IResource) resource); + } + bundle.setTotal(resourcelist.size()); + + return bundle; + } + + /** + * 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); + + 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.getId().getIdPart())) { + return restHookSubscription; + } + } + } + } + + return null; + } + + public boolean isNotifyOnDelete() { + return notifyOnDelete; + } + + public void setNotifyOnDelete(boolean notifyOnDelete) { + this.notifyOnDelete = 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 new file mode 100644 index 00000000000..141769cf661 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java @@ -0,0 +1,446 @@ +/* + * 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.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 org.apache.http.NameValuePair; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +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; +import org.hl7.fhir.instance.model.api.IIdType; +import org.slf4j.Logger; +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; + +public class RestHookSubscriptionDstu3Interceptor extends InterceptorAdapter implements IJpaServerInterceptor { + + @Autowired + @Qualifier("mySubscriptionDaoDstu3") + private IFhirResourceDao mySubscriptionDao; + @Autowired + @Qualifier("myObservationDaoDstu3") + private IFhirResourceDao myObservationDao; + + private static volatile ExecutorService executor; + + private static final Logger logger = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class); + private final List restHookSubscriptions = new ArrayList(); + private boolean notifyOnDelete = false; + + private final static int MAX_THREADS = 1; + + @PostConstruct + public void postConstruct() { + try { + executor = Executors.newFixedThreadPool(MAX_THREADS); + } catch (Exception e) { + throw new RuntimeException("Unable to get DAO from PROXY"); + } + } + + /** + * 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())); + + RequestDetails req = new ServletSubRequestDetails(); + req.setSubRequest(true); + + IBundleProvider subscriptionBundleList = mySubscriptionDao.search(map, req); + List resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); + + for (IBaseResource resource : resourceList) { + restHookSubscriptions.add((Subscription) resource); + } + } + + /** + * 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); + } + } + + /** + * 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(); + + logger.info("resource updated type: " + resourceType); + + 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()); + + if (subscription.getStatus() == Subscription.SubscriptionStatus.ACTIVE) { + restHookSubscriptions.add(subscription); + logger.info("Subscription was updated. Id: " + subscription.getId()); + } + } + } else { + checkSubscriptions(idType, resourceType); + } + } + + + /** + * 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(); + + if (resourceType.equals(Subscription.class.getSimpleName())) { + String id = idType.getIdPart(); + removeLocalSubscription(id); + } else { + if (notifyOnDelete) { + checkSubscriptions(idType, resourceType); + } + } + } + + return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); + } + + /** + * 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); + + 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 + 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); + + IBundleProvider results = getBundleProvider(criteria); + + if (results.size() == 0) { + continue; + } + + //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)); + } + } + } + + /** + * 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; + + 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); + + 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); + + 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); + + //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); + + 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; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu2Interceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu2Interceptor.java new file mode 100644 index 00000000000..50d43f283a7 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu2Interceptor.java @@ -0,0 +1,88 @@ +/* + * 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.interceptor; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSubscription; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +import javax.annotation.PostConstruct; + +public class WebSocketSubscriptionDstu2Interceptor extends InterceptorAdapter implements IJpaServerInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketSubscriptionDstu2Interceptor.class); + + @Autowired + @Qualifier("mySubscriptionDaoDstu2") + private IFhirResourceDao reference; + + private IFhirResourceDaoSubscription casted; + + @PostConstruct + public void postConstruct(){ + casted = (IFhirResourceDaoSubscription) reference; + } + + @Override + public void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { + } + + @Override + public void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { + } + + @Override + public void resourceDeleted(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); + } + + 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()); + } + + return super.outgoingResponse(theRequestDetails, theResponseObject); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..e80488b95a9 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/WebSocketSubscriptionDstu3Interceptor.java @@ -0,0 +1,101 @@ +/* + * 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.interceptor; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSubscription; +import ca.uhn.fhir.jpa.entity.ResourceTable; +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.InterceptorAdapter; +import org.hl7.fhir.dstu3.model.Subscription; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +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; + +public class WebSocketSubscriptionDstu3Interceptor extends InterceptorAdapter implements IJpaServerInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketSubscriptionDstu3Interceptor.class); + + @Autowired + @Qualifier("mySubscriptionDaoDstu3") + private IFhirResourceDao reference; + + private IFhirResourceDaoSubscription casted; + + @PostConstruct + public void postConstruct(){ + casted = (IFhirResourceDaoSubscription) reference; + } + + @Override + public void resourceCreated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { + } + + @Override + public void resourceUpdated(ActionRequestDetails theDetails, ResourceTable theResourceTable) { + } + + @Override + public void resourceDeleted(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); + } + + 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()); + } + + return super.outgoingResponse(theRequestDetails, theResponseObject); + } + + @Override + public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { + if (theRequestDetails.getRestOperationType().equals(RestOperationTypeEnum.DELETE)) { + casted.pollForNewUndeliveredResources(theRequestDetails.getResourceName()); + } + + return super.incomingRequestPostProcessed(theRequestDetails, theRequest, theResponse); + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/service/TMinusService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/service/TMinusService.java new file mode 100644 index 00000000000..38415091208 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/service/TMinusService.java @@ -0,0 +1,126 @@ +/* + * 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.service; + +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TMinusService { + + private static final Logger logger = LoggerFactory.getLogger(TMinusService.class); + + private static final String TMINUS = "Tminus"; + private static final String WEEK = "w"; + private static final String DAY = "d"; + private static final String HOUR = "h"; + private static final String MINUTE = "m"; + private static final String SECOND = "s"; + + private static final long MINUTE_AS_MILLIS = 60 * 1000; + private static final long HOUR_AS_MILLIS = 60 * 60 * 1000; + private static final long DAY_AS_MILLIS = 24 * 60 * 60 * 1000; + private static final long WEEK_AS_MILLIS = 7 * 24 * 60 * 60 * 1000; + + private static final Pattern TMINUS_PATTERN_REGEX = Pattern.compile("&([a-zA-Z0-9_]+)=Tminus([0-9]+)([wdhms])"); + + public static void main(String ... aaa){ + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&effectiveDate=Tminus1m" + "&_format=xml"; + + Pattern myPattern = TMINUS_PATTERN_REGEX; + Matcher matcher = myPattern.matcher(payloadCriteria); + + if (matcher.find()) { + String tMinus = matcher.group(); + String tMinusVarName = tMinus.substring(1, tMinus.indexOf("=")); + String tMinusValue = tMinus.substring(tMinus.indexOf(TMINUS) + TMINUS.length(), tMinus.length() - 1); + String tMinusPeriod = tMinus.substring(tMinus.length() - 1); + + System.out.println(matcher.group()); + System.out.println(tMinusVarName); + System.out.println(tMinusValue); + System.out.println(tMinusPeriod); + }else{ + System.out.println("mmm"); + } + } + + public static String parseCriteria(String criteria) { + Matcher matcher = TMINUS_PATTERN_REGEX.matcher(criteria); + String response = criteria; + boolean matcherFound = false; + Date currentDate = new Date(); + + + while (matcher.find()) { + matcherFound = true; + + String tMinus = matcher.group(); + String tMinusVarName = tMinus.substring(1, tMinus.indexOf("=")); + String tMinusValue = tMinus.substring(tMinus.indexOf(TMINUS) + TMINUS.length(), tMinus.length() - 1); + String tMinusPeriod = tMinus.substring(tMinus.length() - 1); + long tMinusMillis = getTMinusValueAsLong(tMinusValue, tMinusPeriod); + String dateValue = getDateParameterValue(tMinusMillis, tMinusVarName, currentDate); + + logger.debug("Tminus value replaced in criteria: " + criteria); + response = response.replace(tMinus, dateValue); + } + + if(!matcherFound){ + logger.debug("Tminus value not found in criteria: " + criteria); + } + + return response; + } + + private static String getDateParameterValue(long tMinusMillis, String tMinusVarName, Date currentDate){ + Date lowerDate = new Date(currentDate.getTime() - tMinusMillis); + + DateTimeType lowerDateTimeType = new DateTimeType(lowerDate); + DateTimeType currentDateTimeType = new DateTimeType(currentDate); + + return "&" + tMinusVarName + "=%3E%3D" + lowerDateTimeType.getValueAsString() + "&" + tMinusVarName + "=%3C%3D" + currentDateTimeType.getValueAsString(); + } + + private static long getTMinusValueAsLong(String tMinusValue, String tMinusPeriod) { + long tMinusLongValue = Long.parseLong(tMinusValue); + long tMinusMillis; + + if (WEEK.equals(tMinusPeriod)) { + tMinusMillis = WEEK_AS_MILLIS * tMinusLongValue; + } else if (DAY.equals(tMinusPeriod)) { + tMinusMillis = DAY_AS_MILLIS * tMinusLongValue; + } else if (HOUR.equals(tMinusPeriod)) { + tMinusMillis = HOUR_AS_MILLIS * tMinusLongValue; + } else if (MINUTE.equals(tMinusPeriod)) { + tMinusMillis = MINUTE_AS_MILLIS * tMinusLongValue; + } else if (SECOND.equals(tMinusPeriod)) { + tMinusMillis = 1000 * tMinusLongValue; + } else { + throw new IllegalArgumentException("Period not recognized: " + tMinusPeriod); + } + + return tMinusMillis; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionWebsocketReturnResourceHandlerDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionWebsocketReturnResourceHandlerDstu2.java new file mode 100644 index 00000000000..e02ab77959f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionWebsocketReturnResourceHandlerDstu2.java @@ -0,0 +1,357 @@ +/* + * 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.subscription; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSubscription; +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum; +import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ScheduledFuture; + +public class SubscriptionWebsocketReturnResourceHandlerDstu2 extends TextWebSocketHandler implements ISubscriptionWebsocketHandler, Runnable { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionWebsocketReturnResourceHandlerDstu2.class); + + @Autowired + @Qualifier("myFhirContextDstu2") + private FhirContext myCtx; + + private ScheduledFuture myScheduleFuture; + + private IState myState = new InitialState(); + + @Autowired + private IFhirResourceDaoSubscription mySubscriptionDao; + + private IIdType mySubscriptionId; + private Long mySubscriptionPid; + + @Autowired + @Qualifier("websocketTaskScheduler") + private TaskScheduler myTaskScheduler; + + @Override + public void afterConnectionClosed(WebSocketSession theSession, CloseStatus theStatus) throws Exception { + super.afterConnectionClosed(theSession, theStatus); + ourLog.info("Closing WebSocket connection from {}", theSession.getRemoteAddress()); + } + + @Override + public void afterConnectionEstablished(WebSocketSession theSession) throws Exception { + super.afterConnectionEstablished(theSession); + ourLog.info("Incoming WebSocket connection from {}", theSession.getRemoteAddress()); + } + + protected void handleFailure(Exception theE) { + ourLog.error("Failure during communication", theE); + } + + @Override + protected void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) throws Exception { + ourLog.info("Textmessage: " + theMessage.getPayload()); + + myState.handleTextMessage(theSession, theMessage); + } + + @Override + public void handleTransportError(WebSocketSession theSession, Throwable theException) throws Exception { + super.handleTransportError(theSession, theException); + ourLog.error("Transport error", theException); + } + + @PostConstruct + public void postConstruct() { + ourLog.info("Creating scheduled task for subscription websocket connection"); + myScheduleFuture = myTaskScheduler.scheduleWithFixedDelay(this, 1000); + } + + @PreDestroy + public void preDescroy() { + ourLog.info("Cancelling scheduled task for subscription websocket connection"); + myScheduleFuture.cancel(true); + IState state = myState; + if (state != null) { + state.closing(); + } + } + + @Override + public void run() { + Long subscriptionPid = mySubscriptionPid; + if (subscriptionPid == null) { + return; + } + + ourLog.debug("Subscription {} websocket handler polling", subscriptionPid); + + List results = mySubscriptionDao.getUndeliveredResourcesAndPurge(subscriptionPid); + if (results.isEmpty() == false) { + myState.deliver(results); + } + } + + private class BoundDynamicSubscriptionState implements IState { + + private EncodingEnum myEncoding; + private WebSocketSession mySession; + + public BoundDynamicSubscriptionState(WebSocketSession theSession, EncodingEnum theEncoding) { + mySession = theSession; + myEncoding = theEncoding; + } + + @Override + public void closing() { + ourLog.info("Deleting subscription {}", mySubscriptionId); + try { + mySubscriptionDao.delete(mySubscriptionId, null); + } catch (Exception e) { + handleFailure(e); + } + } + + @Override + public void deliver(List theResults) { + try { + for (IBaseResource nextResource : theResults) { + ourLog.info("Sending WebSocket message for resource: {}", nextResource.getIdElement()); + String encoded = myEncoding.newParser(myCtx).encodeResourceToString(nextResource); + String payload = "add " + mySubscriptionId.getIdPart() + '\n' + encoded; + mySession.sendMessage(new TextMessage(payload)); + } + } catch (IOException e) { + handleFailure(e); + } + } + + @Override + public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { + try { + theSession.sendMessage(new TextMessage("Unexpected client message: " + theMessage.getPayload())); + } catch (IOException e) { + handleFailure(e); + } + } + + } + + private class BoundStaticSubscriptionState implements IState { + + private EncodingEnum myEncoding; + private WebSocketSession mySession; + + public BoundStaticSubscriptionState(WebSocketSession theSession, EncodingEnum theEncoding) { + mySession = theSession; + myEncoding = theEncoding; + } + + @Override + public void closing() { + // nothing + } + + @Override + public void deliver(List theResults) { + try { + for (IBaseResource nextResource : theResults) { + ourLog.info("Sending WebSocket message for resource: {}", nextResource.getIdElement()); + String encoded = myEncoding.newParser(myCtx).encodeResourceToString(nextResource); + String payload = "add " + mySubscriptionId.getIdPart() + '\n' + encoded; + mySession.sendMessage(new TextMessage(payload)); + } + } catch (IOException e) { + handleFailure(e); + } + } + + @Override + public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { + try { + theSession.sendMessage(new TextMessage("Unexpected client message: " + theMessage.getPayload())); + } catch (IOException e) { + handleFailure(e); + } + } + + } + + private class InitialState implements IState { + + private IIdType bindSimple(WebSocketSession theSession, String theBindString) { + IdDt id = new IdDt(theBindString); + + if (!id.hasIdPart() || !id.isIdPartValid()) { + try { + String message = "Invalid bind request - No ID included"; + ourLog.warn(message); + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), message)); + } catch (IOException e) { + handleFailure(e); + } + return null; + } + + if (id.hasResourceType() == false) { + id = id.withResourceType("Subscription"); + } + + try { + Subscription subscription = mySubscriptionDao.read(id, null); + EncodingEnum encoding = EncodingEnum.JSON; + String criteria = subscription.getCriteria(); + 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())) { + EncodingEnum nextEncoding = EncodingEnum.forContentType(nameValuePair.getValue()); + if (nextEncoding != null) { + encoding = nextEncoding; + } + } + } + + + mySubscriptionPid = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(id); + mySubscriptionId = subscription.getIdElement(); + myState = new BoundStaticSubscriptionState(theSession, encoding); + } catch (ResourceNotFoundException e) { + try { + String message = "Invalid bind request - Unknown subscription: " + id.getValue(); + ourLog.warn(message); + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), message)); + } catch (IOException e1) { + handleFailure(e); + } + return null; + } + + return id; + } + + private IIdType bindSearch(WebSocketSession theSession, String theRemaining) { + Subscription subscription = new Subscription(); + subscription.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET); + subscription.setStatus(SubscriptionStatusEnum.ACTIVE); + subscription.setCriteria(theRemaining); + + try { + String params = theRemaining.substring(theRemaining.indexOf('?')+1); + List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); + EncodingEnum encoding = EncodingEnum.JSON; + for (NameValuePair nameValuePair : paramValues) { + if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { + EncodingEnum nextEncoding = EncodingEnum.forContentType(nameValuePair.getValue()); + if (nextEncoding != null) { + encoding = nextEncoding; + } + } + } + + IIdType id = mySubscriptionDao.create(subscription).getId(); + + mySubscriptionPid = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(id); + mySubscriptionId = subscription.getIdElement(); + myState = new BoundDynamicSubscriptionState(theSession, encoding); + + return id; + } catch (UnprocessableEntityException e) { + ourLog.warn("Failed to bind subscription: " + e.getMessage()); + try { + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), "Invalid bind request - " + e.getMessage())); + } catch (IOException e2) { + handleFailure(e2); + } + } catch (Exception e) { + handleFailure(e); + try { + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), "Invalid bind request - No ID included")); + } catch (IOException e2) { + handleFailure(e2); + } + } + return null; + } + + @Override + public void closing() { + // nothing + } + + @Override + public void deliver(List theResults) { + throw new IllegalStateException(); + } + + @Override + public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { + String message = theMessage.getPayload(); + if (message.startsWith("bind ")) { + String remaining = message.substring("bind ".length()); + + IIdType subscriptionId; + if (remaining.contains("?")) { + subscriptionId = bindSearch(theSession, remaining); + } else { + subscriptionId = bindSimple(theSession, remaining); + if (subscriptionId == null) { + return; + } + } + + try { + theSession.sendMessage(new TextMessage("bound " + subscriptionId.getIdPart())); + } catch (IOException e) { + handleFailure(e); + } + + } + } + + } + + private interface IState { + + void closing(); + + void deliver(List theResults); + + void handleTextMessage(WebSocketSession theSession, TextMessage theMessage); + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionWebsocketReturnResourceHandlerDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionWebsocketReturnResourceHandlerDstu3.java new file mode 100644 index 00000000000..5b86ac3b508 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionWebsocketReturnResourceHandlerDstu3.java @@ -0,0 +1,355 @@ +/* + * 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.subscription; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSubscription; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.Subscription; +import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType; +import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ScheduledFuture; + +public class SubscriptionWebsocketReturnResourceHandlerDstu3 extends TextWebSocketHandler implements ISubscriptionWebsocketHandler, Runnable { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionWebsocketReturnResourceHandlerDstu3.class); + + @Autowired + private FhirContext myCtx; + + private ScheduledFuture myScheduleFuture; + + private IState myState = new InitialState(); + + @Autowired + private IFhirResourceDaoSubscription mySubscriptionDao; + + private IIdType mySubscriptionId; + private Long mySubscriptionPid; + + @Autowired + @Qualifier("websocketTaskScheduler") + private TaskScheduler myTaskScheduler; + + @Override + public void afterConnectionClosed(WebSocketSession theSession, CloseStatus theStatus) throws Exception { + super.afterConnectionClosed(theSession, theStatus); + ourLog.info("Closing WebSocket connection from {}", theSession.getRemoteAddress()); + } + + @Override + public void afterConnectionEstablished(WebSocketSession theSession) throws Exception { + super.afterConnectionEstablished(theSession); + ourLog.info("Incoming WebSocket connection from {}", theSession.getRemoteAddress()); + } + + protected void handleFailure(Exception theE) { + ourLog.error("Failure during communication", theE); + } + + @Override + protected void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) throws Exception { + ourLog.info("Textmessage: " + theMessage.getPayload()); + + myState.handleTextMessage(theSession, theMessage); + } + + @Override + public void handleTransportError(WebSocketSession theSession, Throwable theException) throws Exception { + super.handleTransportError(theSession, theException); + ourLog.error("Transport error", theException); + } + + @PostConstruct + public void postConstruct() { + ourLog.info("Creating scheduled task for subscription websocket connection"); + myScheduleFuture = myTaskScheduler.scheduleWithFixedDelay(this, 1000); + } + + @PreDestroy + public void preDescroy() { + ourLog.info("Cancelling scheduled task for subscription websocket connection"); + myScheduleFuture.cancel(true); + IState state = myState; + if (state != null) { + state.closing(); + } + } + + @Override + public void run() { + Long subscriptionPid = mySubscriptionPid; + if (subscriptionPid == null) { + return; + } + + ourLog.debug("Subscription {} websocket handler polling", subscriptionPid); + + List results = mySubscriptionDao.getUndeliveredResourcesAndPurge(subscriptionPid); + if (results.isEmpty() == false) { + myState.deliver(results); + } + } + + private class BoundDynamicSubscriptionState implements IState { + + private EncodingEnum myEncoding; + private WebSocketSession mySession; + + public BoundDynamicSubscriptionState(WebSocketSession theSession, EncodingEnum theEncoding) { + mySession = theSession; + myEncoding = theEncoding; + } + + @Override + public void closing() { + ourLog.info("Deleting subscription {}", mySubscriptionId); + try { + mySubscriptionDao.delete(mySubscriptionId, null); + } catch (Exception e) { + handleFailure(e); + } + } + + @Override + public void deliver(List theResults) { + try { + for (IBaseResource nextResource : theResults) { + ourLog.info("Sending WebSocket message for resource: {}", nextResource.getIdElement()); + String encoded = myEncoding.newParser(myCtx).encodeResourceToString(nextResource); + String payload = "add " + mySubscriptionId.getIdPart() + '\n' + encoded; + mySession.sendMessage(new TextMessage(payload)); + } + } catch (IOException e) { + handleFailure(e); + } + } + + @Override + public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { + try { + theSession.sendMessage(new TextMessage("Unexpected client message: " + theMessage.getPayload())); + } catch (IOException e) { + handleFailure(e); + } + } + + } + + private class BoundStaticSubscriptionState implements IState { + + private EncodingEnum myEncoding; + private WebSocketSession mySession; + + public BoundStaticSubscriptionState(WebSocketSession theSession, EncodingEnum theEncoding) { + mySession = theSession; + myEncoding = theEncoding; + } + + @Override + public void closing() { + // nothing + } + + @Override + public void deliver(List theResults) { + try { + for (IBaseResource nextResource : theResults) { + ourLog.info("Sending WebSocket message for resource: {}", nextResource.getIdElement()); + String encoded = myEncoding.newParser(myCtx).encodeResourceToString(nextResource); + String payload = "add " + mySubscriptionId.getIdPart() + '\n' + encoded; + mySession.sendMessage(new TextMessage(payload)); + } + } catch (IOException e) { + handleFailure(e); + } + } + + @Override + public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { + try { + theSession.sendMessage(new TextMessage("Unexpected client message: " + theMessage.getPayload())); + } catch (IOException e) { + handleFailure(e); + } + } + + } + + private class InitialState implements IState { + + private IIdType bindSimple(WebSocketSession theSession, String theBindString) { + IdType id = new IdType(theBindString); + + if (!id.hasIdPart() || !id.isIdPartValid()) { + try { + String message = "Invalid bind request - No ID included"; + ourLog.warn(message); + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), message)); + } catch (IOException e) { + handleFailure(e); + } + return null; + } + + if (id.hasResourceType() == false) { + id = id.withResourceType("Subscription"); + } + + try { + Subscription subscription = mySubscriptionDao.read(id, null); + EncodingEnum encoding = EncodingEnum.JSON; + String criteria = subscription.getCriteria(); + 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())) { + EncodingEnum nextEncoding = EncodingEnum.forContentType(nameValuePair.getValue()); + if (nextEncoding != null) { + encoding = nextEncoding; + } + } + } + + mySubscriptionPid = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(id); + mySubscriptionId = subscription.getIdElement(); + myState = new BoundStaticSubscriptionState(theSession, encoding); + } catch (ResourceNotFoundException e) { + try { + String message = "Invalid bind request - Unknown subscription: " + id.getValue(); + ourLog.warn(message); + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), message)); + } catch (IOException e1) { + handleFailure(e); + } + return null; + } + + return id; + } + + private IIdType bindSearch(WebSocketSession theSession, String theRemaining) { + Subscription subscription = new Subscription(); + subscription.getChannel().setType(SubscriptionChannelType.WEBSOCKET); + subscription.setStatus(SubscriptionStatus.ACTIVE); + subscription.setCriteria(theRemaining); + + try { + String params = theRemaining.substring(theRemaining.indexOf('?')+1); + List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); + EncodingEnum encoding = EncodingEnum.JSON; + for (NameValuePair nameValuePair : paramValues) { + if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { + EncodingEnum nextEncoding = EncodingEnum.forContentType(nameValuePair.getValue()); + if (nextEncoding != null) { + encoding = nextEncoding; + } + } + } + + IIdType id = mySubscriptionDao.create(subscription).getId(); + + mySubscriptionPid = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(id); + mySubscriptionId = subscription.getIdElement(); + myState = new BoundDynamicSubscriptionState(theSession, encoding); + + return id; + } catch (UnprocessableEntityException e) { + ourLog.warn("Failed to bind subscription: " + e.getMessage()); + try { + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), "Invalid bind request - " + e.getMessage())); + } catch (IOException e2) { + handleFailure(e2); + } + } catch (Exception e) { + handleFailure(e); + try { + theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), "Invalid bind request - No ID included")); + } catch (IOException e2) { + handleFailure(e2); + } + } + return null; + } + + @Override + public void closing() { + // nothing + } + + @Override + public void deliver(List theResults) { + throw new IllegalStateException(); + } + + @Override + public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { + String message = theMessage.getPayload(); + if (message.startsWith("bind ")) { + String remaining = message.substring("bind ".length()); + + IIdType subscriptionId; + if (remaining.contains("?")) { + subscriptionId = bindSearch(theSession, remaining); + } else { + subscriptionId = bindSimple(theSession, remaining); + if (subscriptionId == null) { + return; + } + } + + try { + theSession.sendMessage(new TextMessage("bound " + subscriptionId.getIdPart())); + } catch (IOException e) { + handleFailure(e); + } + + } + } + + } + + private interface IState { + + void closing(); + + void deliver(List theResults); + + void handleTextMessage(WebSocketSession theSession, TextMessage theMessage); + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/thread/HttpRequestDstu2Job.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/thread/HttpRequestDstu2Job.java new file mode 100644 index 00000000000..db6a0a32640 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/thread/HttpRequestDstu2Job.java @@ -0,0 +1,49 @@ +package ca.uhn.fhir.jpa.thread; + +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.HttpClientBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class HttpRequestDstu2Job implements Runnable{ + + private HttpUriRequest request; + private Subscription subscription; + + private static final Logger logger = LoggerFactory.getLogger(HttpRequestDstu2Job.class); + + public HttpRequestDstu2Job(HttpUriRequest request, Subscription subscription){ + this.request = request; + this.subscription = subscription; + } + + @Override + public void run() { + executeRequest(request, subscription); + } + + /** + * Sends a post back to the subscription client + * + * @param request + * @param subscription + */ + private void executeRequest(HttpUriRequest request, Subscription subscription) { + String url = subscription.getChannel().getEndpoint(); + + try { + HttpClient client = HttpClientBuilder.create().build(); + client.execute(request); + logger.info("sent: " + request.getURI()); + } catch (IOException e) { + logger.error("Error sending rest post call from subscription " + subscription.getId() + " with endpoint " + url); + e.printStackTrace(); + } + + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/thread/HttpRequestDstu3Job.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/thread/HttpRequestDstu3Job.java new file mode 100644 index 00000000000..45a5f5e287b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/thread/HttpRequestDstu3Job.java @@ -0,0 +1,48 @@ +package ca.uhn.fhir.jpa.thread; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.HttpClientBuilder; +import org.hl7.fhir.dstu3.model.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class HttpRequestDstu3Job implements Runnable { + + private HttpUriRequest request; + private Subscription subscription; + + private static final Logger logger = LoggerFactory.getLogger(HttpRequestDstu3Job.class); + + public HttpRequestDstu3Job(HttpUriRequest request, Subscription subscription) { + this.request = request; + this.subscription = subscription; + } + + @Override + public void run() { + executeRequest(request, subscription); + } + + /** + * Sends a post back to the subscription client + * + * @param request + * @param subscription + */ + private void executeRequest(HttpUriRequest request, Subscription subscription) { + String url = subscription.getChannel().getEndpoint(); + + try { + HttpClient client = HttpClientBuilder.create().build(); + client.execute(request); + } catch (IOException e) { + logger.error("Error sending rest post call from subscription " + subscription.getId() + " with endpoint " + url); + e.printStackTrace(); + } + + logger.info("sent: " + url); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/MethodRequest.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/MethodRequest.java new file mode 100644 index 00000000000..2bf702ffa33 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/MethodRequest.java @@ -0,0 +1,11 @@ +package ca.uhn.fhir.jpa.util; + +/** + * Created by Jeff on 2/8/2017. + */ +public enum MethodRequest { + POST, + GET, + PUT, + DELETE +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/RestUtilities.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/RestUtilities.java new file mode 100644 index 00000000000..026cccae56f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/RestUtilities.java @@ -0,0 +1,110 @@ +/* + * 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.util; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; + +import javax.xml.ws.http.HTTPException; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * Rest service utilities. Generally used in the tests + */ +public class RestUtilities { + + public static final String CONTEXT_PATH = ""; + public static final String APPLICATION_JSON = "application/json"; + + /** + * Get the response for a CXF REST service without an object parameter + * + * @param url + * @param typeRequest + * @return + * @throws IOException + */ + public static String getResponse(String url, MethodRequest typeRequest) throws IOException { + return getResponse(url, (StringEntity) null, typeRequest); + } + + /** + * Get the response for a CXF REST service with an object parameter + * + * @param url + * @param parameterEntity + * @param typeRequest + * @return + * @throws IOException + */ + public static String getResponse(String url, StringEntity parameterEntity, MethodRequest typeRequest) throws IOException { + HttpClient httpclient = new DefaultHttpClient(); + HttpResponse response; + + switch (typeRequest) { + case POST: + HttpPost httppost = new HttpPost(url); + httppost.setHeader("Content-type", APPLICATION_JSON); + if (parameterEntity != null) { + httppost.setEntity(parameterEntity); + } + response = httpclient.execute(httppost); + break; + case PUT: + HttpPut httpPut = new HttpPut(url); + httpPut.setHeader("Content-type", APPLICATION_JSON); + if (parameterEntity != null) { + httpPut.setEntity(parameterEntity); + } + response = httpclient.execute(httpPut); + break; + case DELETE: + HttpDelete httpDelete = new HttpDelete(url); + httpDelete.setHeader("Content-type", APPLICATION_JSON); + response = httpclient.execute(httpDelete); + break; + case GET: + HttpGet httpGet = new HttpGet(url); + httpGet.setHeader("Content-type", APPLICATION_JSON); + response = httpclient.execute(httpGet); + break; + default: + throw new IllegalArgumentException("Cannot handle type request " + typeRequest); + } + + if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) { + throw new HTTPException(response.getStatusLine().getStatusCode()); + } + + if (response.getStatusLine().getStatusCode() == 204) { + return ""; + } + + //Closes connections that have already been closed by the server + //org.apache.http.NoHttpResponseException: The target server failed to respond + httpclient.getConnectionManager().closeIdleConnections(1, TimeUnit.SECONDS); + + return EntityUtils.toString(response.getEntity()); + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SpringObjectCaster.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SpringObjectCaster.java new file mode 100644 index 00000000000..ec325915a58 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SpringObjectCaster.java @@ -0,0 +1,42 @@ +/* + * 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.util; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; + +/** + * Utility to get the Spring proxy object's target object + */ +public class SpringObjectCaster { + + /** + * Retrieve the Spring proxy object's target object + * @param proxy + * @param clazz + * @param + * @return + * @throws Exception + */ + public static T getTargetObject(Object proxy, Class clazz) throws Exception { + while( (AopUtils.isJdkDynamicProxy(proxy))) { + return clazz.cast(getTargetObject(((Advised)proxy).getTargetSource().getTarget(), clazz)); + } + + return clazz.cast(proxy); + } +} diff --git a/hapi-fhir-jpaserver-example/pom.xml b/hapi-fhir-jpaserver-example/pom.xml index cce1837ccfe..29ad78582b8 100644 --- a/hapi-fhir-jpaserver-example/pom.xml +++ b/hapi-fhir-jpaserver-example/pom.xml @@ -30,6 +30,28 @@ + + org.eclipse.jetty.websocket + websocket-api + ${jetty_version} + + + org.eclipse.jetty.websocket + websocket-client + ${jetty_version} + + + mysql + mysql-connector-java + 6.0.5 + + @@ -93,6 +115,18 @@ thymeleaf + + + org.ebaysf.web + cors-filter + + + servlet-api + javax.servlet + + + + org.springframework 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 new file mode 100644 index 00000000000..f42b73b3834 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3WSocket.java @@ -0,0 +1,233 @@ +/* + * 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 new file mode 100644 index 00000000000..0a82d56d092 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigWSocket.java @@ -0,0 +1,217 @@ +/* + * 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/JpaServerDemoDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java new file mode 100644 index 00000000000..92cc71dfd04 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java @@ -0,0 +1,190 @@ +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 new file mode 100644 index 00000000000..3ae5f4395da --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu3.java @@ -0,0 +1,184 @@ +/* + * 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-dstu2.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu2.xml new file mode 100644 index 00000000000..2bd24f980a9 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu2.xml @@ -0,0 +1,108 @@ + + + + org.springframework.web.context.ContextLoaderListener + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + contextConfigLocation + + ca.uhn.fhir.jpa.demo.FhirServerConfigWSocket + + + + + + + spring + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + ca.uhn.fhir.jpa.demo.FhirTesterConfig + + 2 + + + + fhirServlet + ca.uhn.fhir.jpa.demo.JpaServerDemoDstu2 + + ImplementationDescription + FHIR JPA Server + + + FhirVersion + DSTU2 + + 1 + + + + fhirServlet + /baseDstu2/* + + + + spring + / + + + + + + + CORS Filter + org.ebaysf.web.cors.CORSFilter + + A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. + cors.allowed.origins + * + + + A comma separated list of HTTP verbs, using which a CORS request can be made. + cors.allowed.methods + GET,POST,PUT,DELETE,OPTIONS + + + A comma separated list of allowed headers when making a non simple CORS request. + cors.allowed.headers + X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers + + + A comma separated list non-standard response headers that will be exposed to XHR2 object. + cors.exposed.headers + Location,Content-Location + + + A flag that suggests if CORS is supported with cookies + cors.support.credentials + true + + + A flag to control logging + cors.logging.enabled + true + + + Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. + cors.preflight.maxage + 300 + + + + CORS Filter + /* + + + + 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 new file mode 100644 index 00000000000..8de1fe1e02a --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml @@ -0,0 +1,108 @@ + + + + org.springframework.web.context.ContextLoaderListener + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + contextConfigLocation + + ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3WSocket + + + + + + + spring + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + ca.uhn.fhir.jpa.demo.FhirTesterConfigDstu3 + + 2 + + + + fhirServlet + ca.uhn.fhir.jpa.demo.JpaServerDemoDstu3 + + ImplementationDescription + FHIR JPA Server + + + FhirVersion + DSTU3 + + 1 + + + + fhirServlet + /baseDstu3/* + + + + spring + / + + + + + + + CORS Filter + org.ebaysf.web.cors.CORSFilter + + A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. + cors.allowed.origins + * + + + A comma separated list of HTTP verbs, using which a CORS request can be made. + cors.allowed.methods + GET,POST,PUT,DELETE,OPTIONS + + + A comma separated list of allowed headers when making a non simple CORS request. + cors.allowed.headers + X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers + + + A comma separated list non-standard response headers that will be exposed to XHR2 object. + cors.exposed.headers + Location,Content-Location + + + A flag that suggests if CORS is supported with cookies + cors.support.credentials + true + + + A flag to control logging + cors.logging.enabled + true + + + Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. + cors.preflight.maxage + 300 + + + + CORS Filter + /* + + + + diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml new file mode 100644 index 00000000000..c81a6f602d0 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml @@ -0,0 +1,108 @@ + + + + org.springframework.web.context.ContextLoaderListener + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + contextConfigLocation + + ca.uhn.fhir.jpa.demo.FhirServerConfig + + + + + + + spring + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + ca.uhn.fhir.jpa.demo.FhirTesterConfig + + 2 + + + + fhirServlet + ca.uhn.fhir.jpa.demo.JpaServerDemo + + ImplementationDescription + FHIR JPA Server + + + FhirVersion + DSTU2 + + 1 + + + + fhirServlet + /baseDstu2/* + + + + spring + / + + + + + + + CORS Filter + org.ebaysf.web.cors.CORSFilter + + A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. + cors.allowed.origins + * + + + A comma separated list of HTTP verbs, using which a CORS request can be made. + cors.allowed.methods + GET,POST,PUT,DELETE,OPTIONS + + + A comma separated list of allowed headers when making a non simple CORS request. + cors.allowed.headers + X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers + + + A comma separated list non-standard response headers that will be exposed to XHR2 object. + cors.exposed.headers + Location,Content-Location + + + A flag that suggests if CORS is supported with cookies + cors.support.credentials + true + + + A flag to control logging + cors.logging.enabled + true + + + Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. + cors.preflight.maxage + 300 + + + + CORS Filter + /* + + + + 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 fdfc4ce3545..1bde5e34fcf 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 @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd"> org.springframework.web.context.ContextLoaderListener @@ -13,7 +13,7 @@ contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3 + ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3WSocket @@ -35,7 +35,7 @@ fhirServlet - ca.uhn.fhir.jpa.demo.JpaServerDemo + ca.uhn.fhir.jpa.demo.JpaServerDemoDstu3 ImplementationDescription FHIR JPA Server @@ -57,4 +57,52 @@ / + + + + + CORS Filter + org.ebaysf.web.cors.CORSFilter + + A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. + cors.allowed.origins + * + + + A comma separated list of HTTP verbs, using which a CORS request can be made. + cors.allowed.methods + GET,POST,PUT,DELETE,OPTIONS + + + A comma separated list of allowed headers when making a non simple CORS request. + cors.allowed.headers + X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers + + + A comma separated list non-standard response headers that will be exposed to XHR2 object. + cors.exposed.headers + Location,Content-Location + + + A flag that suggests if CORS is supported with cookies + cors.support.credentials + true + + + A flag to control logging + cors.logging.enabled + true + + + Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. + cors.preflight.maxage + 300 + + + + CORS Filter + /* + + + diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu2Util.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu2Util.java new file mode 100644 index 00000000000..dc294f517fc --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu2Util.java @@ -0,0 +1,111 @@ +/* + * 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.CodingDt; +import ca.uhn.fhir.model.dstu2.composite.IdentifierDt; +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.valueset.AdministrativeGenderEnum; +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 org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseMetaType; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class FhirDstu2Util { + + public static final String LPI_CODESYSTEM = "http://cognitivemedicine.com/lpi"; + public static final String LPI_CODE = "LPI-FHIR"; + + public static 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(SubscriptionStatusEnum.REQUESTED); + subscription.setCriteria(criteria); + + Subscription.Channel channel = new Subscription.Channel(); + channel.setType(SubscriptionChannelTypeEnum.REST_HOOK); + channel.setPayload(payload); + channel.setEndpoint(endpoint); + subscription.setChannel(channel); + + MethodOutcome methodOutcome = client.create().resource(subscription).execute(); + subscription.setId(methodOutcome.getId().getIdPart()); + + return subscription; + } + + public static Observation getSnomedObservation() { + CodingDt snomedCoding = new CodingDt(); + snomedCoding.setSystem("SNOMED-CT"); + snomedCoding.setCode("1000000050"); + + Observation observation = new Observation(); + + observation.setStatus(ObservationStatusEnum.FINAL); + observation.getCode().addCoding(snomedCoding); + + return observation; + } + + public static Observation getLoincObservation() { + CodingDt snomedCoding = new CodingDt(); + snomedCoding.setSystem("http://loinc.org"); + snomedCoding.setCode("55284-4"); + snomedCoding.setDisplay("Blood Pressure"); + + Observation observation = new Observation(); + + observation.setStatus(ObservationStatusEnum.FINAL); + observation.getCode().addCoding(snomedCoding); + + return observation; + } + + public static Patient getPatient() { + String patientId = "1"; + + Patient patient = new Patient(); + patient.setGender(AdministrativeGenderEnum.MALE); + + IdentifierDt identifier = patient.addIdentifier(); + identifier.setValue(patientId); + identifier.setSystem(LPI_CODESYSTEM); + + IBaseMetaType meta = patient.getMeta(); + IBaseCoding tag = meta.addTag(); + tag.setCode(LPI_CODE); + tag.setSystem(LPI_CODESYSTEM); + + setTag(patient); + + return patient; + } + + public static void setTag(IBaseResource resource) { + IBaseMetaType meta = resource.getMeta(); + IBaseCoding tag = meta.addTag(); + tag.setCode(LPI_CODE); + tag.setSystem(LPI_CODESYSTEM); + } +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu3Util.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu3Util.java new file mode 100644 index 00000000000..6b608645c5b --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirDstu3Util.java @@ -0,0 +1,112 @@ +/* + * 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 org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseMetaType; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class FhirDstu3Util { + + public static final String LPI_CODESYSTEM = "http://cognitivemedicine.com/lpi"; + public static final String LPI_CODE = "LPI-FHIR"; + + public static 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 static Observation getSnomedObservation() { + Coding snomedCoding = new Coding(); + snomedCoding.setSystem("SNOMED-CT"); + snomedCoding.setCode("1000000050"); + + Observation observation = new Observation(); + + observation.setStatus(Observation.ObservationStatus.FINAL); + observation.getCode().addCoding(snomedCoding); + + return observation; + } + + public static Observation getLoincObservation() { + Coding snomedCoding = new Coding(); + snomedCoding.setSystem("http://loinc.org"); + snomedCoding.setCode("55284-4"); + snomedCoding.setDisplay("Blood Pressure"); + + Observation observation = new Observation(); + + observation.setStatus(Observation.ObservationStatus.FINAL); + observation.getCode().addCoding(snomedCoding); + + return observation; + } + + /** + * Create a patient object for the test + * + * @return + */ + public static Patient getPatient() { + String patientId = "1"; + + Patient patient = new Patient(); + patient.setGender(Enumerations.AdministrativeGender.MALE); + + Identifier identifier = patient.addIdentifier(); + identifier.setValue(patientId); + identifier.setSystem(LPI_CODESYSTEM); + + IBaseMetaType meta = patient.getMeta(); + IBaseCoding tag = meta.addTag(); + tag.setCode(LPI_CODE); + tag.setSystem(LPI_CODESYSTEM); + setTag(patient); + return patient; + } + + /** + * Set the tag for a resource + * + * @param resource + */ + public static void setTag(IBaseResource resource) { + IBaseMetaType meta = resource.getMeta(); + IBaseCoding tag = meta.addTag(); + tag.setCode(LPI_CODE); + tag.setSystem(LPI_CODESYSTEM); + } + +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirServiceUtil.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirServiceUtil.java new file mode 100644 index 00000000000..9accc677f7b --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirServiceUtil.java @@ -0,0 +1,60 @@ +/* + * 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.context.FhirContext; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.client.IGenericClient; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class FhirServiceUtil { + + public static final String FHIR_DSTU3_URL = "http://localhost:9093/baseDstu3"; + public static final String FHIR_DSTU2_URL = "http://localhost:9092/baseDstu2"; + public static final String JSON_PAYLOAD = "application/json"; + public static final String XML_PAYLOAD = "application/xml"; + public static final String REST_HOOK_ENDPOINT = "http://localhost:10080/rest-hook"; + + public static IGenericClient getFhirDstu3Client() { + FhirContext ctx = FhirContext.forDstu3(); + return ctx.newRestfulGenericClient(FHIR_DSTU3_URL); + } + + public static IGenericClient getFhirDstu2Client() { + FhirContext ctx = FhirContext.forDstu2(); + return ctx.newRestfulGenericClient(FHIR_DSTU2_URL); + } + + public static String createResource(IBaseResource resource, IGenericClient client) { + MethodOutcome response = client.create().resource(resource).execute(); + resource.setId(response.getId()); + + return response.getId().getIdPart(); + } + + public static String updateResource(IBaseResource resource, IGenericClient client) { + MethodOutcome response = client.update().resource(resource).execute(); + return response.getId().getIdPart(); + } + + public static void deleteResource(String id, Class clazz, IGenericClient client) { + client.delete().resourceById(clazz.getSimpleName(), id).execute(); + } + + +} 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 new file mode 100644 index 00000000000..13075ed4228 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu2IT.java @@ -0,0 +1,133 @@ +/* + * 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 new file mode 100644 index 00000000000..88b7ba92dfc --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithSubscriptionIdDstu3IT.java @@ -0,0 +1,173 @@ +/* + * 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 new file mode 100644 index 00000000000..3eee21bd809 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu2IT.java @@ -0,0 +1,103 @@ +/* + * 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 new file mode 100644 index 00000000000..400a8ec1dc7 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/FhirSubscriptionWithWebsocketCriteriaDstu3IT.java @@ -0,0 +1,114 @@ +/* + * 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/RemoveDstu2TestIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu2TestIT.java new file mode 100644 index 00000000000..e082dd81fb2 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu2TestIT.java @@ -0,0 +1,100 @@ +/* + * 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.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.gclient.IQuery; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@Ignore +public class RemoveDstu2TestIT { + + private static Logger logger = LoggerFactory.getLogger(RemoveDstu2TestIT.class); + public static final int NUM_TO_DELETE_PER_QUERY = 100; + + @Test + public void remove() { + IGenericClient client = FhirServiceUtil.getFhirDstu2Client(); + deleteResources(Subscription.class, null, client); + deleteResources(Observation.class, null, client); + Bundle bundle = searchResources(Observation.class, null, NUM_TO_DELETE_PER_QUERY, client); + Assert.assertNotNull(bundle); + List entry = bundle.getEntry(); + Assert.assertTrue(entry.isEmpty()); + } + + /** + * Delete resources from specified class and tag + * + * @param clazz + * @param tag + * @param + */ + public static void deleteResources(Class clazz, IBaseCoding tag, IGenericClient client) { + Bundle bundle = searchResources(clazz, tag, NUM_TO_DELETE_PER_QUERY, client); + List bundleEntryComponents = bundle.getEntry(); + + while (bundleEntryComponents.size() > 0) { + for (Bundle.Entry bundleEntryComponent : bundleEntryComponents) { + IBaseResource resource = bundleEntryComponent.getResource(); + String id = resource.getIdElement().getIdPart(); + String className = clazz.getSimpleName(); + + logger.info("deleting resource------------------------------------------>" + className + "/" + id); + + client.delete().resourceById(className, id).execute(); + } + bundle = searchResources(clazz, tag, NUM_TO_DELETE_PER_QUERY, client); + bundleEntryComponents = bundle.getEntry(); + } + } + + /** + * Get resources from specified class and tag + * + * @param clazz + * @param tag + * @param limit + * @param + * @return + */ + public static Bundle searchResources(Class clazz, IBaseCoding tag, Integer limit, IGenericClient client) { + IQuery iquery = client.search().forResource(clazz); + + if (tag != null) { + iquery.withTag(tag.getSystem(), tag.getCode()); + } + + if (limit != null) { + iquery.count(limit); + } + + return (Bundle) iquery.returnBundle(Bundle.class).execute(); + } +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu3TestIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu3TestIT.java new file mode 100644 index 00000000000..34e556d3523 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RemoveDstu3TestIT.java @@ -0,0 +1,108 @@ +/* + * 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.client.IGenericClient; +import ca.uhn.fhir.rest.gclient.IQuery; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Subscription; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@Ignore +public class RemoveDstu3TestIT { + + private static Logger logger = LoggerFactory.getLogger(RemoveDstu3TestIT.class); + public static final int NUM_TO_DELETE_PER_QUERY = 10000; + + @Test + public void remove() { + IGenericClient client = FhirServiceUtil.getFhirDstu3Client(); + deleteResources(Subscription.class, null, client); + deleteResources(Observation.class, null, client); +/* try { + //wait for cache to clear + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } +*/ + Bundle bundle = searchResources(Observation.class, null, NUM_TO_DELETE_PER_QUERY, client); + Assert.assertNotNull(bundle); + List entry = bundle.getEntry(); + Assert.assertTrue(entry.isEmpty()); + } + + /** + * Delete resources from specified class and tag + * + * @param clazz + * @param tag + * @param + */ + public static void deleteResources(Class clazz, IBaseCoding tag, IGenericClient client) { + Bundle bundle = searchResources(clazz, tag, NUM_TO_DELETE_PER_QUERY, client); + List bundleEntryComponents = bundle.getEntry(); + +// while (bundleEntryComponents.size() > 0) { + for (Bundle.BundleEntryComponent bundleEntryComponent : bundleEntryComponents) { + IBaseResource resource = bundleEntryComponent.getResource(); + String id = resource.getIdElement().getIdPart(); + String className = clazz.getSimpleName(); + + logger.info("deleting resource------------------------------------------>" + className + "/" + id); + + client.delete().resourceById(className, id).execute(); + } +// currently loops forever due to the FHIR server using a cached query result +// bundle = searchResources(clazz, tag, NUM_TO_DELETE_PER_QUERY, client); +// bundleEntryComponents = bundle.getEntry(); +// } + } + + /** + * Get resources from specified class and tag + * + * @param clazz + * @param tag + * @param limit + * @param + * @return + */ + public static Bundle searchResources(Class clazz, IBaseCoding tag, Integer limit, IGenericClient client) { + IQuery iquery = client.search().forResource(clazz); + + if (tag != null) { + iquery.withTag(tag.getSystem(), tag.getCode()); + } + + if (limit != null) { + iquery.count(limit); + } + + return (Bundle) iquery.returnBundle(Bundle.class).execute(); + } +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu2IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu2IT.java new file mode 100644 index 00000000000..336326a35ac --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu2IT.java @@ -0,0 +1,147 @@ +/* + * 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 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 RestHookTestDstu2IT { + + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3IT.class); + private static String code = "1000000012"; + private IGenericClient client = FhirServiceUtil.getFhirDstu2Client(); + + @Before + public void clean() { + RemoveDstu2TestIT.deleteResources(Subscription.class, null, client); + RemoveDstu2TestIT.deleteResources(Observation.class, null, client); + } + + @Test + public void testRestHookSubscription() { + String payload = "application/json"; + String endpoint = "http://localhost:10080/rest-hook"; + + 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 observationTemp1 = sendObservation(code, "SNOMED-CT", client); + Observation observation1 = client.read(Observation.class, observationTemp1.getId()); + //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 observationTemp2 = sendObservation(code, "SNOMED-CT", client); + Observation observation2 = client.read(Observation.class, observationTemp2.getId()); + //Should see two subscription notifications + + client.delete().resourceById("Subscription", subscription2.getId().getIdPart()).execute(); + + Observation observationTemp3 = sendObservation(code, "SNOMED-CT", client); + //Should see only one subscription notification + + Observation observation3 = client.read(Observation.class, observationTemp1.getId()); + CodeableConceptDt codeableConcept = new CodeableConceptDt(); + observation3.setCode(codeableConcept); + CodingDt 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, observationTemp1.getId()); + CodeableConceptDt codeableConcept2 = new CodeableConceptDt(); + observation3a.setCode(codeableConcept2); + CodingDt coding2 = codeableConcept2.addCoding(); + coding2.setCode(code); + coding2.setSystem("SNOMED-CT"); + client.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); + //Should see only one subscription notification + + System.out.println("subscription id 1: " + subscription1.getId()); + System.out.println("subscription id 2: " + subscription2.getId()); + System.out.println("subscription temp id 2: " + subscriptionTemp.getId()); + System.out.println("observation id 1: " + observation1.getId()); + System.out.println("observation id 2: " + observation2.getId()); + System.out.println("observation id 3: " + observation3.getId()); + + 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(SubscriptionStatusEnum.REQUESTED); + subscription.setCriteria(criteria); + + Subscription.Channel channel = new Subscription.Channel(); + channel.setType(SubscriptionChannelTypeEnum.REST_HOOK); + 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(); + CodeableConceptDt codeableConcept = new CodeableConceptDt(); + observation.setCode(codeableConcept); + CodingDt coding = codeableConcept.addCoding(); + coding.setCode(code); + coding.setSystem(system); + + observation.setStatus(ObservationStatusEnum.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/RestHookTestDstu3IT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3IT.java new file mode 100644 index 00000000000..ed15bfd190a --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3IT.java @@ -0,0 +1,139 @@ +/* + * 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/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java new file mode 100644 index 00000000000..23b0419546b --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/RestHookTestDstu3WithSubscriptionResponseCriteriaIT.java @@ -0,0 +1,130 @@ +/* + * 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.Ignore; +import org.junit.Test; +import org.slf4j.Logger; + +/** + * Test the rest-hook subscriptions + */ +@Ignore +public class RestHookTestDstu3WithSubscriptionResponseCriteriaIT { + + private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSubscriptionWithSubscriptionIdDstu3IT.class); + + @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, "Observation?_format=xml", endpoint, client); + Subscription subscription2 = createSubscription(criteria2, payload, endpoint, client); + + Observation observation1 = sendObservation(code, "SNOMED-CT", client); + //Should see a bundle + + 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/ResthookSubscriptionDstu2TestsIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu2TestsIT.java new file mode 100644 index 00000000000..3e5288200dc --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu2TestsIT.java @@ -0,0 +1,181 @@ +/* + * 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.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.rest.client.IGenericClient; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Must have a fhir server and web service endpoint to run these tests which subscribe to the fhir and receive notifications + */ +@Ignore +public class ResthookSubscriptionDstu2TestsIT { + + private static IGenericClient client; + + @BeforeClass + public static void init() { + client = FhirServiceUtil.getFhirDstu2Client(); + } + + @Before + public void clean() { + RemoveDstu2TestIT.deleteResources(Subscription.class, null, client); + RemoveDstu2TestIT.deleteResources(Observation.class, null, client); + } + + @Test + public void testSubscriptionsWithoutPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + + Observation loincObservation = FhirDstu2Util.getLoincObservation(); + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(loincObservation, client); //should not trigger a notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + + snomedObservation.setComments("mock change"); + + FhirServiceUtil.updateResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.deleteResource(snomedObservation.getIdElement().getIdPart(), Observation.class, client); //should trigger one notification + } + + @Test + public void testSubscriptionsWithXmlPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + + FhirDstu2Util.createSubscription(criteria, FhirServiceUtil.XML_PAYLOAD, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with xml resource in the body + + snomedObservation.setComments("mock change"); + + FhirServiceUtil.updateResource(snomedObservation, client); //should trigger one notification with xml resource in the body + FhirServiceUtil.deleteResource(snomedObservation.getIdElement().getIdPart(), Observation.class, client); //should trigger one notification with xml resource in the body + } + + @Test + public void testSubscriptionsWithJsonPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + FhirServiceUtil.createResource(snomedObservation, client); + FhirServiceUtil.createResource(snomedObservation, client); + + FhirDstu2Util.createSubscription(criteria, FhirServiceUtil.JSON_PAYLOAD, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with json resource in the body + } + + @Test + public void testSubscriptionsWithCustomXmlPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria + "&_format=xml"; + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + FhirServiceUtil.createResource(snomedObservation, client); + FhirServiceUtil.createResource(snomedObservation, client); + + FhirDstu2Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with xml bundle resource in the body containing three observations + } + + @Test + public void testSubscriptionsWithCustomJsonPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria + "&_format=json"; + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + FhirServiceUtil.createResource(snomedObservation, client); + + FhirDstu2Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with JSON bundle resource in the body containing two observations + } + + @Test + public void testSubscriptionsWithCustomDefaultPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria; + + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + + FhirDstu2Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with JSON bundle resource in the body containing one observations + } + + @Test + public void testSubscriptionsWithCustomDefaultPayloadThatIsEmpty() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/Observation?code=SNOMED-CT|" + code + "1111"; + + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + + FhirDstu2Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with JSON bundle resource in the body containing no observations + } + + /** + * Add a 5 second delay to the HttpRequestDstu3Job to test if threading is improving creation speed + */ + @Test + public void testSubscriptionsThreading() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + System.out.println("start"); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + System.out.println("done"); + } + + /** + * Add a 5 second delay to the HttpRequestDstu3Job to test if threading is improving creation speed + */ + @Test + public void testSubscriptionsThreading2() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + Observation snomedObservation = FhirDstu2Util.getSnomedObservation(); + + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu2Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + + System.out.println("start"); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + System.out.println("done"); + } +} diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu3TestsIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu3TestsIT.java new file mode 100644 index 00000000000..b143a61fd36 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/ResthookSubscriptionDstu3TestsIT.java @@ -0,0 +1,227 @@ +/* + * 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.client.IGenericClient; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Subscription; +import org.junit.*; + +/** + * Must have a fhir server and web service endpoint to run these + * tests which subscribe to the fhir and receive notifications + */ +@Ignore +public class ResthookSubscriptionDstu3TestsIT { + + private static IGenericClient client; + + @BeforeClass + public static void init() { + client = FhirServiceUtil.getFhirDstu3Client(); + } + + //@Before + //@Test + public void clean() { + RemoveDstu3TestIT.deleteResources(Subscription.class, null, client); + RemoveDstu3TestIT.deleteResources(Observation.class, null, client); + } + + @Test + public void createSnomedObservation() { + String id = FhirServiceUtil.createResource(FhirDstu3Util.getSnomedObservation(), client); + FhirServiceUtil.deleteResource(id, Observation.class, client); + } + + @Test + public void testSubscriptionsWithoutPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + + Observation loincObservation = FhirDstu3Util.getLoincObservation(); + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + +// FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(loincObservation, client); //should not trigger a notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + + snomedObservation.setComment("mock change"); + + FhirServiceUtil.updateResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.deleteResource(snomedObservation.getIdElement().getIdPart(), Observation.class, client); //should trigger one notification + } + + @Test + public void testSubscriptionsWithXmlPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + + FhirDstu3Util.createSubscription(criteria, FhirServiceUtil.XML_PAYLOAD, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with xml resource in the body + + snomedObservation.setComment("mock change"); + + FhirServiceUtil.updateResource(snomedObservation, client); //should trigger one notification with xml resource in the body + FhirServiceUtil.deleteResource(snomedObservation.getIdElement().getIdPart(), Observation.class, client); //should trigger one notification with xml resource in the body + } + + @Test + public void testSubscriptionsWithJsonPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + FhirServiceUtil.createResource(snomedObservation, client); + FhirServiceUtil.createResource(snomedObservation, client); + + FhirDstu3Util.createSubscription(criteria, FhirServiceUtil.JSON_PAYLOAD, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with json resource in the body + } + + @Test + public void testSubscriptionsWithCustomXmlPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria + "&_format=xml"; + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + FhirServiceUtil.createResource(snomedObservation, client); + FhirServiceUtil.createResource(snomedObservation, client); + + FhirDstu3Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with xml bundle resource in the body containing three observations + } + + @Test + public void testSubscriptionsWithCustomJsonPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria + "&_format=json"; + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + FhirServiceUtil.createResource(snomedObservation, client); + + FhirDstu3Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with JSON bundle resource in the body containing two observations + } + + @Test + public void testSubscriptionsWithCustomDefaultPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria; + + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + + FhirDstu3Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with JSON bundle resource in the body containing one observations + } + + @Test + public void testSubscriptionsWithCustomDefaultPayloadThatIsEmpty() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/Observation?code=SNOMED-CT|" + code + "1111"; + + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + + FhirDstu3Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with JSON bundle resource in the body containing no observations + } + + /** + * Add a 5 second delay to the HttpRequestDstu3Job to test if threading is improving creation speed + */ + @Test + public void testSubscriptionsThreading() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + + FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + System.out.println("start"); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + System.out.println("done"); + } + + /** + * Add a 5 second delay to the HttpRequestDstu3Job to test if threading is improving creation speed + */ + @Test + public void testSubscriptionsThreading2() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + + FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + FhirDstu3Util.createSubscription(criteria, null, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + + System.out.println("start"); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification + System.out.println("done"); + } + + @Test + public void testSubscriptionsWithTMinusPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria + "&date=Tminus100s" + "&_format=xml"; + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + DateTimeType dateTimeType = DateTimeType.now(); + dateTimeType.setYear(2017); + dateTimeType.setMonth(2); + dateTimeType.setDay(1); + System.out.println(dateTimeType.getValueAsString()); + snomedObservation.setEffective(dateTimeType); + FhirServiceUtil.createResource(snomedObservation, client); + snomedObservation.setEffective(DateTimeType.now()); + FhirServiceUtil.createResource(snomedObservation, client); + FhirDstu3Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + snomedObservation.setEffective(DateTimeType.now()); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with xml bundle resource in the body containing two observations + } + + @Test + public void testSubscriptionsWith2TMinusPayload() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = "application/fhir+query/" + criteria + "&date=Tminus2m" + "&_lastUpdated=Tminus20s" + "&_format=xml"; + Observation snomedObservation = FhirDstu3Util.getSnomedObservation(); + DateTimeType dateTimeType = DateTimeType.now(); + dateTimeType.setYear(2017); + dateTimeType.setMonth(2); + dateTimeType.setDay(1); + System.out.println(dateTimeType.getValueAsString()); + snomedObservation.setEffective(dateTimeType); + FhirServiceUtil.createResource(snomedObservation, client); + snomedObservation.setEffective(DateTimeType.now()); + FhirServiceUtil.createResource(snomedObservation, client); + FhirDstu3Util.createSubscription(criteria, payloadCriteria, FhirServiceUtil.REST_HOOK_ENDPOINT, client); + snomedObservation.setEffective(DateTimeType.now()); + FhirServiceUtil.createResource(snomedObservation, client); //should trigger one notification with xml bundle resource in the body containing two observations + } +} 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 new file mode 100644 index 00000000000..7b8ad26aaf5 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/SocketImplementation.java @@ -0,0 +1,98 @@ +/* + * 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 diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/TminusTest.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/TminusTest.java new file mode 100644 index 00000000000..0650cd6a7da --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/subscription/TminusTest.java @@ -0,0 +1,181 @@ +/* + * 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.jpa.service.TMinusService; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TminusTest { + + @Test + public void testDays() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&effectiveDate=Tminus10d" + "&noEffectiveDate=Tminus1d" + "&_format=xml"; + System.out.println(payloadCriteria); + System.out.println("Tminus10d with the current datetime - 10d"); + payloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(!payloadCriteria.contains("Tminus")); + } + + @Test + public void testSeconds() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&date=Tminus200s" + "&_format=xml"; + System.out.println(payloadCriteria); + System.out.println("replace Tminus200s with the current datetime - 200s"); + payloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(!payloadCriteria.contains("Tminus")); + } + + @Test + public void testMinutes() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&effectiveDate=Tminus1m" + "&_format=xml"; + System.out.println(payloadCriteria); + System.out.println("replace Tminus1m with the current datetime - 1m"); + payloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(!payloadCriteria.contains("Tminus")); + } + + @Test + public void testMinutesAtTheEnd() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&date=Tminus1m"; + System.out.println(payloadCriteria); + System.out.println("replace Tminus1m with the current datetime - 1m"); + payloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(!payloadCriteria.contains("Tminus")); + } + + @Test + public void testWithoutTminus() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria; + System.out.println(payloadCriteria); + System.out.println("test without a Tminus"); + String newPayloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(!payloadCriteria.contains("Tminus")); + Assert.assertTrue(payloadCriteria.equals(newPayloadCriteria)); + } + + @Test + public void testWithoutTminusUnits() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&date=Tminus1"; + System.out.println(payloadCriteria); + System.out.println("check Tminus without units"); + String newPayloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(payloadCriteria.contains("Tminus")); + Assert.assertTrue(payloadCriteria.equals(newPayloadCriteria)); + } + + @Test + public void testWithoutTminusValue() { + String code = "1000000050"; + String criteria = "Observation?code=SNOMED-CT|" + code; + String payloadCriteria = criteria + "&date=Tminusm"; + System.out.println(payloadCriteria); + System.out.println("check Tminus without a value"); + String newPayloadCriteria = TMinusService.parseCriteria(payloadCriteria); + System.out.println(payloadCriteria); + Assert.assertTrue(payloadCriteria.contains("Tminus")); + Assert.assertTrue(payloadCriteria.equals(newPayloadCriteria)); + } + + @Test + public void parsingCriteria() { + String criteria = "Observation?code=SNOMED-CT|1000000050&date=Tminus1d&_format=xml"; + //String criteria = "Observation?code=SNOMED-CT|1000000050&date=%3E%3D2017-03-05T12:55:02-08:00&_format=xml"; + + String TMINUS = "Tminus"; + String WEEK = "w"; + String DAY = "d"; + String HOUR = "h"; + String MINUTE = "m"; + String SECOND = "s"; + + long MINUTE_AS_MILLIS = 60 * 1000; + long HOUR_AS_MILLIS = 60 * 60 * 1000; + long DAY_AS_MILLIS = 24 * 60 * 60 * 1000; + long WEEK_AS_MILLIS = 7 * 24 * 60 * 60 * 1000; + + Pattern pattern = Pattern.compile("Tminus([0-9]+)([wdhms])"); + Matcher matcher = pattern.matcher(criteria); + + if (matcher.find()) { + String tMinus = matcher.group(); + String tMinusValue = tMinus.substring(TMINUS.length(), tMinus.length() - 1); + String tMinusPeriod = tMinus.substring(tMinus.length() - 1); + + long tMinusLongValue = Long.parseLong(tMinusValue); + long tMinusMillis = 0L; + + if (WEEK.equals(tMinusPeriod)) { + tMinusMillis = WEEK_AS_MILLIS * tMinusLongValue; + } else if (DAY.equals(tMinusPeriod)) { + tMinusMillis = DAY_AS_MILLIS * tMinusLongValue; + } else if (HOUR.equals(tMinusPeriod)) { + tMinusMillis = HOUR_AS_MILLIS * tMinusLongValue; + } else if (MINUTE.equals(tMinusPeriod)) { + tMinusMillis = MINUTE_AS_MILLIS * tMinusLongValue; + } else if (SECOND.equals(tMinusPeriod)) { + tMinusMillis = 1000 * tMinusLongValue; + } else { + throw new IllegalArgumentException("Period not recognized: " + tMinusPeriod); + } + + Date currentDate = new Date(); + Date lowerDate = new Date(currentDate.getTime() - tMinusMillis); + + DateTimeType lowerDateTimeType = new DateTimeType(lowerDate); + DateTimeType currentDateTimeType = new DateTimeType(currentDate); + + String dateValue = "%3E%3D" + lowerDateTimeType.getValueAsString() + "&date=%3C%3D" + currentDateTimeType.getValueAsString(); + String formattedCriteria = criteria.replace(tMinus, dateValue); + + System.out.println(tMinus); + System.out.println(tMinusValue); + System.out.println(tMinusPeriod); + System.out.println(tMinusMillis); + System.out.println(currentDate); + System.out.println(lowerDate); + System.out.println(lowerDateTimeType.getValueAsString()); + System.out.println(formattedCriteria); + } else { + System.out.println("nothing"); + } + } +} diff --git a/pom.xml b/pom.xml index 5e1d82b8ad8..e50010cc0de 100644 --- a/pom.xml +++ b/pom.xml @@ -370,6 +370,7 @@ 1.6.0 UTF-8 + 1.0.1 @@ -644,6 +645,11 @@ woodstox-core-asl 4.4.1 + + org.ebaysf.web + cors-filter + ${ebay_cors_filter_version} + org.eclipse.jetty jetty-http