Work on subsciprions
This commit is contained in:
parent
f818a3b478
commit
9492744018
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -464,6 +452,11 @@ public class IdDt extends UriDt implements IPrimitiveDatatype<String>, IIdType {
|
|||
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()}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ca.uhn.fhir.rest.method;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
/*
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ 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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()));
|
||||
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()));
|
||||
}
|
||||
if (entity == null) {
|
||||
throw new ResourceNotFoundException(theId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -58,8 +72,12 @@ 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
|
||||
|
@ -83,8 +104,16 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
|||
q.setParameter("status", SubscriptionStatusEnum.ACTIVE);
|
||||
List<SubscriptionTable> subscriptions = q.getResultList();
|
||||
|
||||
for (SubscriptionTable nextSubscriptionTable : subscriptions) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +122,7 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
|||
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) {
|
||||
|
@ -106,7 +135,7 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
|||
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) {
|
||||
|
@ -116,11 +145,27 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
|||
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())) {
|
||||
SubscriptionFlaggedResource nextFlag = new SubscriptionFlaggedResource();
|
||||
// nextFlag.setResource();
|
||||
|
||||
Date updated = ResourceMetadataKeyEnum.PUBLISHED.get((IResource) next).getValue();
|
||||
if (mostRecentMatch == null || mostRecentMatch.getTime() < updated.getTime()) {
|
||||
mostRecentMatch = updated;
|
||||
}
|
||||
|
||||
SubscriptionFlaggedResource nextFlag = new SubscriptionFlaggedResource();
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -64,4 +76,8 @@ public class SubscriptionFlaggedResource {
|
|||
mySubscription = theSubscription;
|
||||
}
|
||||
|
||||
public void setVersion(Long theVersion) {
|
||||
myVersion = theVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package ca.uhn.fhir.jpa.subscription;
|
||||
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
public interface ISubscriptionWebsocketHandler extends WebSocketHandler {
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,18 +103,32 @@ 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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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,9 +197,92 @@ 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
|
||||
|
@ -99,13 +297,27 @@ 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());
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,7 +75,7 @@ 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);
|
||||
|
@ -39,16 +83,61 @@ public class SubscriptionsRequireManualActivationInterceptorTest extends BaseRes
|
|||
}
|
||||
|
||||
@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 {
|
||||
|
||||
SubscriptionsRequireManualActivationInterceptor interceptor = new SubscriptionsRequireManualActivationInterceptor();
|
||||
interceptor.setDao(mySubscriptionDao);
|
||||
myDaoConfig.getInterceptors().add(interceptor);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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
25
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue