Work on subsciprions

This commit is contained in:
jamesagnew 2015-09-28 09:06:57 -04:00
parent f818a3b478
commit 9492744018
46 changed files with 1455 additions and 268 deletions

View File

@ -439,21 +439,9 @@ public class IdDt extends UriDt implements IPrimitiveDatatype<String>, IIdType {
return true;
}
/**
* Returns <code>true</code> if the unqualified ID is a valid {@link Long} value (in other words, it consists only of digits)
*/
@Override
public boolean isIdPartValidLong() {
String id = getIdPart();
if (StringUtils.isBlank(id)) {
return false;
}
for (int i = 0; i < id.length(); i++) {
if (Character.isDigit(id.charAt(i)) == false) {
return false;
}
}
return true;
return isValidLong(getIdPart());
}
/**
@ -463,6 +451,11 @@ public class IdDt extends UriDt implements IPrimitiveDatatype<String>, IIdType {
public boolean isLocal() {
return "#".equals(myBaseUrl);
}
@Override
public boolean isVersionIdPartValidLong() {
return isValidLong(getVersionIdPart());
}
/**
* Copies the value from the given IdDt to <code>this</code> IdDt. It is generally not neccesary to use this method but it is provided for consistency with the rest of the API.
@ -630,6 +623,18 @@ public class IdDt extends UriDt implements IPrimitiveDatatype<String>, IIdType {
return new IdDt(value + '/' + Constants.PARAM_HISTORY + '/' + theVersion);
}
private static boolean isValidLong(String id) {
if (StringUtils.isBlank(id)) {
return false;
}
for (int i = 0; i < id.length(); i++) {
if (Character.isDigit(id.charAt(i)) == false) {
return false;
}
}
return true;
}
/**
* Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new, randomly
* created UUID generated by {@link UUID#randomUUID()}

View File

@ -152,20 +152,25 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
addParametersForServerRequest(theRequest, params);
HttpServletResponse servletResponse = theRequest.getServletResponse();
/*
* No need to catch nd handle exceptions here, we already handle them one level up
* including invoking interceptors on them
*/
MethodOutcome response;
try {
// try {
response = (MethodOutcome) invokeServerMethod(theServer, theRequest, params);
} catch (InternalErrorException e) {
ourLog.error("Internal error during method invocation", e);
EncodingEnum encodingNotNull = RestfulServerUtils.determineResponseEncodingWithDefault(theServer, theRequest.getServletRequest());
streamOperationOutcome(e, theServer, encodingNotNull, servletResponse, theRequest);
return;
} catch (BaseServerResponseException e) {
ourLog.info("Exception during method invocation: " + e.getMessage());
EncodingEnum encodingNotNull = RestfulServerUtils.determineResponseEncodingWithDefault(theServer, theRequest.getServletRequest());
streamOperationOutcome(e, theServer, encodingNotNull, servletResponse, theRequest);
return;
}
// } catch (InternalErrorException e) {
// ourLog.error("Internal error during method invocation", e);
// EncodingEnum encodingNotNull = RestfulServerUtils.determineResponseEncodingWithDefault(theServer, theRequest.getServletRequest());
// streamOperationOutcome(e, theServer, encodingNotNull, servletResponse, theRequest);
// return;
// } catch (BaseServerResponseException e) {
// ourLog.info("Exception during method invocation: " + e.getMessage());
// EncodingEnum encodingNotNull = RestfulServerUtils.determineResponseEncodingWithDefault(theServer, theRequest.getServletRequest());
// streamOperationOutcome(e, theServer, encodingNotNull, servletResponse, theRequest);
// return;
// }
if (response != null && response.getId() != null && response.getId().hasResourceType()) {
if (getContext().getResourceDefinition(response.getId().getResourceType()) == null) {

View File

@ -49,8 +49,7 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
private Integer myIdParameterIndex;
public DeleteMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(theMethod, theContext, Delete.class,
theProvider);
super(theMethod, theContext, Delete.class, theProvider);
Delete deleteAnnotation = theMethod.getAnnotation(Delete.class);
Class<? extends IResource> resourceType = deleteAnnotation.type();
@ -62,8 +61,8 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
RuntimeResourceDefinition def = theContext.getResourceDefinition(((IResourceProvider) theProvider).getResourceType());
myResourceName = def.getName();
} else {
throw new ConfigurationException("Can not determine resource type for method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getCanonicalName() + " - Did you forget to include the resourceType() value on the @"
+ Delete.class.getSimpleName() + " method annotation?");
throw new ConfigurationException(
"Can not determine resource type for method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getCanonicalName() + " - Did you forget to include the resourceType() value on the @" + Delete.class.getSimpleName() + " method annotation?");
}
}
@ -74,8 +73,7 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
Integer versionIdParameterIndex = MethodUtil.findVersionIdParameterIndex(theMethod);
if (versionIdParameterIndex != null) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has a parameter annotated with the @" + VersionIdParam.class.getSimpleName()
+ " annotation but delete methods may not have this annotation");
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has a parameter annotated with the @" + VersionIdParam.class.getSimpleName() + " annotation but delete methods may not have this annotation");
}
}
@ -114,13 +112,13 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
if (idDt == null) {
throw new NullPointerException("ID can not be null");
}
if (idDt.hasResourceType()==false) {
if (idDt.hasResourceType() == false) {
idDt = idDt.withResourceType(getResourceName());
}else if (getResourceName().equals(idDt.getResourceType())==false) {
} else if (getResourceName().equals(idDt.getResourceType()) == false) {
throw new InvalidRequestException("ID parameter has the wrong resource type, expected '" + getResourceName() + "', found: " + idDt.getResourceType());
}
HttpDeleteClientInvocation retVal = createDeleteInvocation(idDt);
for (int idx = 0; idx < theArgs.length; idx++) {
@ -141,7 +139,6 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
theParams[myIdParameterIndex] = theRequest.getId();
}
@Override
protected String getMatchingOperation() {
return null;
@ -156,5 +153,4 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding {
return new HttpDeleteClientInvocation(theResourceType, theParams);
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.io.PushbackReader;

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.isBlank;
/*
* #%L
* HAPI FHIR - Core Library

View File

@ -34,19 +34,13 @@ package org.hl7.fhir.instance.model.api;
*/
public interface IIdType {
boolean isEmpty();
void applyTo(IBaseResource theResource);
/**
* Returns <code>true</code> if the ID is a local reference (in other words, it begins with the '#' character)
* Returns the server base URL if this ID contains one. For example, the base URL is
* the 'http://example.com/fhir' in the following ID: <code>http://example.com/fhir/Patient/123/_history/55</code>
*/
boolean isLocal();
/**
* Returns the value of this ID. Note that this value may be a fully qualified URL, a relative/partial URL, or a simple ID. Use {@link #getIdPart()} to get just the ID portion.
*
* @see #getIdPart()
*/
String getValue();
String getBaseUrl();
/**
* Returns only the logical ID part of this ID. For example, given the ID
@ -55,43 +49,52 @@ public interface IIdType {
*/
String getIdPart();
/**
* Returns the ID part of this ID (e.g. in the ID http://example.com/Patient/123/_history/456 this would be the
* part "123") parsed as a {@link Long}.
*
* @throws NumberFormatException If the value can't be parsed as a long
*/
Long getIdPartAsLong();
String getResourceType();
/**
* Returns the value of this ID. Note that this value may be a fully qualified URL, a relative/partial URL, or a simple ID. Use {@link #getIdPart()} to get just the ID portion.
*
* @see #getIdPart()
*/
String getValue();
String getVersionIdPart();
/**
* Returns the version ID part of this ID (e.g. in the ID http://example.com/Patient/123/_history/456 this would be the
* part "456") parsed as a {@link Long}.
*
* @throws NumberFormatException If the value can't be parsed as a long
*/
Long getVersionIdPartAsLong();
boolean hasBaseUrl();
/**
* Returns <code>true</code> if this ID contains an actual ID part. For example, the ID part is
* the '123' in the following ID: <code>http://example.com/fhir/Patient/123/_history/55</code>
*/
boolean hasIdPart();
/**
* Returns the server base URL if this ID contains one. For example, the base URL is
* the 'http://example.com/fhir' in the following ID: <code>http://example.com/fhir/Patient/123/_history/55</code>
*/
String getBaseUrl();
IIdType toUnqualifiedVersionless();
IIdType toVersionless();
IIdType setValue(String theString);
boolean hasVersionIdPart();
String getVersionIdPart();
IIdType toUnqualified();
boolean hasResourceType();
IIdType withResourceType(String theResName);
String getResourceType();
IIdType withServerBase(String theServerBase, String theResourceName);
boolean hasVersionIdPart();
/**
* Returns <code>true</code> if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://"
*/
boolean isAbsolute();
boolean isEmpty();
/**
* Returns <code>true</code> if the {@link #getIdPart() ID part of this object} is valid according to the FHIR rules for valid IDs.
* <p>
@ -107,12 +110,29 @@ public interface IIdType {
*/
boolean isIdPartValidLong();
Long getIdPartAsLong();
/**
* Returns <code>true</code> if the ID is a local reference (in other words, it begins with the '#' character)
*/
boolean isLocal();
boolean hasBaseUrl();
/**
* Returns <code>true</code> if the {@link #getVersionIdPart() version ID part of this object} contains
* only numbers
*/
boolean isVersionIdPartValidLong();
IIdType setValue(String theString);
IIdType toUnqualified();
IIdType toUnqualifiedVersionless();
IIdType toVersionless();
IIdType withResourceType(String theResName);
IIdType withServerBase(String theServerBase, String theResourceName);
IIdType withVersion(String theVersion);
void applyTo(IBaseResource theResource);
}

View File

@ -56,7 +56,8 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithInvalidId=Can not
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedNumericId=Can not create resource with ID[{0}], no resource with this ID exists and clients may only assign IDs which contain at least one non-numeric character
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedId=Can not create resource with ID[{0}], ID must not be supplied on a create (POST) operation
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidParameterChain=Invalid parameter chain: {0}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidVersion=Version "{0}" is not valid for resource {1}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.multipleParamsWithSameNameOneIsMissingTrue=This server does not know how to handle multiple "{0}" parameters where one has a value of :missing=true
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed.
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulCreate=Successfully created resource "{0}" in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully updated resource "{0}" in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully updated resource "{0}" in {1}ms

View File

@ -6,7 +6,7 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**/*.java" including="**/*.java" kind="src" path="src/test/resources"/>
<classpathentry excluding="**/*.java" including="**/*.java" kind="src" output="target/test-classes" path="src/test/resources"/>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
@ -21,7 +21,6 @@
</attributes>
</classpathentry>
<classpathentry kind="var" path="M2_REPO/org/slf4j/slf4j-api/1.6.0/slf4j-api-1.6.0.jar"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/hapi-fhir-base"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>

View File

@ -267,11 +267,6 @@
<artifactId>websocket-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>

View File

@ -581,6 +581,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
}
protected boolean isValidPid(IIdType theId) {
if (theId == null || theId.getIdPart() == null) {
return false;
}
String idPart = theId.getIdPart();
for (int i = 0; i < idPart.length(); i++) {
char nextChar = idPart.charAt(i);

View File

@ -38,6 +38,7 @@ import java.util.Set;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.TemporalType;
@ -98,6 +99,7 @@ import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
import ca.uhn.fhir.model.base.composite.BaseQuantityDt;
import ca.uhn.fhir.model.dstu.resource.BaseResource;
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
@ -1205,7 +1207,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return;
}
if ("_id".equals(theSort.getParamName())) {
if (BaseResource.SP_RES_ID.equals(theSort.getParamName())) {
From<?, ?> forcedIdJoin = theFrom.join("myForcedId", JoinType.LEFT);
if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
theOrders.add(theBuilder.asc(forcedIdJoin.get("myForcedId")));
@ -1219,6 +1221,17 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return;
}
if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) {
if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
theOrders.add(theBuilder.asc(theFrom.get("myUpdated")));
} else {
theOrders.add(theBuilder.desc(theFrom.get("myUpdated")));
}
createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates);
return;
}
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType);
RuntimeSearchParam param = resourceDef.getSearchParam(theSort.getParamName());
if (param == null) {
@ -1286,6 +1299,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
@Override
public DaoMethodOutcome delete(IIdType theId) {
if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided");
}
StopWatch w = new StopWatch();
final ResourceTable entity = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
@ -1886,8 +1902,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
Long pid = translateForcedIdToPid(theId);
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
if (theId.hasVersionIdPart()) {
if (entity.getVersion() != Long.parseLong(theId.getVersionIdPart())) {
if (theId.isVersionIdPartValidLong() == false) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
}
if (entity.getVersion() != theId.getVersionIdPartAsLong().longValue()) {
entity = null;
}
}
@ -1898,11 +1922,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
q.setParameter("RID", pid);
q.setParameter("RTYP", myResourceName);
q.setParameter("RVER", Long.parseLong(theId.getVersionIdPart()));
entity = q.getSingleResult();
}
if (entity == null) {
throw new ResourceNotFoundException(theId);
q.setParameter("RVER", theId.getVersionIdPartAsLong());
try {
entity = q.getSingleResult();
} catch (NoResultException e) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
}
}
}
@ -2019,6 +2044,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
for (Long next : query.getResultList()) {
loadPids.add(next);
}
if (loadPids.isEmpty()) {
return new SimpleBundleProvider();
}
}
// Handle sorting if any was provided
@ -2044,7 +2073,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
loadPids.add(next.get(0, Long.class));
}
ourLog.info("Sort PID order is now: {}", loadPids);
ourLog.debug("Sort PID order is now: {}", loadPids);
pids = new ArrayList<Long>(loadPids);

