Resthook subscription implementation, event driven websocket subscription implementation, Tminus subscription support, notification on delete

This commit is contained in:
Jeff Chung 2017-05-09 15:08:59 -07:00
parent e53d747f2b
commit 663125fe94
50 changed files with 5630 additions and 14 deletions

View File

@ -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<RuntimeSearchParam> mySearchParams;
private final FhirVersionEnum myStructureVersion;
private volatile RuntimeResourceDefinition myBaseDefinition;
public RuntimeResourceDefinition(FhirContext theContext, String theResourceName, Class<? extends IBaseResource> theClass, ResourceDef theResourceAnnotation, boolean theStandardType, Map<Class<? extends IBase>, 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);
}

View File

@ -14,6 +14,28 @@
<name>HAPI FHIR JPA Server</name>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate4</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>9.5.1-5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
@ -363,6 +385,7 @@
<properties>
<skip-hib4>false</skip-hib4>
<jackson.version>2.7.1</jackson.version>
</properties>
<build>

View File

@ -886,6 +886,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (theRequestDetails.isSubRequest()) {
theParams.setLoadSynchronous(true);
int max = myDaoConfig.getMaximumSearchResultCountInTransaction();
theParams.setLoadSynchronousUpTo(myDaoConfig.getMaximumSearchResultCountInTransaction());
}

View File

@ -180,7 +180,7 @@ public class DaoConfig {
@Deprecated
public List<IServerInterceptor> getInterceptors() {
if (myInterceptors == null) {
return Collections.emptyList();
myInterceptors = new ArrayList<IServerInterceptor>();
}
return myInterceptors;
}

View File

@ -41,6 +41,11 @@ import ca.uhn.fhir.util.FhirTerser;
public class FhirResourceDaoDstu1<T extends IResource> extends BaseHapiFhirResourceDao<T> {
@Override
public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) {
return null;
}
@Override
protected List<Object> getIncludeValues(FhirTerser t, Include next, IBaseResource nextResource, RuntimeResourceDefinition def) {
List<Object> values;

View File

@ -145,6 +145,11 @@ public class FhirResourceDaoDstu2<T extends IResource> extends BaseHapiFhirResou
}
@Override
public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) {
return null;
}
private class IdChecker implements IValidatorModule {
private ValidationModeEnum myMode;

View File

@ -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<Subsc
}
@Override
public int pollForNewUndeliveredResources() {
return pollForNewUndeliveredResources((String) null);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public synchronized int pollForNewUndeliveredResources() {
public synchronized int pollForNewUndeliveredResources(final String resourceType) {
if (getConfig().isSubscriptionEnabled() == false) {
return 0;
}
@ -141,7 +146,7 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
@Override
public Integer doInTransaction(TransactionStatus theStatus) {
SubscriptionTable nextSubscriptionTable = mySubscriptionTableDao.findOne(nextSubscriptionTablePid);
return pollForNewUndeliveredResources(nextSubscriptionTable);
return pollForNewUndeliveredResources(nextSubscriptionTable, resourceType);
}
});
}
@ -149,8 +154,20 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
return retVal;
}
private int pollForNewUndeliveredResources(SubscriptionTable theSubscriptionTable) {
private int pollForNewUndeliveredResources(SubscriptionTable theSubscriptionTable, String resourceType) {
Subscription subscription = toResource(Subscription.class, theSubscriptionTable.getSubscriptionResource(), false);
ourLog.info("subscription for " + resourceType + " with criteria " + subscription.getCriteria());
if (!subscription.getChannel().getType().equals(SubscriptionChannelTypeEnum.WEBSOCKET.getCode())){
ourLog.info("Skipping non web socket subscription");
return 0;
}
if (resourceType != null && subscription.getCriteria() != null && !subscription.getCriteria().startsWith(resourceType)) {
ourLog.info("Skipping subscription search for " + resourceType + " because it does not match the criteria " + subscription.getCriteria());
return 0;
}
RuntimeResourceDefinition resourceDef = validateCriteriaAndReturnResourceDefinition(subscription);
SearchParameterMap criteriaUrl = translateMatchUrl(this, getContext(), subscription.getCriteria(), resourceDef);
@ -297,7 +314,7 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
return retVal;
}
private RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
String query = theResource.getCriteria();
if (isBlank(query)) {
throw new UnprocessableEntityException("Subscription.criteria must be populated");

View File

@ -25,6 +25,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
@ -243,6 +244,10 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/
MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequestDetails);
RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria);
<R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) ;
// /**
// * Invoke the everything operation
// */

