Add support for _lastUpdated parameter in client and JPA server

This commit is contained in:
James Agnew 2015-06-19 17:46:14 -04:00
parent 886fa76ef2
commit e8c75c5a45
17 changed files with 371 additions and 19 deletions

View File

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

View File

@ -445,7 +445,7 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
* @throws DataFormatException
*/
public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
clearTimeZone();
setTimeZone(TimeZone.getDefault());
myPrecision = thePrecision;
super.setValue(theValue);
}

View File

@ -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<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> 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<? extends IBaseBundle> myReturnBundleType;
private List<Include> myRevInclude = new ArrayList<Include>();
private SearchStyleEnum mySearchStyle;
private DateRangeParam myLastUpdated;
private List<SortInternal> mySort = new ArrayList<SortInternal>();
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")

View File

@ -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<T> extends IClientExecutable<IQuery<T>, T>, IBaseQuery<IQuery<T>> {
@ -50,10 +51,17 @@ public interface IQuery<T> extends IClientExecutable<IQuery<T>, T>, IBaseQuery<I
/**
* Add a "_revinclude" specification
*
* @since 1.0
* @since HAPI FHIR 1.0 - Note that option was added to FHIR itself in DSTU2
*/
IQuery<T> revInclude(Include theIncludeTarget);
/**
* Add a "_lastUpdated" specification
*
* @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2
*/
IQuery<T> lastUpdated(DateRangeParam theLastUpdated);
/**
* Request that the client return the specified bundle type, e.g. <code>org.hl7.fhir.instance.model.Bundle.class</code>
* or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code>

View File

@ -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.
*

View File

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

View File

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

View File

@ -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<NameValuePair> 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<QualifiedParamList> 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<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
IQueryParameterAnd<?> param = MethodUtil.parseQueryParams(paramDef, nextParamName, paramList);
paramMap.add(nextParamName, param);
}

View File

@ -1620,9 +1620,34 @@ public abstract class BaseFhirResourceDao<T extends IResource> extends BaseFhirD
}
}
final List<Long> pids;
// Handle _lastUpdated
DateRangeParam lu = theParams.getLastUpdated();
if (lu != null && (lu.getLowerBoundAsInstant() != null || lu.getUpperBoundAsInstant() != null)) {
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
Root<ResourceTable> 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.<Date>get("myUpdated"), lu.getLowerBoundAsInstant()) : null;
Predicate predicateUpper = lu.getUpperBoundAsInstant() != null ? builder.lessThanOrEqualTo(from.<Date>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<Long> query = myEntityManager.createQuery(cq);
loadPids.clear();
for (Long next : query.getResultList()) {
loadPids.add(next);
}
}
// Handle sorting if any was provided
final List<Long> pids;
if (theParams.getSort() != null && isNotBlank(theParams.getSort().getParamName())) {
List<Order> orders = new ArrayList<Order>();
List<Predicate> predicates = new ArrayList<Predicate>();

View File

@ -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<String, List<List<? extends IQueryParameterType>>> {
@ -41,7 +42,9 @@ public class SearchParameterMap extends HashMap<String, List<List<? extends IQue
private Integer myCount;
private Set<Include> myIncludes;
private DateRangeParam myLastUpdated;
private Set<Include> myRevIncludes;
private SortSpec mySort;
public void add(String theName, IQueryParameterAnd<?> theAnd) {
@ -98,6 +101,10 @@ public class SearchParameterMap extends HashMap<String, List<List<? extends IQue
return myIncludes;
}
public DateRangeParam getLastUpdated() {
return myLastUpdated;
}
public Set<Include> getRevIncludes() {
return myRevIncludes;
}
@ -114,6 +121,10 @@ public class SearchParameterMap extends HashMap<String, List<List<? extends IQue
myIncludes = theIncludes;
}
public void setLastUpdated(DateRangeParam theLastUpdated) {
myLastUpdated = theLastUpdated;
}
public void setRevIncludes(Set<Include> theRevIncludes) {
myRevIncludes = theRevIncludes;
}

View File

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

View File

@ -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<IdDt> patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params));
assertThat(patients, hasItems(id1a, id1b, id2));
}
{
SearchParameterMap params = new SearchParameterMap();
params.setLastUpdated(new DateRangeParam(beforeAny, null));
List<IdDt> patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params));
assertThat(patients, hasItems(id1a, id1b, id2));
}
{
SearchParameterMap params = new SearchParameterMap();
params.setLastUpdated(new DateRangeParam(beforeR2, null));
List<IdDt> 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<IdDt> 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<IdDt> patients = toUnqualifiedVersionlessIds(ourPatientDao.search(params));
assertThat(patients, (hasItems(id1a, id1b)));
assertThat(patients, not(hasItems(id2)));
}
}
@Test
public void testSearchNameParam() {
IdDt id1;

View File

@ -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<IdDt> 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<IdDt> 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<IdDt> 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<IdDt> 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<IdDt> patients = toIdListUnqualifiedVersionless(found);
assertThat(patients, (hasItems(id1a, id1b)));
assertThat(patients, not(hasItems(id2)));
}
}
}

View File

@ -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<HttpUriRequest> 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 {

View File

@ -70,6 +70,9 @@ public class ${className}ResourceProvider extends JpaResourceProvider${versionCa
#if ( $version != 'dstu' )
@IncludeParam(reverse=true)
Set<Include> 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);

View File

@ -111,6 +111,14 @@
<action type="add">
JPA server now supports ifNoneMatch in GET within a transaction request.
</action>
<action type="add">
DateRangeParam now supports null values in the constructor for lower or upper bounds (but
still not both)
</action>
<action type="add">
Generic/fluent client and JPA server now both support _lastUpdated search parameter
which was added in DSTU2
</action>
</release>
<release version="1.0" date="2015-May-8">
<action type="add">

View File

@ -178,7 +178,7 @@
<h4>Search - Other Query Options</h4>
<p>
The fluent search also has methods for sorting, limiting, specifying
JSON encoding, etc.
JSON encoding, _include, _revinclude, _lastUpdated, etc.
</p>
<macro name="snippet">
<param name="id" value="searchAdv" />