View File

@ -4,6 +4,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
/*
* #%L
* HAPI FHIR JPA Server
@ -37,6 +40,7 @@ public class DaoConfig {
private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC;
private boolean mySubscriptionEnabled;
private long mySubscriptionPollDelay = 1000;
private Long mySubscriptionPurgeInactiveAfterMillis;
/**
* See {@link #setIncludeLimit(int)}
@ -128,11 +132,9 @@ public class DaoConfig {
}
/**
* Does this server support subscription? If set to true, the server
* will enable the subscription monitoring mode, which adds a bit of
* overhead. Note that if this is enabled, you must also include
* Spring task scanning to your XML config for the scheduled tasks
* used by the subscription module.
* Does this server support subscription? If set to true, the server will enable the subscription monitoring mode,
* which adds a bit of overhead. Note that if this is enabled, you must also include Spring task scanning to your XML
* config for the scheduled tasks used by the subscription module.
*/
public void setSubscriptionEnabled(boolean theSubscriptionEnabled) {
mySubscriptionEnabled = theSubscriptionEnabled;
@ -142,4 +144,19 @@ public class DaoConfig {
mySubscriptionPollDelay = theSubscriptionPollDelay;
}
public void setSubscriptionPurgeInactiveAfterSeconds(int theSeconds) {
setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND);
}
public void setSubscriptionPurgeInactiveAfterMillis(Long theMillis) {
if (theMillis != null) {
Validate.exclusiveBetween(0, Long.MAX_VALUE, theMillis);
}
mySubscriptionPurgeInactiveAfterMillis = theMillis;
}
public Long getSubscriptionPurgeInactiveAfterMillis() {
return mySubscriptionPurgeInactiveAfterMillis;
}
}

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao;
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@ -31,23 +32,36 @@ import javax.persistence.TypedQuery;
import org.apache.commons.lang3.time.DateUtils;
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.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionFlaggedResourceDataDao;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource;
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
@ -57,9 +71,13 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
@Autowired
private ISubscriptionFlaggedResourceDataDao mySubscriptionFlaggedResourceDataDao;
@Autowired
private ISubscriptionTableDao mySubscriptionTableDao;
private void createSubscriptionTable(ResourceTable theEntity, Subscription theSubscription) {
SubscriptionTable subscriptionEntity = new SubscriptionTable();
subscriptionEntity.setCreated(new Date());
subscriptionEntity.setSubscriptionResource(theEntity);
subscriptionEntity.setNextCheck(theEntity.getPublished().getValue());
subscriptionEntity.setMostRecentMatch(theEntity.getPublished().getValue());
@ -67,6 +85,9 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
myEntityManager.persist(subscriptionEntity);
}
@Autowired
private PlatformTransactionManager myTxManager;
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override
@ -76,15 +97,23 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
}
ourLog.trace("Beginning pollForNewUndeliveredResources()");
// SubscriptionCandidateResource
// SubscriptionCandidateResource
TypedQuery<SubscriptionTable> q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_NEXT_CHECK", SubscriptionTable.class);
q.setParameter("next_check", new Date());
q.setParameter("status", SubscriptionStatusEnum.ACTIVE);
List<SubscriptionTable> subscriptions = q.getResultList();
for (SubscriptionTable nextSubscriptionTable : subscriptions) {
pollForNewUndeliveredResources(nextSubscriptionTable);
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
for (final SubscriptionTable nextSubscriptionTable : subscriptions) {
txTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus theStatus) {
pollForNewUndeliveredResources(nextSubscriptionTable);
return null;
}
});
}
}
@ -92,8 +121,8 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
Subscription subscription = toResource(Subscription.class, theSubscriptionTable.getSubscriptionResource(), false);
RuntimeResourceDefinition resourceDef = validateCriteriaAndReturnResourceDefinition(subscription);
SearchParameterMap criteriaUrl = translateMatchUrl(subscription.getCriteria(), resourceDef);
criteriaUrl = new SearchParameterMap();//TODO:remove
criteriaUrl = new SearchParameterMap();
long start = theSubscriptionTable.getMostRecentMatch().getTime();
long end = System.currentTimeMillis() - getConfig().getSubscriptionPollDelay();
if (end <= start) {
@ -101,26 +130,42 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
return;
}
ourLog.info("Subscription search from {} to {}", start, end);
DateRangeParam range = new DateRangeParam();
range.setLowerBound(new DateParam(QuantityCompararatorEnum.GREATERTHAN, start));
range.setUpperBound(new DateParam(QuantityCompararatorEnum.LESSTHAN, end));
criteriaUrl.setLastUpdated(range);
criteriaUrl.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.ASC));
IFhirResourceDao<? extends IBaseResource> dao = getDao(resourceDef.getImplementingClass());
IBundleProvider results = dao.search(criteriaUrl);
if (results.size() == 0) {
return;
}
ourLog.info("Found {} new results for Subscription {}", results.size(), subscription.getId().getIdPart());
List<SubscriptionFlaggedResource> flags = new ArrayList<SubscriptionFlaggedResource>();
Date mostRecentMatch = null;
for (IBaseResource next : results.getResources(0, results.size())) {
Date updated = ResourceMetadataKeyEnum.PUBLISHED.get((IResource) next).getValue();
if (mostRecentMatch == null || mostRecentMatch.getTime() < updated.getTime()) {
mostRecentMatch = updated;
}
SubscriptionFlaggedResource nextFlag = new SubscriptionFlaggedResource();
// nextFlag.setResource();
Long pid = IDao.RESOURCE_PID.get((IResource) next);
nextFlag.setResource(myEntityManager.find(ResourceTable.class, pid));
nextFlag.setSubscription(theSubscriptionTable);
nextFlag.setVersion(next.getIdElement().getVersionIdPartAsLong());
flags.add(nextFlag);
}
mySubscriptionFlaggedResourceDataDao.save(flags);
theSubscriptionTable.setMostRecentMatch(mostRecentMatch);
myEntityManager.merge(theSubscriptionTable);
}
@Override
@ -137,9 +182,11 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
Subscription resource = (Subscription) theResource;
Long resourceId = theEntity.getId();
if (theDeletedTimestampOrNull != null) {
Query q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_DELETE");
q.setParameter("res_id", resourceId);
q.executeUpdate();
Long subscriptionId = getSubscriptionTablePidForSubscriptionResource(theEntity.getIdDt());
if (subscriptionId != null) {
mySubscriptionFlaggedResourceDataDao.deleteAllForSubscription(subscriptionId);
mySubscriptionTableDao.deleteAllForSubscription(subscriptionId);
}
} else {
Query q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_SET_STATUS");
q.setParameter("res_id", resourceId);
@ -200,4 +247,57 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
return resDef;
}
@Override
public List<IBaseResource> getUndeliveredResourcesAndPurge(Long theSubscriptionPid) {
List<IBaseResource> retVal = new ArrayList<IBaseResource>();
Page<SubscriptionFlaggedResource> flaggedResources = mySubscriptionFlaggedResourceDataDao.findAllBySubscriptionId(theSubscriptionPid, new PageRequest(0, 100));
for (SubscriptionFlaggedResource nextFlaggedResource : flaggedResources) {
retVal.add(toResource(nextFlaggedResource.getResource(), false));
}
mySubscriptionFlaggedResourceDataDao.delete(flaggedResources);
mySubscriptionFlaggedResourceDataDao.flush();
mySubscriptionTableDao.updateLastClientPoll(new Date());
return retVal;
}
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId) {
ResourceTable entity = readEntityLatestVersion(theId);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;
}
return table.getId();
}
@Scheduled(fixedDelay = DateUtils.MILLIS_PER_MINUTE)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override
public void purgeInactiveSubscriptions() {
Long purgeInactiveAfterMillis = getConfig().getSubscriptionPurgeInactiveAfterMillis();
if (getConfig().isSubscriptionEnabled()==false || purgeInactiveAfterMillis == null) {
return;
}
Date cutoff = new Date(System.currentTimeMillis() - purgeInactiveAfterMillis);
Collection<SubscriptionTable> toPurge = mySubscriptionTableDao.findInactiveBeforeCutoff(cutoff);
for (SubscriptionTable subscriptionTable : toPurge) {
final IdDt subscriptionId = subscriptionTable.getSubscriptionResource().getIdDt();
ourLog.info("Deleting inactive subscription {} - Created {}, last client poll {}", new Object[] { subscriptionId.toUnqualified(), subscriptionTable.getCreated(), subscriptionTable.getLastClientPoll() });
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
txTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus theStatus) {
delete(subscriptionId);
return null;
}
});
}
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.jpa.dao;
import java.util.List;
/*
* #%L
* HAPI FHIR JPA Server
@ -21,9 +23,16 @@ package ca.uhn.fhir.jpa.dao;
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
public interface IFhirResourceDaoSubscription<T extends IBaseResource> extends IFhirResourceDao<T> {
void pollForNewUndeliveredResources();
List<IBaseResource> getUndeliveredResourcesAndPurge(Long theSubscriptionPid);
Long getSubscriptionTablePidForSubscriptionResource(IIdType theId);
void purgeInactiveSubscriptions();
}

View File

@ -35,6 +35,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.Constants;
public class SearchParameterMap extends LinkedHashMap<String, List<List<? extends IQueryParameterType>>> {
@ -75,6 +76,8 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
}
public void add(String theName, IQueryParameterType theParam) {
assert !Constants.PARAM_LASTUPDATED.equals(theName); // this has it's own field in the map
if (theParam == null) {
return;
}

View File

@ -1,29 +1,21 @@
package ca.uhn.fhir.jpa.dao.data;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.entity.SubscriptionFlaggedResource;
public interface ISubscriptionFlaggedResourceDataDao extends CrudRepository<SubscriptionFlaggedResource, Long> {
public interface ISubscriptionFlaggedResourceDataDao extends JpaRepository<SubscriptionFlaggedResource, Long> {
@Query("SELECT r FROM SubscriptionFlaggedResource r WHERE r.mySubscription.myId = :id ORDER BY r.myId ASC")
public Page<SubscriptionFlaggedResource> findAllBySubscriptionId(@Param("id") Long theId, Pageable thePage);
@Modifying
@Query("DELETE FROM SubscriptionFlaggedResource r WHERE r.mySubscription.myId = :id")
public void deleteAllForSubscription(@Param("id") Long theSubscriptionId);
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.jpa.dao.data;
import java.util.Collection;
import java.util.Date;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
public interface ISubscriptionTableDao extends JpaRepository<SubscriptionTable, Long> {
@Query("SELECT t FROM SubscriptionTable t WHERE t.myResId = :pid")
public SubscriptionTable findOneByResourcePid(@Param("pid") Long theId);
@Modifying
@Query("DELETE FROM SubscriptionTable t WHERE t.myId = :id ")
public void deleteAllForSubscription(@Param("id") Long theSubscriptionId);
@Modifying
@Query("UPDATE SubscriptionTable t SET t.myLastClientPoll = :last_client_poll")
public int updateLastClientPoll(@Param("last_client_poll") Date theLastClientPoll);
@Query("SELECT t FROM SubscriptionTable t WHERE t.myLastClientPoll < :cutoff OR (t.myLastClientPoll IS NULL AND t.myCreated < :cutoff)")
public Collection<SubscriptionTable> findInactiveBeforeCutoff(@Param("cutoff") Date theCutoff);
}

View File

@ -32,7 +32,9 @@ import org.apache.commons.lang3.builder.ToStringStyle;
@Entity
@Table(name = "HFJ_SPIDX_STRING"/* , indexes= {@Index(name="IDX_SP_STRING", columnList="SP_VALUE_NORMALIZED")} */)
@org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_STRING", indexes = { @org.hibernate.annotations.Index(name = "IDX_SP_STRING", columnNames = { "RES_TYPE", "SP_NAME", "SP_VALUE_NORMALIZED" }) })
@org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_STRING", indexes = {
@org.hibernate.annotations.Index(name = "IDX_SP_STRING", columnNames = { "RES_TYPE", "SP_NAME", "SP_VALUE_NORMALIZED" })
})
public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchParam {
public static final int MAX_LENGTH = 100;

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ForeignKey;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@ -40,13 +41,20 @@ public class SubscriptionFlaggedResource {
@Column(name = "PID", insertable = false, updatable = false)
private Long myId;
@ManyToOne
@JoinColumn(name="RES_ID")
@ManyToOne()
@JoinColumn(name="RES_ID", nullable=false)
private ResourceTable myResource;
//@formatter:off
@ManyToOne()
@JoinColumn(name="SUBSCRIPTION_ID")
@JoinColumn(name="SUBSCRIPTION_ID",
foreignKey=@ForeignKey(name="FK_SUBSFLAG_SUBS")
)
private SubscriptionTable mySubscription;
//@formatter:om
@Column(name="RES_VERSION", nullable=false)
private Long myVersion;
public ResourceTable getResource() {
return myResource;
@ -56,6 +64,10 @@ public class SubscriptionFlaggedResource {
return mySubscription;
}
public Long getVersion() {
return myVersion;
}
public void setResource(ResourceTable theResource) {
myResource = theResource;
}
@ -63,5 +75,9 @@ public class SubscriptionFlaggedResource {
public void setSubscription(SubscriptionTable theSubscription) {
mySubscription = theSubscription;
}
public void setVersion(Long theVersion) {
myVersion = theVersion;
}
}

View File

@ -53,8 +53,7 @@ import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
@NamedQueries({
@NamedQuery(name="Q_HFJ_SUBSCRIPTION_SET_STATUS", query="UPDATE SubscriptionTable t SET t.myStatus = :status WHERE t.myResId = :res_id"),
@NamedQuery(name="Q_HFJ_SUBSCRIPTION_NEXT_CHECK", query="SELECT t FROM SubscriptionTable t WHERE t.myStatus = :status AND t.myNextCheck <= :next_check"),
@NamedQuery(name="Q_HFJ_SUBSCRIPTION_GET_BY_RES", query="SELECT t FROM SubscriptionTable t WHERE t.myResId = :res_id"),
@NamedQuery(name="Q_HFJ_SUBSCRIPTION_DELETE", query="DELETE FROM SubscriptionTable t WHERE t.myResId = :res_id"),
@NamedQuery(name="Q_HFJ_SUBSCRIPTION_GET_BY_RES", query="SELECT t FROM SubscriptionTable t WHERE t.myResId = :res_id")
})
//@formatter:on
public class SubscriptionTable {
@ -62,6 +61,13 @@ public class SubscriptionTable {
@Column(name = "CHECK_INTERVAL", nullable = false)
private long myCheckInterval;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED_TIME", nullable = false, insertable = true, updatable = false)
private Date myCreated;
@OneToMany(mappedBy = "mySubscription")
private Collection<SubscriptionFlaggedResource> myFlaggedResources;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@SequenceGenerator(name = "SEQ_SUBSCRIPTION_ID", sequenceName = "SEQ_SUBSCRIPTION_ID")
@ -69,13 +75,17 @@ public class SubscriptionTable {
private Long myId;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "NEXT_CHECK", nullable = false)
private Date myNextCheck;
@Column(name = "LAST_CLIENT_POLL", nullable = true)
private Date myLastClientPoll;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "MOST_RECENT_MATCH", nullable = false)
private Date myMostRecentMatch;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "NEXT_CHECK", nullable = false)
private Date myNextCheck;
@Column(name = "RES_ID", insertable = false, updatable = false)
private Long myResId;
@ -83,21 +93,34 @@ public class SubscriptionTable {
@Enumerated(EnumType.STRING)
private SubscriptionStatusEnum myStatus;
//@formatter:off
@OneToOne()
@JoinColumn(name = "RES_ID", insertable = true, updatable = false, referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_SUBSCRIPTION_RESOURCE_ID") )
@JoinColumn(name = "RES_ID", insertable = true, updatable = false, referencedColumnName = "RES_ID",
foreignKey = @ForeignKey(name = "FK_SUBSCRIPTION_RESOURCE_ID")
)
private ResourceTable mySubscriptionResource;
@OneToMany(orphanRemoval=true, mappedBy="mySubscription")
private Collection<SubscriptionFlaggedResource> myFlaggedResources;
//@formatter:on
public long getCheckInterval() {
return myCheckInterval;
}
public Date getCreated() {
return myCreated;
}
public Long getId() {
return myId;
}
public Date getLastClientPoll() {
return myLastClientPoll;
}
public Date getMostRecentMatch() {
return myMostRecentMatch;
}
public Date getNextCheck() {
return myNextCheck;
}
@ -114,6 +137,18 @@ public class SubscriptionTable {
myCheckInterval = theCheckInterval;
}
public void setCreated(Date theCreated) {
myCreated = theCreated;
}
public void setLastClientPoll(Date theLastClientPoll) {
myLastClientPoll = theLastClientPoll;
}
public void setMostRecentMatch(Date theMostRecentMatch) {
myMostRecentMatch = theMostRecentMatch;
}
public void setNextCheck(Date theNextCheck) {
myNextCheck = theNextCheck;
}
@ -126,12 +161,4 @@ public class SubscriptionTable {
mySubscriptionResource = theSubscriptionResource;
}
public Date getMostRecentMatch() {
return myMostRecentMatch;
}
public void setMostRecentMatch(Date theMostRecentMatch) {
myMostRecentMatch = theMostRecentMatch;
}
}

View File

@ -0,0 +1,7 @@
package ca.uhn.fhir.jpa.subscription;
import org.springframework.web.socket.WebSocketHandler;
public interface ISubscriptionWebsocketHandler extends WebSocketHandler {
}

View File

@ -0,0 +1,179 @@
package ca.uhn.fhir.jpa.subscription;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
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.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 ca.uhn.fhir.jpa.dao.IFhirResourceDaoSubscription;
import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class SubscriptionWebsocketHandler extends TextWebSocketHandler implements ISubscriptionWebsocketHandler, Runnable {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionWebsocketHandler.class);
private ScheduledFuture<?> myScheduleFuture;
private IState myState = new InitialState();
@Autowired
private IFhirResourceDaoSubscription<Subscription> mySubscriptionDao;
private IIdType mySubscriptionId;
private Long mySubscriptionPid;
@Autowired
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);
}
@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);
}
@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 SimpleBoundState implements IState {
private WebSocketSession mySession;
public SimpleBoundState(WebSocketSession theSession) {
mySession = theSession;
}
@Override
public void deliver(List<IBaseResource> theResults) {
try {
String payload = "ping " + mySubscriptionId.getIdPart();
ourLog.info("Sending WebSocket message: {}", payload);
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 {
@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 ")) {
IdDt id = new IdDt(message.substring("bind ".length()));
if (!id.hasIdPart() || !id.isIdPartValid()) {
try {
theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), "Invalid bind request - No ID included"));
} catch (IOException e) {
handleFailure(e);
}
return;
}
if (id.hasResourceType()==false) {
id = id.withResourceType("Subscription");
}
try {
Subscription subscription = mySubscriptionDao.read(id);
mySubscriptionPid = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(id);
mySubscriptionId = subscription.getIdElement();
myState = new SimpleBoundState(theSession);
} catch (ResourceNotFoundException e) {
try {
theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), "Invalid bind request - Unknown subscription: " + id.getValue()));
} catch (IOException e1) {
handleFailure(e);
}
return;
}
try {
theSession.sendMessage(new TextMessage("bound " + id.getIdPart()));
} catch (IOException e) {
handleFailure(e);
}
}
}
}
private interface IState{
void deliver(List<IBaseResource> theResults);
void handleTextMessage(WebSocketSession theSession, TextMessage theMessage);
}
}

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.jpa.subscription;
import org.springframework.beans.factory.FactoryBean;
public class SubscriptionWebsocketHandlerFactory implements FactoryBean<ISubscriptionWebsocketHandler> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionWebsocketHandler.class);
@Override
public ISubscriptionWebsocketHandler getObject() throws Exception {
return new SubscriptionWebsocketHandler();
}
@Override
public Class<ISubscriptionWebsocketHandler> getObjectType() {
return ISubscriptionWebsocketHandler.class;
}
@Override
public boolean isSingleton() {
return false;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.jpa.util;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
* HAPI FHIR JPA Server
@ -29,6 +31,7 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
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;
@ -69,7 +72,7 @@ public class SubscriptionsRequireManualActivationInterceptor extends Interceptor
case CREATE:
case UPDATE:
if (theProcessedRequest.getResourceType().equals("Subscription")) {
verifyStatusOk(theProcessedRequest);
verifyStatusOk(theOperation, theProcessedRequest);
}
default:
break;
@ -80,15 +83,19 @@ public class SubscriptionsRequireManualActivationInterceptor extends Interceptor
myDao = theDao;
}
private void verifyStatusOk(ActionRequestDetails theRequestDetails) {
private void verifyStatusOk(RestOperationTypeEnum theOperation, ActionRequestDetails theRequestDetails) {
Subscription subscription = (Subscription) theRequestDetails.getResource();
;
SubscriptionStatusEnum newStatus = subscription.getStatusElement().getValueAsEnum();
if (newStatus == SubscriptionStatusEnum.REQUESTED || newStatus == SubscriptionStatusEnum.OFF) {
return;
}
if (newStatus == null) {
String actualCode = subscription.getStatusElement().getValueAsString();
throw new UnprocessableEntityException("Can not " + theOperation.getCode() + " resource: Subscription.status must be populated" + ((isNotBlank(actualCode)) ? " (invalid value " + actualCode + ")" : ""));
}
IIdType requestId = theRequestDetails.getId();
if (requestId != null && requestId.hasIdPart()) {
Subscription existing;
@ -96,20 +103,34 @@ public class SubscriptionsRequireManualActivationInterceptor extends Interceptor
existing = myDao.read(requestId);
SubscriptionStatusEnum existingStatus = existing.getStatusElement().getValueAsEnum();
if (existingStatus != newStatus) {
throw new UnprocessableEntityException("Subscription.status can not be changed from " + describeStatus(existingStatus) + " to " + describeStatus(newStatus));
verifyActiveStatus(subscription, newStatus, existingStatus);
}
} catch (ResourceNotFoundException e) {
if (newStatus != SubscriptionStatusEnum.REQUESTED) {
throw new UnprocessableEntityException("Subscription.status must be '" + SubscriptionStatusEnum.REQUESTED.getCode() + "' on a newly created subscription");
}
verifyActiveStatus(subscription, newStatus, null);
}
} else {
if (newStatus != SubscriptionStatusEnum.REQUESTED) {
throw new UnprocessableEntityException("Subscription.status must be '" + SubscriptionStatusEnum.REQUESTED.getCode() + "' on a newly created subscription");
}
verifyActiveStatus(subscription, newStatus, null);
}
}
private void verifyActiveStatus(Subscription theSubscription, SubscriptionStatusEnum newStatus, SubscriptionStatusEnum theExistingStatus) {
SubscriptionChannelTypeEnum channelType = theSubscription.getChannel().getTypeElement().getValueAsEnum();
if (channelType == null) {
throw new UnprocessableEntityException("Subscription.channel.type must be populated");
}
if (channelType == SubscriptionChannelTypeEnum.WEBSOCKET) {
return;
}
if (theExistingStatus != null) {
throw new UnprocessableEntityException("Subscription.status can not be changed from " + describeStatus(theExistingStatus) + " to " + describeStatus(newStatus));
}
throw new UnprocessableEntityException("Subscription.status must be '" + SubscriptionStatusEnum.OFF.getCode() + "' or '" + SubscriptionStatusEnum.REQUESTED.getCode() + "' on a newly created subscription");
}
private String describeStatus(SubscriptionStatusEnum existingStatus) {
String existingStatusString;
if (existingStatus != null) {

View File

@ -40,10 +40,10 @@
</property>
</bean>
<bean id="myTxManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<tx:annotation-driven transaction-manager="myTxManager" />
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>

View File

@ -0,0 +1,29 @@
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<context:annotation-config />
<websocket:handlers>
<websocket:mapping path="/baseDstu2/websocket" handler="mySubscriptionWebsocketHandler" />
</websocket:handlers>
<!--
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
-->
<bean id="mySubscriptionWebsocketHandler" class="org.springframework.web.socket.handler.PerConnectionWebSocketHandler">
<constructor-arg value="ca.uhn.fhir.jpa.subscription.SubscriptionWebsocketHandler"/>
</bean>
<bean id="mySubscriptionSecurityInterceptor" class="ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptor"/>
</beans>

View File

@ -15,6 +15,7 @@ import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
@ -71,6 +72,9 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
//@formatter:on
public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Autowired
protected ApplicationContext myAppCtx;
@Autowired
@Qualifier("myConceptMapDaoDstu2")
protected IFhirResourceDao<ConceptMap> myConceptMapDao;

View File

@ -35,5 +35,13 @@ public class BaseJpaTest {
return retVal;
}
protected List<IIdType> toUnqualifiedVersionlessIds(List<IBaseResource> theFound) {
List<IIdType> retVal = new ArrayList<IIdType>();
for (IBaseResource next : theFound) {
retVal.add((IIdType) next.getIdElement().toUnqualifiedVersionless());
}
return retVal;
}
}

View File

@ -1,24 +1,32 @@
package ca.uhn.fhir.jpa.dao;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Set;
import java.util.Date;
import java.util.List;
import javax.persistence.TypedQuery;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Before;
import org.junit.Test;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.beans.factory.annotation.Autowired;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionFlaggedResourceDataDao;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.Patient;
@ -26,12 +34,119 @@ 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.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public class FhirResourceDaoDstu2SubscriptionTest extends BaseJpaDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2SubscriptionTest.class);
@Autowired
private ISubscriptionFlaggedResourceDataDao mySubscriptionFlaggedResourceDataDao;
@Autowired
private ISubscriptionTableDao mySubscriptionTableDao;
@Before
public void beforeEnableSubscription() {
myDaoConfig.setSubscriptionEnabled(true);
myDaoConfig.setSubscriptionPurgeInactiveAfterSeconds(60);
}
@Test
public void testSubscriptionGetsPurgedIfItIsNeverActive() throws Exception {
myDaoConfig.setSubscriptionPurgeInactiveAfterSeconds(1);
Subscription subs = new Subscription();
subs.setCriteria("Observation?subject=Patient/123");
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless();
mySubscriptionDao.purgeInactiveSubscriptions();
mySubscriptionDao.read(id);
Thread.sleep(1500);
mySubscriptionDao.purgeInactiveSubscriptions();
try {
mySubscriptionDao.read(id);
fail();
} catch (ResourceGoneException e) {
// good
}
}
@Test
public void testSubscriptionGetsPurgedIfItIsInactive() throws Exception {
myDaoConfig.setSubscriptionPurgeInactiveAfterSeconds(1);
Subscription subs = new Subscription();
subs.setCriteria("Observation?subject=Patient/123");
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless();
mySubscriptionDao.purgeInactiveSubscriptions();
mySubscriptionDao.read(id);
mySubscriptionDao.getUndeliveredResourcesAndPurge(mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(id));
Thread.sleep(1500);
mySubscriptionDao.purgeInactiveSubscriptions();
try {
mySubscriptionDao.read(id);
fail();
} catch (ResourceGoneException e) {
// good
}
}
@Test
public void testCreateSubscription() {
Subscription subs = new Subscription();
subs.setCriteria("Observation?subject=Patient/123");
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless();
TypedQuery<SubscriptionTable> q = myEntityManager.createQuery("SELECT t from SubscriptionTable t WHERE t.mySubscriptionResource.myId = :id", SubscriptionTable.class);
q.setParameter("id", id.getIdPartAsLong());
final SubscriptionTable table = q.getSingleResult();
assertNotNull(table);
assertNotNull(table.getNextCheck());
assertEquals(table.getNextCheck(), table.getSubscriptionResource().getPublished().getValue());
assertEquals(SubscriptionStatusEnum.REQUESTED, myEntityManager.find(SubscriptionTable.class, table.getId()).getStatus());
assertEquals(SubscriptionStatusEnum.REQUESTED, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum());
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
mySubscriptionDao.update(subs);
assertEquals(SubscriptionStatusEnum.ACTIVE, myEntityManager.find(SubscriptionTable.class, table.getId()).getStatus());
assertEquals(SubscriptionStatusEnum.ACTIVE, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum());
mySubscriptionDao.delete(id);
assertNull(myEntityManager.find(SubscriptionTable.class, table.getId()));
/*
* Re-create again
*/
subs = new Subscription();
subs.setCriteria("Observation?subject=Patient/123");
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setId(id);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
mySubscriptionDao.update(subs);
assertEquals(SubscriptionStatusEnum.REQUESTED, myEntityManager.createQuery("SELECT t FROM SubscriptionTable t WHERE t.myResId = " + id.getIdPart(), SubscriptionTable.class).getSingleResult().getStatus());
assertEquals(SubscriptionStatusEnum.REQUESTED, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum());
}
@Test
public void testCreateSubscriptionInvalidCriteria() {
Subscription subs = new Subscription();
@ -82,15 +197,98 @@ public class FhirResourceDaoDstu2SubscriptionTest extends BaseJpaDstu2Test {
}
@Before
public void beforeEnableSubscription() {
myDaoConfig.setSubscriptionEnabled(true);
@Test
public void testDeleteSubscriptionWithFlaggedResources() throws Exception {
myDaoConfig.setSubscriptionPollDelay(0);
String methodName = "testDeleteSubscriptionWithFlaggedResources";
Patient p = new Patient();
p.addName().addFamily(methodName);
IIdType pId = myPatientDao.create(p).getId().toUnqualifiedVersionless();
Subscription subs;
/*
* Create 2 identical subscriptions
*/
subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setCriteria("Observation?subject=Patient/" + pId.getIdPart());
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
IIdType subsId = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless();
Long subsPid = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(subsId);
assertNull(mySubscriptionTableDao.findOne(subsPid).getLastClientPoll());
Thread.sleep(100);
ourLog.info("Before: {}", System.currentTimeMillis());
assertThat(mySubscriptionFlaggedResourceDataDao.count(), not(greaterThan(0L)));
assertThat(mySubscriptionTableDao.count(), equalTo(1L));
Observation obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
myObservationDao.create(obs).getId().toUnqualifiedVersionless();
obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
myObservationDao.create(obs).getId().toUnqualifiedVersionless();
Thread.sleep(100);
ourLog.info("After: {}", System.currentTimeMillis());
mySubscriptionDao.pollForNewUndeliveredResources();
assertThat(mySubscriptionFlaggedResourceDataDao.count(), greaterThan(0L));
assertThat(mySubscriptionTableDao.count(), greaterThan(0L));
/*
* Delete the subscription
*/
mySubscriptionDao.delete(subsId);
assertThat(mySubscriptionFlaggedResourceDataDao.count(), not(greaterThan(0L)));
assertThat(mySubscriptionTableDao.count(), not(greaterThan(0L)));
/*
* Delete a second time just to make sure that works
*/
mySubscriptionDao.delete(subsId);
/*
* Re-create the subscription
*/
subs.setId(subsId);
mySubscriptionDao.update(subs).getId();
assertThat(mySubscriptionFlaggedResourceDataDao.count(), not(greaterThan(0L)));
assertThat(mySubscriptionTableDao.count(), (greaterThan(0L)));
/*
* Create another resource and make sure it gets flagged
*/
obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
myObservationDao.create(obs).getId().toUnqualifiedVersionless();
Thread.sleep(100);
mySubscriptionDao.pollForNewUndeliveredResources();
assertThat(mySubscriptionFlaggedResourceDataDao.count(), greaterThan(0L));
assertThat(mySubscriptionTableDao.count(), greaterThan(0L));
}
@Test
public void testSubscriptionResourcesAppear() throws Exception {
myDaoConfig.setSubscriptionPollDelay(0);
String methodName = "testSubscriptionResourcesAppear";
Patient p = new Patient();
p.addName().addFamily(methodName);
@ -99,17 +297,31 @@ public class FhirResourceDaoDstu2SubscriptionTest extends BaseJpaDstu2Test {
Observation obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
IIdType beforeId = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
myObservationDao.create(obs).getId().toUnqualifiedVersionless();
Subscription subs = new Subscription();
Subscription subs;
/*
* Create 2 identical subscriptions
*/
subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setCriteria("Observation?subject=Patient/" + pId.getIdPart());
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless();
Long subsId1 = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(mySubscriptionDao.create(subs).getId());
subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setCriteria("Observation?subject=Patient/" + pId.getIdPart());
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
Long subsId2 = mySubscriptionDao.getSubscriptionTablePidForSubscriptionResource(mySubscriptionDao.create(subs).getId());
assertNull(mySubscriptionTableDao.findOne(subsId1).getLastClientPoll());
Thread.sleep(100);
ourLog.info("Before: {}", System.currentTimeMillis());
obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
@ -124,51 +336,34 @@ public class FhirResourceDaoDstu2SubscriptionTest extends BaseJpaDstu2Test {
ourLog.info("After: {}", System.currentTimeMillis());
List<IBaseResource> results;
List<IIdType> resultIds;
mySubscriptionDao.pollForNewUndeliveredResources();
}
results = mySubscriptionDao.getUndeliveredResourcesAndPurge(subsId1);
resultIds = toUnqualifiedVersionlessIds(results);
assertThat(resultIds, contains(afterId1, afterId2));
@Test
public void testCreateSubscription() {
Subscription subs = new Subscription();
subs.setCriteria("Observation?subject=Patient/123");
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
Date lastClientPoll = mySubscriptionTableDao.findOne(subsId1).getLastClientPoll();
assertNotNull(lastClientPoll);
IIdType id = mySubscriptionDao.create(subs).getId().toUnqualifiedVersionless();
mySubscriptionDao.pollForNewUndeliveredResources();
results = mySubscriptionDao.getUndeliveredResourcesAndPurge(subsId2);
resultIds = toUnqualifiedVersionlessIds(results);
assertThat(resultIds, contains(afterId1, afterId2));
TypedQuery<SubscriptionTable> q = myEntityManager.createQuery("SELECT t from SubscriptionTable t WHERE t.mySubscriptionResource.myId = :id", SubscriptionTable.class);
q.setParameter("id", id.getIdPartAsLong());
final SubscriptionTable table = q.getSingleResult();
mySubscriptionDao.pollForNewUndeliveredResources();
results = mySubscriptionDao.getUndeliveredResourcesAndPurge(subsId1);
resultIds = toUnqualifiedVersionlessIds(results);
assertThat(resultIds, empty());
assertNotNull(table);
assertNotNull(table.getNextCheck());
assertEquals(table.getNextCheck(), table.getSubscriptionResource().getPublished().getValue());
assertEquals(SubscriptionStatusEnum.REQUESTED, myEntityManager.find(SubscriptionTable.class, table.getId()).getStatus());
assertEquals(SubscriptionStatusEnum.REQUESTED, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum());
assertNotEquals(lastClientPoll, mySubscriptionTableDao.findOne(subsId1).getLastClientPoll());
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
mySubscriptionDao.update(subs);
mySubscriptionDao.pollForNewUndeliveredResources();
results = mySubscriptionDao.getUndeliveredResourcesAndPurge(subsId2);
resultIds = toUnqualifiedVersionlessIds(results);
assertThat(resultIds, empty());
assertEquals(SubscriptionStatusEnum.ACTIVE, myEntityManager.find(SubscriptionTable.class, table.getId()).getStatus());
assertEquals(SubscriptionStatusEnum.ACTIVE, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum());
mySubscriptionDao.delete(id);
assertNull(myEntityManager.find(SubscriptionTable.class, table.getId()));
/*
* Re-create again
*/
subs = new Subscription();
subs.setCriteria("Observation?subject=Patient/123");
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setId(id);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
mySubscriptionDao.update(subs);
assertEquals(SubscriptionStatusEnum.REQUESTED, myEntityManager.createQuery("SELECT t FROM SubscriptionTable t WHERE t.myResId = " + id.getIdPart(), SubscriptionTable.class).getSingleResult().getStatus());
assertEquals(SubscriptionStatusEnum.REQUESTED, mySubscriptionDao.read(id).getStatusElement().getValueAsEnum());
}
}

View File

@ -56,6 +56,7 @@ import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.composite.PeriodDt;
import ca.uhn.fhir.model.dstu2.composite.QuantityDt;
import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu2.resource.BaseResource;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.ConceptMap;
import ca.uhn.fhir.model.dstu2.resource.Device;
@ -1576,6 +1577,45 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
}
@Test
public void testReadInvalidVersion() throws Exception {
String methodName = "testReadInvalidVersion";
Patient pat = new Patient();
pat.addIdentifier().setSystem("urn:system").setValue(methodName);
IIdType id = myPatientDao.create(pat).getId();
assertEquals(methodName, myPatientDao.read(id).getIdentifier().get(0).getValue());
try {
myPatientDao.read(id.withVersion("0"));
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Version \"0\" is not valid for resource Patient/" + id.getIdPart(), e.getMessage());
}
try {
myPatientDao.read(id.withVersion("2"));
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Version \"2\" is not valid for resource Patient/" + id.getIdPart(), e.getMessage());
}
try {
myPatientDao.read(id.withVersion("H"));
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Version \"H\" is not valid for resource Patient/" + id.getIdPart(), e.getMessage());
}
try {
myPatientDao.read(new IdDt("Patient/9999999999999/_history/1"));
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Patient/9999999999999/_history/1 is not known", e.getMessage());
}
}
@Test
public void testReadWithDeletedResource() {
String methodName = "testReadWithDeletedResource";
@ -1940,6 +1980,79 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
}
@Test
public void testSortByLastUpdated() {
String methodName = "testSortByLastUpdated";
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system1").setValue(methodName);
p.addName().addFamily(methodName);
IIdType id1 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
p = new Patient();
p.addIdentifier().setSystem("urn:system2").setValue(methodName);
p.addName().addFamily(methodName);
IIdType id2 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
p = new Patient();
p.addIdentifier().setSystem("urn:system3").setValue(methodName);
p.addName().addFamily(methodName);
IIdType id3 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
p = new Patient();
p.addIdentifier().setSystem("urn:system4").setValue(methodName);
p.addName().addFamily(methodName);
IIdType id4 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
SearchParameterMap pm;
List<IIdType> actual;
pm = new SearchParameterMap();
pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED));
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
assertThat(actual, contains(id1, id2, id3, id4));
pm = new SearchParameterMap();
pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.ASC));
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
assertThat(actual, contains(id1, id2, id3, id4));
pm = new SearchParameterMap();
pm.setSort(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC));
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
assertThat(actual, contains(id4, id3, id2, id1));
pm = new SearchParameterMap();
pm.add(Patient.SP_IDENTIFIER, new TokenParam(null, methodName));
pm.setSort(new SortSpec(Patient.SP_NAME).setChain(new SortSpec(Constants.PARAM_LASTUPDATED, SortOrderEnum.DESC)));
actual = toUnqualifiedVersionlessIds(myPatientDao.search(pm));
assertThat(actual, contains(id4, id3, id2, id1));
}
@Test
public void testSortNoMatches() {
String methodName = "testSortNoMatches";
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
IIdType id1 = myPatientDao.create(p).getId().toUnqualifiedVersionless();
SearchParameterMap map;
map = new SearchParameterMap();
map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart()));
map.setLastUpdated(new DateRangeParam("2001", "2003"));
map.setSort(new SortSpec(Constants.PARAM_LASTUPDATED));
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty());
map = new SearchParameterMap();
map.add(BaseResource.SP_RES_ID, new StringParam(id1.getIdPart()));
map.setLastUpdated(new DateRangeParam("2001", "2003"));
map.setSort(new SortSpec(Patient.SP_NAME));
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), empty());
}
@Test
public void testSortById() {
String methodName = "testSortBTyId";

View File

@ -1,11 +1,14 @@
package ca.uhn.fhir.jpa.provider;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.mockito.Mockito.mock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@ -15,6 +18,11 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.DispatcherServlet;
import ca.uhn.fhir.jpa.dao.BaseJpaDstu2Test;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
@ -108,6 +116,22 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test {
servletHolder.setServlet(restServer);
proxyHandler.addServlet(servletHolder, "/fhir/context/*");
GenericWebApplicationContext webApplicationContext = new GenericWebApplicationContext();
webApplicationContext.setParent(myAppCtx);
webApplicationContext.refresh();
// ContextLoaderListener loaderListener = new ContextLoaderListener(webApplicationContext);
// loaderListener.initWebApplicationContext(mock(ServletContext.class));
//
proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, webApplicationContext);
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// dispatcherServlet.setApplicationContext(webApplicationContext);
dispatcherServlet.setContextConfigLocation("classpath:/fhir-spring-subscription-config-dstu2.xml");
ServletHolder subsServletHolder = new ServletHolder();
subsServletHolder.setServlet(dispatcherServlet);
proxyHandler.addServlet(subsServletHolder, "/*");
server.setHandler(proxyHandler);
server.start();

View File

@ -361,6 +361,20 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
}
@Test
public void testDeleteInvalidReference() throws IOException {
HttpDelete delete = new HttpDelete(ourServerBase + "/Patient");
CloseableHttpResponse response = ourHttpClient.execute(delete);
try {
String responseString = IOUtils.toString(response.getEntity().getContent());
ourLog.info(responseString);
assertEquals(400, response.getStatusLine().getStatusCode());
assertThat(responseString, containsString("Can not perform delete, no ID provided"));
} finally {
response.close();
}
}
@Test
public void testDeleteResourceConditional1() throws IOException {
String methodName = "testDeleteResourceConditional1";
@ -427,7 +441,8 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
}
/*
* Try it with a raw socket call. The Apache client won't let us use the unescaped "|" in the URL but we want to make sure that works too..
* Try it with a raw socket call. The Apache client won't let us use the unescaped "|" in the URL but we want to
* make sure that works too..
*/
Socket sock = new Socket();
sock.setSoTimeout(3000);
@ -699,8 +714,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
patient.setId(id);
ourClient.update().resource(patient).execute();
ca.uhn.fhir.model.dstu2.resource.Bundle history = ourClient.history().onInstance(id).andReturnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class).prettyPrint().summaryMode(SummaryEnum.DATA)
.execute();
ca.uhn.fhir.model.dstu2.resource.Bundle history = ourClient.history().onInstance(id).andReturnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class).prettyPrint().summaryMode(SummaryEnum.DATA).execute();
assertEquals(3, history.getEntry().size());
assertEquals(id.withVersion("3"), history.getEntry().get(0).getResource().getId());
assertEquals(1, ((Patient) history.getEntry().get(0).getResource()).getName().size());
@ -947,8 +961,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
p1.addIdentifier().setValue("testSearchByIdentifierWithoutSystem01");
IdDt p1Id = (IdDt) ourClient.create().resource(p1).execute().getId();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint()
.execute();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint().execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getResource().getId().getIdPart());
@ -1488,6 +1501,28 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
}
@Test
public void testUpdateInvalidReference() throws IOException, Exception {
String methodName = "testUpdateInvalidReference";
Patient pt = new Patient();
pt.addName().addFamily(methodName);
String resource = myFhirCtx.newXmlParser().encodeResourceToString(pt);
HttpPut post = new HttpPut(ourServerBase + "/Patient");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
try {
String responseString = IOUtils.toString(response.getEntity().getContent());
ourLog.info(responseString);
assertThat(responseString, containsString("<pre>Can not update a resource with no ID</pre>"));
assertThat(responseString, containsString("<OperationOutcome"));
assertEquals(400, response.getStatusLine().getStatusCode());
} finally {
response.close();
}
}
@Test
public void testUpdateResourceWithPrefer() throws IOException, Exception {
String methodName = "testUpdateResourceWithPrefer";

View File

@ -1,29 +1,73 @@
package ca.uhn.fhir.jpa.provider;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import java.net.URI;
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.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Test;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptor;
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.ObservationStatusEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public class SubscriptionsRequireManualActivationInterceptorTest extends BaseResourceProviderDstu2Test {
public class SubscriptionsDstu2Test extends BaseResourceProviderDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionsDstu2Test.class);
@Override
public void beforeCreateInterceptor() {
super.beforeCreateInterceptor();
SubscriptionsRequireManualActivationInterceptor interceptor = new SubscriptionsRequireManualActivationInterceptor();
interceptor.setDao(mySubscriptionDao);
myDaoConfig.getInterceptors().add(interceptor);
}
private void sleepUntilPingCount(SimpleEchoSocket socket, int wantPingCount) throws InterruptedException {
ourLog.info("Entering loop");
for (long start = System.currentTimeMillis(), now = System.currentTimeMillis(); now - start <= 20000; now = System.currentTimeMillis()) {
ourLog.debug("Starting");
if (socket.myError != null) {
fail(socket.myError);
}
if (socket.myPingCount >= wantPingCount) {
ourLog.info("Breaking loop");
break;
}
ourLog.debug("Sleeping");
Thread.sleep(100);
}
ourLog.info("Out of loop, pingcount {} error {}", socket.myPingCount, socket.myError);
assertNull(socket.myError, socket.myError);
assertEquals(wantPingCount, socket.myPingCount);
}
@Test
public void testCreateInvalidNoStatus() {
Subscription subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.getChannel().setType(SubscriptionChannelTypeEnum.REST_HOOK);
subs.setCriteria("Observation?identifier=123");
try {
ourClient.create().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status must be 'requested' on a newly created subscription", e.getMessage());
assertEquals("HTTP 422 Unprocessable Entity: Can not create resource: Subscription.status must be populated", e.getMessage());
}
subs.setId("ABC");
@ -31,24 +75,69 @@ public class SubscriptionsRequireManualActivationInterceptorTest extends BaseRes
ourClient.update().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status must be 'requested' on a newly created subscription", e.getMessage());
assertEquals("HTTP 422 Unprocessable Entity: Can not create resource: Subscription.status must be populated", e.getMessage());
}
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
ourClient.update().resource(subs).execute();
}
@Test
public void testCreateInvalidWrongStatus() {
public void testUpdateToInvalidStatus() {
Subscription subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.REST_HOOK);
subs.setCriteria("Observation?identifier=123");
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
IIdType id = ourClient.create().resource(subs).execute().getId();
subs.setId(id);
try {
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
ourClient.update().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status can not be changed from 'requested' to 'active'", e.getMessage());
}
try {
subs.setStatus((SubscriptionStatusEnum)null);
ourClient.update().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Can not update resource: Subscription.status must be populated", e.getMessage());
}
subs.setStatus(SubscriptionStatusEnum.OFF);
ourClient.update().resource(subs).execute();
}
@Test
public void testCreateWithPopulatedButInvalidStatue() {
Subscription subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setCriteria("Observation?identifier=123");
subs.getStatusElement().setValue("aaaaa");
try {
ourClient.create().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Can not create resource: Subscription.status must be populated (invalid value aaaaa)", e.getMessage());
}
}
@Test
public void testCreateInvalidWrongStatus() {
Subscription subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.REST_HOOK);
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
subs.setCriteria("Observation?identifier=123");
try {
ourClient.create().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status must be 'requested' on a newly created subscription", e.getMessage());
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status must be 'off' or 'requested' on a newly created subscription", e.getMessage());
}
subs.setId("ABC");
@ -56,14 +145,73 @@ public class SubscriptionsRequireManualActivationInterceptorTest extends BaseRes
ourClient.update().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status must be 'requested' on a newly created subscription", e.getMessage());
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status must be 'off' or 'requested' on a newly created subscription", e.getMessage());
}
}
@Test
public void testSubscriptionSimple() throws Exception {
myDaoConfig.setSubscriptionEnabled(true);
myDaoConfig.setSubscriptionPollDelay(0);
String methodName = "testSubscriptionResourcesAppear";
Patient p = new Patient();
p.addName().addFamily(methodName);
IIdType pId = myPatientDao.create(p).getId().toUnqualifiedVersionless();
Subscription subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.setCriteria("Observation?subject=Patient/" + pId.getIdPart());
subs.setStatus(SubscriptionStatusEnum.ACTIVE);
String subsId = mySubscriptionDao.create(subs).getId().getIdPart();
Thread.sleep(100);
Observation obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
IIdType afterId1 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
IIdType afterId2 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
Thread.sleep(100);
WebSocketClient client = new WebSocketClient();
SimpleEchoSocket socket = new SimpleEchoSocket(subsId);
try {
client.start();
URI echoUri = new URI("ws://localhost:" + ourPort + "/baseDstu2/websocket");
ClientUpgradeRequest request = new ClientUpgradeRequest();
client.connect(socket, echoUri, request);
ourLog.info("Connecting to : {}", echoUri);
sleepUntilPingCount(socket, 1);
obs = new Observation();
obs.getSubject().setReference(pId);
obs.setStatus(ObservationStatusEnum.FINAL);
IIdType afterId3 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
sleepUntilPingCount(socket, 2);
} finally {
try {
client.stop();
} catch (Exception e) {
ourLog.error("Failure", e);
fail(e.getMessage());
}
}
}
@Test
public void testUpdateFails() {
Subscription subs = new Subscription();
subs.getChannel().setType(SubscriptionChannelTypeEnum.WEBSOCKET);
subs.getChannel().setType(SubscriptionChannelTypeEnum.REST_HOOK);
subs.setStatus(SubscriptionStatusEnum.REQUESTED);
subs.setCriteria("Observation?identifier=123");
IIdType id = ourClient.create().resource(subs).execute().getId().toUnqualifiedVersionless();
@ -83,19 +231,62 @@ public class SubscriptionsRequireManualActivationInterceptorTest extends BaseRes
ourClient.update().resource(subs).execute();
fail();
} catch (UnprocessableEntityException e) {
assertEquals("HTTP 422 Unprocessable Entity: Subscription.status can not be changed from 'requested' to null", e.getMessage());
assertEquals("HTTP 422 Unprocessable Entity: Can not update resource: Subscription.status must be populated", e.getMessage());
}
subs.setStatus(SubscriptionStatusEnum.OFF);
}
@Override
public void beforeCreateInterceptor() {
super.beforeCreateInterceptor();
/**
* Basic Echo Client Socket
*/
@WebSocket(maxTextMessageSize = 64 * 1024)
public static class SimpleEchoSocket {
private String myError;
private boolean myGotBound;
private int myPingCount;
// @OnWebSocketClose
// public void onClose(int statusCode, String reason) {
// ourLog.info("Connection closed: {} - {}", statusCode, reason);
// this.session = null;
// this.closeLatch.countDown();
// }
private String mySubsId;
@SuppressWarnings("unused")
private Session session;
public SimpleEchoSocket(String theSubsId) {
mySubsId = theSubsId;
}
@OnWebSocketConnect
public void onConnect(Session session) {
ourLog.info("Got connect: {}", session);
this.session = session;
try {
String sending = "bind " + mySubsId;
ourLog.info("Sending: {}", sending);
session.getRemote().sendString(sending);
} catch (Throwable t) {
ourLog.error("Failure", t);
}
}
SubscriptionsRequireManualActivationInterceptor interceptor = new SubscriptionsRequireManualActivationInterceptor();
interceptor.setDao(mySubscriptionDao);
myDaoConfig.getInterceptors().add(interceptor);
@OnWebSocketMessage
public void onMessage(String theMsg) {
ourLog.info("Got msg: {}", theMsg);
if (theMsg.equals("bound " + mySubsId)) {
myGotBound = true;
} else if (myGotBound && theMsg.startsWith("ping " + mySubsId)){
myPingCount++;
} else {
myError = "Unexpected message: " + theMsg;
}
}
}
}

View File

@ -37,9 +37,9 @@
</bean>
</property>
</bean>
<bean id="myTxManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<tx:annotation-driven transaction-manager="myTxManager" />
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>

View File

@ -7,7 +7,15 @@
</encoder>
</appender>
<logger name="org.eclipse" additivity="false">
<logger name="org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator" additivity="false" info="debug">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.eclipse.jetty.websocket" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.eclipse" additivity="false" level="error">
</logger>
<logger name="ca.uhn.fhir.rest.client" additivity="false" level="info">

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd
"
default-autowire="no" default-lazy-init="false">
<context:annotation-config />
</beans>

View File

@ -0,0 +1,32 @@
<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>contextConfigLocation</param-name>
<param-value>
classpath:/spring-context-loader.xml
</param-value>
</context-param>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:/fhir-spring-subscription-config-dstu2.xml
</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

View File

@ -124,6 +124,21 @@
<artifactId>jetty-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<scope>test</scope>
</dependency>
<!--
<dependency>

View File

@ -18,6 +18,7 @@ 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.util.SubscriptionsRequireManualActivationInterceptor;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.EncodingEnum;
@ -81,6 +82,7 @@ public class TestRestfulServer extends RestfulServer {
myAppCtx = new ClassPathXmlApplicationContext(new String[] {
"hapi-fhir-server-database-config-dstu2.xml",
"hapi-fhir-server-resourceproviders-dstu2.xml",
"fhir-spring-subscription-config-dstu2.xml"
}, parentAppCtx);
setFhirContext(FhirContext.forDstu2());
beans = myAppCtx.getBean("myResourceProvidersDstu2", List.class);
@ -91,6 +93,7 @@ public class TestRestfulServer extends RestfulServer {
confProvider.setImplementationDescription(implDesc);
setServerConformanceProvider(confProvider);
baseUrlProperty = "fhir.baseurl.dstu2";
registerInterceptor(myAppCtx.getBean("mySubscriptionSecurityInterceptor", SubscriptionsRequireManualActivationInterceptor.class));
break;
}
default:

View File

@ -14,11 +14,13 @@
<context:mbean-server />
<bean id="myDaoConfig" class="ca.uhn.fhir.jpa.dao.DaoConfig">
<property name="subscriptionEnabled" value="true"></property>
<property name="subscriptionPurgeInactiveAfterSeconds" value="3600" /> <!-- 1 hour -->
<property name="subscriptionPollDelay" value="5000"></property>
</bean>
<util:list id="myServerInterceptors">
<ref bean="myLoggingInterceptor"/>
<!-- <ref bean="mySubscriptionSecurityInterceptor"/> -->
</util:list>
<!--
@ -44,10 +46,6 @@
<bean id="dbServer" class="ca.uhn.fhirtest.DerbyNetworkServer">
</bean>
<!--
<bean id="mySubscriptionSecurityInterceptor" class="ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptor"/>
-->
<!--
Do some fancy logging to create a nice access log that has details
about each incoming request.

View File

@ -34,7 +34,7 @@ public class UhnFhirTestApp {
WebAppContext root = new WebAppContext();
root.setContextPath("/");
root.setDescriptor("target/hapi-fhir-jpaserver/WEB-INF/web.xml");
root.setDescriptor("src/main/webapp/WEB-INF/web.xml");
root.setResourceBase("target/hapi-fhir-jpaserver");
root.setParentLoaderPriority(true);

View File

@ -28,16 +28,14 @@ import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class DeleteConditionalTest {
public class DeleteDstu2Test {
private static CloseableHttpClient ourClient;
private static String ourLastConditionalUrl;
private static int ourPort;
private static final FhirContext ourCtx = FhirContext.forDstu2();
private static Server ourServer;
private static IdDt ourLastIdParam;
private static boolean ourInvoked;
@ -45,6 +43,7 @@ public class DeleteConditionalTest {
public void before() {
ourLastConditionalUrl = null;
ourLastIdParam = null;
ourInvoked = false;
}
@Test
@ -62,7 +61,6 @@ public class DeleteConditionalTest {
assertEquals("Patient?identifier=system%7C001", ourLastConditionalUrl);
}
@Test
public void testUpdateWithoutConditionalUrl() throws Exception {
Patient patient = new Patient();
@ -115,9 +113,10 @@ public class DeleteConditionalTest {
@Delete()
public MethodOutcome updatePatient(@ConditionalUrlParam String theConditional, @IdParam IdDt theIdParam) {
public MethodOutcome delete(@ConditionalUrlParam String theConditional, @IdParam IdDt theIdParam) {
ourLastConditionalUrl = theConditional;
ourLastIdParam = theIdParam;
ourInvoked = true;
return new MethodOutcome(new IdDt("Patient/001/_history/002"));
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import java.util.ArrayList;

View File

@ -491,22 +491,9 @@ public final class IdType extends UriType implements IPrimitiveType<String>, IId
return true;
}
/**
* Returns <code>true</code> if the unqualified ID is a valid {@link Long}
* value (in other words, it consists only of digits)
*/
@Override
public boolean isIdPartValidLong() {
String id = getIdPart();
if (StringUtils.isBlank(id)) {
return false;
}
for (int i = 0; i < id.length(); i++) {
if (Character.isDigit(id.charAt(i)) == false) {
return false;
}
}
return true;
return isValidLong(getIdPart());
}
/**
@ -518,6 +505,11 @@ public final class IdType extends UriType implements IPrimitiveType<String>, IId
return "#".equals(myBaseUrl);
}
@Override
public boolean isVersionIdPartValidLong() {
return isValidLong(getVersionIdPart());
}
/**
* Set the value
*
@ -685,6 +677,18 @@ public final class IdType extends UriType implements IPrimitiveType<String>, IId
return value.startsWith("http://") || value.startsWith("https://");
}
private static boolean isValidLong(String id) {
if (StringUtils.isBlank(id)) {
return false;
}
for (int i = 0; i < id.length(); i++) {
if (Character.isDigit(id.charAt(i)) == false) {
return false;
}
}
return true;
}
/**
* Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new,
* randomly created UUID generated by {@link UUID#randomUUID()}

25
pom.xml
View File

@ -428,6 +428,21 @@
<artifactId>jetty-webapp</artifactId>
<version>9.2.6.v20141205</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<version>9.2.6.v20141205</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>9.2.6.v20141205</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>9.2.6.v20141205</version>
</dependency>
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
@ -508,6 +523,11 @@
<artifactId>spring-data-jpa</artifactId>
<version>1.9.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring_version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
@ -533,6 +553,11 @@
<artifactId>spring-webmvc</artifactId>
<version>${spring_version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring_version}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>

View File

@ -72,9 +72,18 @@
</action>
<action type="add">
JPA server now supports searching with sort by token, quantity,
number and URI (previously only string, date, _id and _lastUpdated
number, Uri, and _lastUpdated (previously only string, date, and _id
were supported)
</action>
<action type="fix">
Fix issue in JPA where a search with a _lastUpdated filter which matches no results
would crash if the search also had a _sort
</action>
<action type="fix">
Fix several cases where invalid requests would cause an HTTP 500 instead of
a more appropriate 400/404 in the JPA server (vread on invalid version,
delete with no ID, etc.)
</action>
</release>
<release version="1.2" date="2015-09-18">
<action type="add">