View File

@ -37,4 +37,5 @@ public interface IFhirResourceDaoSubscription<T extends IBaseResource> extends I
void pollForNewUndeliveredResourcesScheduler();
int pollForNewUndeliveredResources(String resourceType);
}

View File

@ -1474,7 +1474,8 @@ public class SearchBuilder implements ISearchBuilder {
public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> 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

View File

@ -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<T extends IAnyResource> 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<T extends IAnyResource> 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;

View File

@ -119,9 +119,13 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
return retVal;
}
public int pollForNewUndeliveredResources() {
return pollForNewUndeliveredResources((String) null);
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public synchronized int pollForNewUndeliveredResources() {
public synchronized int pollForNewUndeliveredResources(final String resourceType) {
if (getConfig().isSubscriptionEnabled() == false) {
return 0;
}
@ -145,7 +149,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
@Override
public Integer doInTransaction(TransactionStatus theStatus) {
SubscriptionTable nextSubscriptionTable = mySubscriptionTableDao.findOne(nextSubscriptionTablePid);
return pollForNewUndeliveredResources(nextSubscriptionTable);
return pollForNewUndeliveredResources(nextSubscriptionTable, resourceType);
}
});
}
@ -153,8 +157,19 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
return retVal;
}
private int pollForNewUndeliveredResources(SubscriptionTable theSubscriptionTable) {
private int pollForNewUndeliveredResources(SubscriptionTable theSubscriptionTable, String resourceType) {
Subscription subscription = toResource(Subscription.class, theSubscriptionTable.getSubscriptionResource(), false);
if (subscription.getChannel().getType() != Subscription.SubscriptionChannelType.WEBSOCKET){
ourLog.info("Skipping non web socket subscription");
return 0;
}
ourLog.info("subscription for " + resourceType + " with criteria " + subscription.getCriteria());
if (resourceType != null && subscription.getCriteria() != null && !subscription.getCriteria().startsWith(resourceType)) {
ourLog.info("Skipping subscription search for " + resourceType + " because it does not match the criteria " + subscription.getCriteria());
return 0;
}
RuntimeResourceDefinition resourceDef = validateCriteriaAndReturnResourceDefinition(subscription);
SearchParameterMap criteriaUrl = translateMatchUrl(this, getContext(), subscription.getCriteria(), resourceDef);
@ -302,7 +317,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends FhirResourceDaoDstu3<Subsc
return retVal;
}
private RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
String query = theResource.getCriteria();
if (isBlank(query)) {
throw new UnprocessableEntityException("Subscription.criteria must be populated");

View File

@ -0,0 +1,429 @@
/*
* 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.FhirResourceDaoSubscriptionDstu2;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.thread.HttpRequestDstu2Job;
import ca.uhn.fhir.jpa.util.SpringObjectCaster;
import ca.uhn.fhir.model.api.IResource;
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.model.dstu2.valueset.SubscriptionChannelTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.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.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;
/**
* Adds the capability for DSTU2 rest-hook subscriptions. It is compatible with
* DSTU2 websocket subscriptions, but not with DSTU3 subscriptions
*/
public class RestHookSubscriptionDstu2Interceptor extends InterceptorAdapter implements IJpaServerInterceptor {
@Autowired
@Qualifier("mySubscriptionDaoDstu2")
private IFhirResourceDao<Subscription> mySubscriptionDao;
private static volatile ExecutorService executor;
private FhirResourceDaoSubscriptionDstu2 myResourceSubscriptionDao;
private static final Logger logger = LoggerFactory.getLogger(RestHookSubscriptionDstu2Interceptor.class);
private final List<Subscription> restHookSubscriptions = new ArrayList<Subscription>();
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<IBaseResource> 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 <code>false</code>
* 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<IBaseResource> 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<NameValuePair> 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<? extends IBaseResource> 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<IBaseResource> 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;
}
}

View File

@ -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<Subscription> mySubscriptionDao;
@Autowired
@Qualifier("myObservationDaoDstu3")
private IFhirResourceDao<Observation> myObservationDao;
private static volatile ExecutorService executor;
private static final Logger logger = LoggerFactory.getLogger(RestHookSubscriptionDstu3Interceptor.class);
private final List<Subscription> restHookSubscriptions = new ArrayList<Subscription>();
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<IBaseResource> 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 <code>false</code>
* 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<IBaseResource> 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<NameValuePair> 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<? extends IBaseResource> 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<IBaseResource> 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;
}
}

View File

@ -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<Subscription> reference;
private IFhirResourceDaoSubscription<Subscription> 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);
}
}

View File

@ -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<Subscription> reference;
private IFhirResourceDaoSubscription<Subscription> 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);
}
}

View File

@ -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;
}
}

View File

@ -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<Subscription> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<NameValuePair> 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<NameValuePair> 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<IBaseResource> 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<IBaseResource> theResults);
void handleTextMessage(WebSocketSession theSession, TextMessage theMessage);
}
}

View File

@ -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<Subscription> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<NameValuePair> 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<NameValuePair> 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<IBaseResource> 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<IBaseResource> theResults);
void handleTextMessage(WebSocketSession theSession, TextMessage theMessage);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,11 @@
package ca.uhn.fhir.jpa.util;
/**
* Created by Jeff on 2/8/2017.
*/
public enum MethodRequest {
POST,
GET,
PUT,
DELETE
}

View File

@ -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());
}
}

View File

@ -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 <T>
* @return
* @throws Exception
*/
public static <T> T getTargetObject(Object proxy, Class<T> clazz) throws Exception {
while( (AopUtils.isJdkDynamicProxy(proxy))) {
return clazz.cast(getTargetObject(((Advised)proxy).getTargetSource().getTarget(), clazz));
}
return clazz.cast(proxy);
}
}

View File

@ -30,6 +30,28 @@
</repositories>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<version>${jetty_version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${jetty_version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.5</version>
</dependency>
<!--
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
-->
<!-- This dependency includes the core HAPI-FHIR classes -->
<dependency>
@ -93,6 +115,18 @@
<artifactId>thymeleaf</artifactId>
</dependency>
<!-- Used for CORS support -->
<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Web is used to deploy the server to a web container. -->
<dependency>
<groupId>org.springframework</groupId>

View File

@ -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<IServerInterceptor> 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;
}
}

View File

@ -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<IServerInterceptor> 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();
}
}

View File

@ -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<IResourceProvider> 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<List<IResource>, 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<Bundle, MetaDt> 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<org.hl7.fhir.dstu3.model.Bundle, Meta> 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<IServerInterceptor> 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));
//}
}
}

View File

@ -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<IResourceProvider> 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<List<IResource>, 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<ca.uhn.fhir.model.dstu2.resource.Bundle, MetaDt> 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<Bundle, Meta> 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<IServerInterceptor> 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));
//}
}
}

View File

