diff --git a/examples/src/main/java/example/GenericClientExample.java b/examples/src/main/java/example/GenericClientExample.java index 348f779b35f..8aa03d4af05 100644 --- a/examples/src/main/java/example/GenericClientExample.java +++ b/examples/src/main/java/example/GenericClientExample.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue; import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.dstu2.resource.Provenance; import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum; import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum; import ca.uhn.fhir.model.primitive.DateDt; @@ -25,6 +26,7 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.method.SearchStyleEnum; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; public class GenericClientExample { @@ -268,6 +270,8 @@ public class GenericClientExample { .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) .and(Patient.BIRTHDATE.after().day("2011-01-01")) .include(Patient.INCLUDE_ORGANIZATION) + .revInclude(Provenance.INCLUDE_TARGET) + .lastUpdated(new DateRangeParam("2011-01-01", null)) .sort().ascending(Patient.BIRTHDATE) .sort().descending(Patient.NAME).limitTo(123) .returnBundle(Bundle.class) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java index bc44b064bf6..9d5796e2066 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java @@ -445,7 +445,7 @@ public abstract class BaseDateTimeDt extends BasePrimitive { * @throws DataFormatException */ public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException { - clearTimeZone(); + setTimeZone(TimeZone.getDefault()); myPrecision = thePrecision; super.setValue(theValue); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index 06f3595fa49..495941c3c32 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -120,6 +120,8 @@ import ca.uhn.fhir.rest.method.SearchStyleEnum; import ca.uhn.fhir.rest.method.TransactionMethodBinding; import ca.uhn.fhir.rest.method.ValidateMethodBindingDstu1; import ca.uhn.fhir.rest.method.ValidateMethodBindingDstu2; +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.EncodingEnum; import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; @@ -634,7 +636,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { + public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); @@ -1468,6 +1470,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private Class myReturnBundleType; private List myRevInclude = new ArrayList(); private SearchStyleEnum mySearchStyle; + private DateRangeParam myLastUpdated; private List mySort = new ArrayList(); public SearchInternal() { @@ -1507,6 +1510,12 @@ public class GenericClient extends BaseClient implements IGenericClient { if (myParamLimit != null) { addParam(params, Constants.PARAM_COUNT, Integer.toString(myParamLimit)); } + + if (myLastUpdated != null) { + for (DateParam next : myLastUpdated.getValuesAsQueryTokens()) { + addParam(params, Constants.PARAM_LASTUPDATED, next.getValueAsQueryToken()); + } + } if (myReturnBundleType == null && myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU2_HL7ORG)) { throw new IllegalArgumentException("When using the client with HL7.org structures, you must specify " + "the bundle return type for the client by adding \".returnBundle(org.hl7.fhir.instance.model.Bundle.class)\" to your search method call before the \".execute()\" method"); @@ -1612,6 +1621,12 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } + @Override + public IQuery lastUpdated(DateRangeParam theLastUpdated) { + myLastUpdated = theLastUpdated; + return this; + } + } @SuppressWarnings("rawtypes") diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IQuery.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IQuery.java index a4a6bed93fe..ae26266ca3b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IQuery.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IQuery.java @@ -24,6 +24,7 @@ import org.hl7.fhir.instance.model.api.IBaseBundle; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.method.SearchStyleEnum; +import ca.uhn.fhir.rest.param.DateRangeParam; public interface IQuery extends IClientExecutable, T>, IBaseQuery> { @@ -50,10 +51,17 @@ public interface IQuery extends IClientExecutable, T>, IBaseQuery revInclude(Include theIncludeTarget); + /** + * Add a "_lastUpdated" specification + * + * @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2 + */ + IQuery lastUpdated(DateRangeParam theLastUpdated); + /** * Request that the client return the specified bundle type, e.g. org.hl7.fhir.instance.model.Bundle.class * or ca.uhn.fhir.model.dstu2.resource.Bundle.class diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java index a77ba512dce..d05f1851ac9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java @@ -60,6 +60,14 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery setValueAsString(theDate); } + /** + * Constructor + */ + public DateParam(QuantityCompararatorEnum theComparator, DateTimeDt theDate) { + myComparator = theComparator; + setValueAsString(theDate != null ? theDate.getValueAsString() : null); + } + /** * Constructor which takes a complete [qualifier]{date} string. * diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java index b12c8d68f1c..4a26354a01c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java @@ -26,6 +26,7 @@ import java.util.List; import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; +import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.method.QualifiedParamList; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -48,15 +49,29 @@ public class DateRangeParam implements IQueryParameterAnd { * * @param theLowerBound * A qualified date param representing the lower date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00". Will be treated inclusively. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. * @param theUpperBound * A qualified date param representing the upper date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00". Will be treated inclusively. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. */ public DateRangeParam(Date theLowerBound, Date theUpperBound) { setRangeFromDatesInclusive(theLowerBound, theUpperBound); } + /** + * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends) + * + * @param theLowerBound + * A qualified date param representing the lower date bound (optionally may include time), e.g. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. + * @param theUpperBound + * A qualified date param representing the upper date bound (optionally may include time), e.g. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. + */ + public DateRangeParam(DateTimeDt theLowerBound, DateTimeDt theUpperBound) { + setRangeFromDatesInclusive(theLowerBound, theUpperBound); + } + /** * Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound * (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the @@ -96,10 +111,10 @@ public class DateRangeParam implements IQueryParameterAnd { * * @param theLowerBound * An unqualified date param representing the lower date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00" + * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. * @param theUpperBound * An unqualified date param representing the upper date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00" + * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. */ public DateRangeParam(String theLowerBound, String theUpperBound) { setRangeFromDatesInclusive(theLowerBound, theUpperBound); @@ -177,14 +192,14 @@ public class DateRangeParam implements IQueryParameterAnd { * * @param theLowerBound * A qualified date param representing the lower date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00". Will be treated inclusively. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. * @param theUpperBound * A qualified date param representing the upper date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00". Will be treated inclusively. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. */ public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) { - myLowerBound = new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound); - myUpperBound = new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound); + myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null; + myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null; validateAndThrowDataFormatExceptionIfInvalid(); } @@ -193,14 +208,30 @@ public class DateRangeParam implements IQueryParameterAnd { * * @param theLowerBound * A qualified date param representing the lower date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00". Will be treated inclusively. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. * @param theUpperBound * A qualified date param representing the upper date bound (optionally may include time), e.g. - * "2011-02-22" or "2011-02-22T13:12:00". Will be treated inclusively. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. + */ + public void setRangeFromDatesInclusive(DateTimeDt theLowerBound, DateTimeDt theUpperBound) { + myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null; + myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null; + validateAndThrowDataFormatExceptionIfInvalid(); + } + + /** + * Sets the range from a pair of dates, inclusive on both ends + * + * @param theLowerBound + * A qualified date param representing the lower date bound (optionally may include time), e.g. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. + * @param theUpperBound + * A qualified date param representing the upper date bound (optionally may include time), e.g. + * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null. */ public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) { - myLowerBound = new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound); - myUpperBound = new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound); + myLowerBound = theLowerBound != null ? new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theLowerBound) : null; + myUpperBound = theUpperBound != null ? new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theUpperBound) : null; validateAndThrowDataFormatExceptionIfInvalid(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index 2967aae6a84..7f90a5e45a4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -96,6 +96,7 @@ public class Constants { public static final String PARAM_FORMAT = "_format"; public static final String PARAM_HISTORY = "_history"; public static final String PARAM_INCLUDE = "_include"; + public static final String PARAM_LASTUPDATED = "_lastUpdated"; public static final String PARAM_NARRATIVE = "_narrative"; public static final String PARAM_PAGINGACTION = "_getpages"; public static final String PARAM_PAGINGOFFSET = "_getpagesoffset"; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java index e6881c8bbdc..2e3e7cc7671 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java @@ -98,6 +98,8 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.method.MethodUtil; import ca.uhn.fhir.rest.method.QualifiedParamList; import ca.uhn.fhir.rest.method.RestSearchParameterTypeEnum; +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.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -635,7 +637,7 @@ public abstract class BaseFhirDao implements IDao { return ids; } - protected SearchParameterMap translateMatchUrl(String theMatchUrl, RuntimeResourceDefinition resourceDef) { + static SearchParameterMap translateMatchUrl(String theMatchUrl, RuntimeResourceDefinition resourceDef) { SearchParameterMap paramMap = new SearchParameterMap(); List parameters; try { @@ -644,6 +646,10 @@ public abstract class BaseFhirDao implements IDao { throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Error was: URL does not contain any parameters ('?' not detected)"); } matchUrl = matchUrl.replace("|", "%7C"); + matchUrl = matchUrl.replace("=>=", "=%3E%3D"); + matchUrl = matchUrl.replace("=<=", "=%3C%3D"); + matchUrl = matchUrl.replace("=>", "=%3E"); + matchUrl = matchUrl.replace("=<", "=%3C"); parameters = URLEncodedUtils.parse(new URI(matchUrl), "UTF-8"); } catch (URISyntaxException e) { throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Error was: " + e.toString()); @@ -669,12 +675,25 @@ public abstract class BaseFhirDao implements IDao { } for (String nextParamName : nameToParamLists.keySet()) { + List paramList = nameToParamLists.get(nextParamName); + if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) { + if (paramList != null && paramList.size() > 0) { + if (paramList.size() > 2) { + throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions"); + } else { + DateRangeParam p1 = new DateRangeParam(); + p1.setValuesAsQueryTokens(paramList); + paramMap.setLastUpdated(p1); + } + } + continue; + } + RuntimeSearchParam paramDef = resourceDef.getSearchParam(nextParamName); if (paramDef == null) { throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); } - List paramList = nameToParamLists.get(nextParamName); IQueryParameterAnd param = MethodUtil.parseQueryParams(paramDef, nextParamName, paramList); paramMap.add(nextParamName, param); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java index 41b20daca1e..680b31e8893 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirResourceDao.java @@ -1620,9 +1620,34 @@ public abstract class BaseFhirResourceDao extends BaseFhirD } } - final List pids; + // Handle _lastUpdated + DateRangeParam lu = theParams.getLastUpdated(); + if (lu != null && (lu.getLowerBoundAsInstant() != null || lu.getUpperBoundAsInstant() != null)) { + + CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); + CriteriaQuery cq = builder.createQuery(Long.class); + Root from = cq.from(ResourceTable.class); + cq.select(from.get("myId").as(Long.class)); + + Predicate predicateIds = (from.get("myId").in(loadPids)); + Predicate predicateLower = lu.getLowerBoundAsInstant() != null ? builder.greaterThanOrEqualTo(from.get("myUpdated"), lu.getLowerBoundAsInstant()) : null; + Predicate predicateUpper = lu.getUpperBoundAsInstant() != null ? builder.lessThanOrEqualTo(from.get("myUpdated"), lu.getUpperBoundAsInstant()) : null; + if (predicateLower != null && predicateUpper != null) { + cq.where(predicateIds, predicateLower, predicateUpper); + } else if (predicateLower != null) { + cq.where(predicateIds, predicateLower); + } else { + cq.where(predicateIds, predicateUpper); + } + TypedQuery query = myEntityManager.createQuery(cq); + loadPids.clear(); + for (Long next : query.getResultList()) { + loadPids.add(next); + } + } // Handle sorting if any was provided + final List pids; if (theParams.getSort() != null && isNotBlank(theParams.getSort().getParamName())) { List orders = new ArrayList(); List predicates = new ArrayList(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java index b94a4f8c64d..c5204d75d93 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; 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; public class SearchParameterMap extends HashMap>> { @@ -41,7 +42,9 @@ public class SearchParameterMap extends HashMap myIncludes; + private DateRangeParam myLastUpdated; private Set myRevIncludes; + private SortSpec mySort; public void add(String theName, IQueryParameterAnd theAnd) { @@ -98,6 +101,10 @@ public class SearchParameterMap extends HashMap getRevIncludes() { return myRevIncludes; } @@ -114,6 +121,10 @@ public class SearchParameterMap extends HashMap theRevIncludes) { myRevIncludes = theRevIncludes; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseFhirDaoTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseFhirDaoTest.java new file mode 100644 index 00000000000..82ef8eac653 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseFhirDaoTest.java @@ -0,0 +1,20 @@ +package ca.uhn.fhir.jpa.dao; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.resource.Condition; + +public class BaseFhirDaoTest { + + private static FhirContext ourCtx = FhirContext.forDstu2(); + + @Test + public void testTranslateMatchUrl() { + SearchParameterMap match = BaseFhirDao.translateMatchUrl("Condition?subject=304&_lastUpdated=>2011-01-01T11:12:21.0000Z", ourCtx.getResourceDefinition(Condition.class)); + assertEquals("2011-01-01T11:12:21.0000Z", match.getLastUpdated().getLowerBound().getValueAsString()); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java index d0978f91feb..1749b67e9da 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2Test.java @@ -7,6 +7,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -42,6 +43,7 @@ import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; @@ -1309,6 +1311,75 @@ public class FhirResourceDaoDstu2Test { } + @Test + public void testSearchLastUpdatedParam() throws InterruptedException { + String methodName = "testSearchLastUpdatedParam"; + + int sleep = 100; + Thread.sleep(sleep); + + DateTimeDt beforeAny = new DateTimeDt(new Date(), TemporalPrecisionEnum.MILLI); + IdDt id1a; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().addFamily(methodName).addGiven("Joe"); + id1a = ourPatientDao.create(patient).getId().toUnqualifiedVersionless(); + } + IdDt id1b; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().addFamily(methodName + "XXXX").addGiven("Joe"); + id1b = ourPatientDao.create(patient).getId().toUnqualifiedVersionless(); + } + + Thread.sleep(1100); + DateTimeDt beforeR2 = new DateTimeDt(new Date(), TemporalPrecisionEnum.MILLI); + Thread.sleep(1100); + + IdDt id2; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().addFamily(methodName).addGiven("John"); + id2 = ourPatientDao.create(patient).getId().toUnqualifiedVersionless(); + } + + { + SearchParameterMap params = new SearchParameterMap(); + List patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params)); + assertThat(patients, hasItems(id1a, id1b, id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(beforeAny, null)); + List patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params)); + assertThat(patients, hasItems(id1a, id1b, id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(beforeR2, null)); + List patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params)); + assertThat(patients, hasItems(id2)); + assertThat(patients, not(hasItems(id1a, id1b))); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(beforeAny, beforeR2)); + List patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params)); + assertThat(patients.toString(), patients, not(hasItems(id2))); + assertThat(patients.toString(), patients, (hasItems(id1a, id1b))); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(null, beforeR2)); + List patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params)); + assertThat(patients, (hasItems(id1a, id1b))); + assertThat(patients, not(hasItems(id2))); + } + } + @Test public void testSearchNameParam() { IdDt id1; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index 1d7ce0edf08..364bd91e5f4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInRelativeOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; @@ -54,6 +55,7 @@ import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.dstu.resource.Device; import ca.uhn.fhir.model.dstu.resource.Practitioner; import ca.uhn.fhir.model.dstu2.composite.PeriodDt; @@ -74,6 +76,7 @@ import ca.uhn.fhir.model.dstu2.valueset.EncounterClassEnum; import ca.uhn.fhir.model.dstu2.valueset.EncounterStateEnum; import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; import ca.uhn.fhir.model.primitive.DateDt; +import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.UnsignedIntDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -85,6 +88,7 @@ import ca.uhn.fhir.rest.client.ServerValidationModeEnum; import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.IResourceProvider; @@ -1102,5 +1106,102 @@ public class ResourceProviderDstu2Test { ourHttpClient = builder.build(); } + + + + @Test + public void testSearchLastUpdatedParamRp() throws InterruptedException { + String methodName = "testSearchLastUpdatedParamRp"; + + int sleep = 100; + Thread.sleep(sleep); + + DateTimeDt beforeAny = new DateTimeDt(new Date(), TemporalPrecisionEnum.MILLI); + IdDt id1a; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().addFamily(methodName).addGiven("Joe"); + id1a = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless(); + } + IdDt id1b; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().addFamily(methodName + "XXXX").addGiven("Joe"); + id1b = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless(); + } + + Thread.sleep(1100); + DateTimeDt beforeR2 = new DateTimeDt(new Date(), TemporalPrecisionEnum.MILLI); + Thread.sleep(1100); + + IdDt id2; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().addFamily(methodName).addGiven("John"); + id2 = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless(); + } + + { + //@formatter:off + Bundle found = ourClient.search() + .forResource(Patient.class) + .where(Patient.NAME.matches().value("testSearchLastUpdatedParamRp")) + .execute(); + //@formatter:on + List patients = toIdListUnqualifiedVersionless(found); + assertThat(patients, hasItems(id1a, id1b, id2)); + } + { + //@formatter:off + Bundle found = ourClient.search() + .forResource(Patient.class) + .where(Patient.NAME.matches().value("testSearchLastUpdatedParamRp")) + .lastUpdated(new DateRangeParam(beforeAny, null)) + .execute(); + //@formatter:on + List patients = toIdListUnqualifiedVersionless(found); + assertThat(patients, hasItems(id1a, id1b, id2)); + } + { + //@formatter:off + Bundle found = ourClient.search() + .forResource(Patient.class) + .where(Patient.NAME.matches().value("testSearchLastUpdatedParamRp")) + .lastUpdated(new DateRangeParam(beforeR2, null)) + .execute(); + //@formatter:on + List patients = toIdListUnqualifiedVersionless(found); + assertThat(patients, hasItems(id2)); + assertThat(patients, not(hasItems(id1a, id1b))); + } + { + //@formatter:off + Bundle found = ourClient.search() + .forResource(Patient.class) + .where(Patient.NAME.matches().value("testSearchLastUpdatedParamRp")) + .lastUpdated(new DateRangeParam(beforeAny, beforeR2)) + .execute(); + //@formatter:on + List patients = toIdListUnqualifiedVersionless(found); + assertThat(patients.toString(), patients, not(hasItems(id2))); + assertThat(patients.toString(), patients, (hasItems(id1a, id1b))); + } + { + //@formatter:off + Bundle found = ourClient.search() + .forResource(Patient.class) + .where(Patient.NAME.matches().value("testSearchLastUpdatedParamRp")) + .lastUpdated(new DateRangeParam(null, beforeR2)) + .execute(); + //@formatter:on + List patients = toIdListUnqualifiedVersionless(found); + assertThat(patients, (hasItems(id1a, id1b))); + assertThat(patients, not(hasItems(id2))); + } + } + } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java index e9138ae73e6..596a27ff2b4 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java @@ -44,6 +44,7 @@ import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; @@ -792,6 +793,31 @@ public class GenericClientDstu2Test { } + @Test + public void testSearchWithLastUpdated() throws Exception { + String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + //@formatter:off + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .lastUpdated(new DateRangeParam("2011-01-01", "2012-01-01")) + .execute(); + //@formatter:on + + assertEquals("http://example.com/fhir/Patient?name=james&_lastUpdated=%3E%3D2011-01-01&_lastUpdated=%3C%3D2012-01-01", capt.getValue().getURI().toString()); + assertEquals(Patient.class, response.getEntries().get(0).getResource().getClass()); + + } + @SuppressWarnings("unused") @Test public void testSearchWithReverseInclude() throws Exception { diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm index 300e95c0a8d..a7d3e72d533 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm @@ -70,6 +70,9 @@ public class ${className}ResourceProvider extends JpaResourceProvider${versionCa #if ( $version != 'dstu' ) @IncludeParam(reverse=true) Set theRevIncludes, + @Description(shortDefinition="Only return resources which were last updated as specified by the given range") + @OptionalParam(name="_lastUpdated") + DateRangeParam theLastUpdated, #end @IncludeParam(allow= { @@ -105,6 +108,7 @@ public class ${className}ResourceProvider extends JpaResourceProvider${versionCa #end #if ( $version != 'dstu' ) paramMap.setRevIncludes(theRevIncludes); + paramMap.setLastUpdated(theLastUpdated); #end paramMap.setIncludes(theIncludes); paramMap.setSort(theSort); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 87a3a152785..3e61605f653 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -111,6 +111,14 @@ JPA server now supports ifNoneMatch in GET within a transaction request. + + DateRangeParam now supports null values in the constructor for lower or upper bounds (but + still not both) + + + Generic/fluent client and JPA server now both support _lastUpdated search parameter + which was added in DSTU2 + diff --git a/src/site/xdoc/doc_rest_client.xml b/src/site/xdoc/doc_rest_client.xml index 2cb51776370..5650698cc68 100644 --- a/src/site/xdoc/doc_rest_client.xml +++ b/src/site/xdoc/doc_rest_client.xml @@ -178,7 +178,7 @@

Search - Other Query Options

The fluent search also has methods for sorting, limiting, specifying - JSON encoding, etc. + JSON encoding, _include, _revinclude, _lastUpdated, etc.