@ -0,0 +1,108 @@
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
ca.uhn.fhir.jpa.demo.FhirServerConfigWSocket
</param-value>
</context-param>
<!-- Servlets -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>ca.uhn.fhir.jpa.demo.FhirTesterConfig</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet>
<servlet-name>fhirServlet</servlet-name>
<servlet-class>ca.uhn.fhir.jpa.demo.JpaServerDemoDstu2</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>FHIR JPA Server</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>DSTU2</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>fhirServlet</servlet-name>
<url-pattern>/baseDstu2/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- This filters provide support for Cross Origin Resource Sharing (CORS) -->
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<description>A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials.</description>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<description>A comma separated list of HTTP verbs, using which a CORS request can be made.</description>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>A flag to control logging</description>
<param-name>cors.logging.enabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description>
<param-name>cors.preflight.maxage</param-name>
<param-value>300</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -0,0 +1,108 @@
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3WSocket
</param-value>
</context-param>
<!-- Servlets -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>ca.uhn.fhir.jpa.demo.FhirTesterConfigDstu3</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet>
<servlet-name>fhirServlet</servlet-name>
<servlet-class>ca.uhn.fhir.jpa.demo.JpaServerDemoDstu3</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>FHIR JPA Server</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>DSTU3</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>fhirServlet</servlet-name>
<url-pattern>/baseDstu3/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- This filters provide support for Cross Origin Resource Sharing (CORS) -->
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<description>A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials.</description>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<description>A comma separated list of HTTP verbs, using which a CORS request can be made.</description>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>A flag to control logging</description>
<param-name>cors.logging.enabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description>
<param-name>cors.preflight.maxage</param-name>
<param-value>300</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -0,0 +1,108 @@
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
ca.uhn.fhir.jpa.demo.FhirServerConfig
</param-value>
</context-param>
<!-- Servlets -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>ca.uhn.fhir.jpa.demo.FhirTesterConfig</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet>
<servlet-name>fhirServlet</servlet-name>
<servlet-class>ca.uhn.fhir.jpa.demo.JpaServerDemo</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>FHIR JPA Server</param-value>
</init-param>
<init-param>
<param-name>FhirVersion</param-name>
<param-value>DSTU2</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>fhirServlet</servlet-name>
<url-pattern>/baseDstu2/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- This filters provide support for Cross Origin Resource Sharing (CORS) -->
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<description>A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials.</description>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<description>A comma separated list of HTTP verbs, using which a CORS request can be made.</description>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>A flag to control logging</description>
<param-name>cors.logging.enabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description>
<param-name>cors.preflight.maxage</param-name>
<param-value>300</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -1,5 +1,5 @@
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd">
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
@ -13,7 +13,7 @@
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3
ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3WSocket
</param-value>
</context-param>
@ -35,7 +35,7 @@
<servlet>
<servlet-name>fhirServlet</servlet-name>
<servlet-class>ca.uhn.fhir.jpa.demo.JpaServerDemo</servlet-class>
<servlet-class>ca.uhn.fhir.jpa.demo.JpaServerDemoDstu3</servlet-class>
<init-param>
<param-name>ImplementationDescription</param-name>
<param-value>FHIR JPA Server</param-value>
@ -57,4 +57,52 @@
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- This filters provide support for Cross Origin Resource Sharing (CORS) -->
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<description>A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials.</description>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<description>A comma separated list of HTTP verbs, using which a CORS request can be made.</description>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value>Location,Content-Location</param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>A flag to control logging</description>
<param-name>cors.logging.enabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description>
<param-name>cors.preflight.maxage</param-name>
<param-value>300</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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
* <p>
* Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3IT for
* a test that returns the xml of the observation
* <p>
* 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);
}
}

View File

@ -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
* <p>
* Note: This test only returns a ping with the subscription id, Check FhirSubscriptionWithSubscriptionIdDstu3IT for
* a test that returns the xml of the observation
* <p>
* 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);
}
}

View File

@ -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
* <p>
* 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);
}
}

View File

@ -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
* <p>
* 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();
}
}

View File

@ -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<Bundle.Entry> entry = bundle.getEntry();
Assert.assertTrue(entry.isEmpty());
}
/**
* Delete resources from specified class and tag
*
* @param clazz
* @param tag
* @param <T>
*/
public static <T extends IBaseResource> void deleteResources(Class<T> clazz, IBaseCoding tag, IGenericClient client) {
Bundle bundle = searchResources(clazz, tag, NUM_TO_DELETE_PER_QUERY, client);
List<Bundle.Entry> 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 <T>
* @return
*/
public static <T extends IBaseResource> Bundle searchResources(Class<T> 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();
}
}

View File

@ -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<Bundle.BundleEntryComponent> entry = bundle.getEntry();
Assert.assertTrue(entry.isEmpty());
}
/**
* Delete resources from specified class and tag
*
* @param clazz
* @param tag
* @param <T>
*/
public static <T extends IBaseResource> void deleteResources(Class<T> clazz, IBaseCoding tag, IGenericClient client) {
Bundle bundle = searchResources(clazz, tag, NUM_TO_DELETE_PER_QUERY, client);
List<Bundle.BundleEntryComponent> 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 <T>
* @return
*/
public static <T extends IBaseResource> Bundle searchResources(Class<T> 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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}
}

View File

@ -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");
}
}
}

View File

@ -370,6 +370,7 @@
<slf4j_target_version>1.6.0</slf4j_target_version>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<ebay_cors_filter_version>1.0.1</ebay_cors_filter_version>
</properties>
<dependencyManagement>
@ -644,6 +645,11 @@
<artifactId>woodstox-core-asl</artifactId>
<version>4.4.1</version>
</dependency>
<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<version>${ebay_cors_filter_version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>