Expand search range when searching by date

This commit is contained in:
James Agnew 2018-10-03 21:31:01 -04:00
parent 6ce9120132
commit b265c0281b
20 changed files with 204 additions and 53 deletions

View File

@ -23,21 +23,23 @@ package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.api.annotation.SimpleSetter;
import ca.uhn.fhir.model.primitive.BaseDateTimeDt; import ca.uhn.fhir.model.primitive.BaseDateTimeDt;
import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.Collections; import java.util.*;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -222,7 +224,7 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
if (theParameters.size() == 1) { if (theParameters.size() == 1) {
setValueAsString(theParameters.get(0)); setValueAsString(theParameters.get(0));
} else if (theParameters.size() > 1) { } else if (theParameters.size() > 1) {
throw new InvalidRequestException("This server does not support multi-valued dates for this paramater: " + theParameters); throw new InvalidRequestException("This server does not support multi-valued dates for this parameter: " + theParameters);
} }
} }

View File

@ -2,15 +2,14 @@ package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList; import java.util.*;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*; import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*;
import static java.lang.String.format; import static java.lang.String.format;
@ -260,6 +259,14 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return null; return null;
} }
Date retVal = myLowerBound.getValue(); Date retVal = myLowerBound.getValue();
if (myLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
Calendar cal = DateUtils.toCalendar(retVal);
cal.setTimeZone(TimeZone.getTimeZone("GMT-11:30"));
cal = DateUtils.truncate(cal, Calendar.DATE);
retVal = cal.getTime();
}
if (myLowerBound.getPrefix() != null) { if (myLowerBound.getPrefix() != null) {
switch (myLowerBound.getPrefix()) { switch (myLowerBound.getPrefix()) {
case GREATERTHAN: case GREATERTHAN:
@ -306,7 +313,16 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
if (myUpperBound == null) { if (myUpperBound == null) {
return null; return null;
} }
Date retVal = myUpperBound.getValue(); Date retVal = myUpperBound.getValue();
if (myLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
Calendar cal = DateUtils.toCalendar(retVal);
cal.setTimeZone(TimeZone.getTimeZone("GMT+11:30"));
cal = DateUtils.truncate(cal, Calendar.DATE);
retVal = cal.getTime();
}
if (myUpperBound.getPrefix() != null) { if (myUpperBound.getPrefix() != null) {
switch (myUpperBound.getPrefix()) { switch (myUpperBound.getPrefix()) {
case LESSTHAN: case LESSTHAN:

View File

@ -87,7 +87,7 @@ public class ParametersUtil {
addClientParameter(theContext, next, theTargetResource, paramChild, paramChildElem, theName); addClientParameter(theContext, next, theTargetResource, paramChild, paramChildElem, theName);
} }
} else { } else {
throw new IllegalArgumentException("Don't know how to handle value of type " + theValue.getClass() + " for paramater " + theName); throw new IllegalArgumentException("Don't know how to handle value of type " + theValue.getClass() + " for parameter " + theName);
} }
} }

View File

@ -1,7 +1,28 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 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 ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.param.HasAndListParam;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -17,27 +38,32 @@ public class DaoRegistry implements ApplicationContextAware {
@Autowired @Autowired
private FhirContext myCtx; private FhirContext myCtx;
private Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao = new HashMap<>(); private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao = new HashMap<>();
@Override @Override
public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException { public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
myAppCtx = theApplicationContext; myAppCtx = theApplicationContext;
} }
@PostConstruct
public void start() {
Map<String, IFhirResourceDao> resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class);
for (IFhirResourceDao nextResourceDao : resourceDaos.values()) {
RuntimeResourceDefinition nextResourceDef = myCtx.getResourceDefinition(nextResourceDao.getResourceType());
myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao);
}
}
public IFhirResourceDao<?> getResourceDao(String theResourceName) { public IFhirResourceDao<?> getResourceDao(String theResourceName) {
IFhirResourceDao<?> retVal = myResourceNameToResourceDao.get(theResourceName); IFhirResourceDao<?> retVal = getResourceNameToResourceDao().get(theResourceName);
Validate.notNull(retVal, "No DAO exists for resource type %s", theResourceName); Validate.notNull(retVal, "No DAO exists for resource type %s", theResourceName);
return retVal; return retVal;
} }
private Map<String, IFhirResourceDao<?>> getResourceNameToResourceDao() {
Map<String, IFhirResourceDao<?>> retVal = myResourceNameToResourceDao;
if (retVal == null) {
retVal = new HashMap<>();
Map<String, IFhirResourceDao> resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class);
for (IFhirResourceDao nextResourceDao : resourceDaos.values()) {
RuntimeResourceDefinition nextResourceDef = myCtx.getResourceDefinition(nextResourceDao.getResourceType());
retVal.put(nextResourceDef.getName(), nextResourceDao);
}
myResourceNameToResourceDao = retVal;
}
return retVal;
}
} }

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 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 java.util.Iterator; import java.util.Iterator;
public interface IResultIterator extends Iterator<Long> { public interface IResultIterator extends Iterator<Long> {

View File

@ -997,6 +997,8 @@ public class SearchBuilder implements ISearchBuilder {
} }
} }
ourLog.trace("Date range is {} - {}", lowerBound, upperBound);
if (lb != null && ub != null) { if (lb != null && ub != null) {
return (theBuilder.and(lb, ub)); return (theBuilder.and(lb, ub));
} else if (lb != null) { } else if (lb != null) {

View File

@ -29,16 +29,15 @@ import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.instance.model.IdType; import org.hl7.fhir.instance.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.util.List; import java.util.List;
@ -54,7 +53,7 @@ public class SubscriptionRetriggeringProvider implements IResourceProvider {
private List<BaseSubscriptionInterceptor<?>> mySubscriptionInterceptorList; private List<BaseSubscriptionInterceptor<?>> mySubscriptionInterceptorList;
@Operation(name= JpaConstants.OPERATION_RETRIGGER_SUBSCRIPTION) @Operation(name= JpaConstants.OPERATION_RETRIGGER_SUBSCRIPTION)
public IBaseOperationOutcome reTriggerSubscription( public IBaseParameters reTriggerSubscription(
@IdParam IIdType theSubscriptionId, @IdParam IIdType theSubscriptionId,
@OperationParam(name= RESOURCE_ID) UriParam theResourceId) { @OperationParam(name= RESOURCE_ID) UriParam theResourceId) {
@ -77,8 +76,10 @@ public class SubscriptionRetriggeringProvider implements IResourceProvider {
next.submitResourceModified(msg); next.submitResourceModified(msg);
} }
IBaseOperationOutcome retVal = OperationOutcomeUtil.newInstance(myFhirContext); IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
OperationOutcomeUtil.addIssue(myFhirContext, retVal, "information", "Triggered resource " + theResourceId.getValue() + " for subscription", null, null); IPrimitiveType<?> value = (IPrimitiveType<?>) myFhirContext.getElementDefinition("string").newInstance();
value.setValueAsString("Triggered resource " + theResourceId.getValue() + " for subscription");
ParametersUtil.addParameterToParameters(myFhirContext, retVal, "information", value);
return retVal; return retVal;
} }

View File

@ -105,7 +105,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
DataSource dataSource = ProxyDataSourceBuilder DataSource dataSource = ProxyDataSourceBuilder
.create(retVal) .create(retVal)
.logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") // .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS) .logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
.countQuery(new ThreadQueryCountHolder()) .countQuery(new ThreadQueryCountHolder())
.build(); .build();

View File

@ -38,10 +38,7 @@ import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.*;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -3227,6 +3224,79 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
} }
@Test
public void testDateSearchParametersShouldBeTimezoneIndependent() {
createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30");
createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00");
createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30");
createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00");
createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00");
createObservationWithEffective("YES04", "2011-01-02T00:00:00-08:00");
createObservationWithEffective("YES05", "2011-01-02T00:00:00-07:00");
createObservationWithEffective("YES06", "2011-01-02T00:00:00-06:00");
createObservationWithEffective("YES07", "2011-01-02T00:00:00-05:00");
createObservationWithEffective("YES08", "2011-01-02T00:00:00-04:00");
createObservationWithEffective("YES09", "2011-01-02T00:00:00-03:00");
createObservationWithEffective("YES10", "2011-01-02T00:00:00-02:00");
createObservationWithEffective("YES11", "2011-01-02T00:00:00-01:00");
createObservationWithEffective("YES12", "2011-01-02T00:00:00Z");
createObservationWithEffective("YES13", "2011-01-02T00:00:00+01:00");
createObservationWithEffective("YES14", "2011-01-02T00:00:00+02:00");
createObservationWithEffective("YES15", "2011-01-02T00:00:00+03:00");
createObservationWithEffective("YES16", "2011-01-02T00:00:00+04:00");
createObservationWithEffective("YES17", "2011-01-02T00:00:00+05:00");
createObservationWithEffective("YES18", "2011-01-02T00:00:00+06:00");
createObservationWithEffective("YES19", "2011-01-02T00:00:00+07:00");
createObservationWithEffective("YES20", "2011-01-02T00:00:00+08:00");
createObservationWithEffective("YES21", "2011-01-02T00:00:00+09:00");
createObservationWithEffective("YES22", "2011-01-02T00:00:00+10:00");
createObservationWithEffective("YES23", "2011-01-02T00:00:00+11:00");
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Observation.SP_DATE, new DateParam("2011-01-02"));
IBundleProvider results = myObservationDao.search(map);
List<String> values = toUnqualifiedVersionlessIdValues(results);
Collections.sort(values);
assertThat(values.toString(), values, contains(
"Observation/YES01",
"Observation/YES02",
"Observation/YES03",
"Observation/YES04",
"Observation/YES05",
"Observation/YES06",
"Observation/YES07",
"Observation/YES08",
"Observation/YES09",
"Observation/YES10",
"Observation/YES11",
"Observation/YES12",
"Observation/YES13",
"Observation/YES14",
"Observation/YES15",
"Observation/YES16",
"Observation/YES17",
"Observation/YES18",
"Observation/YES19",
"Observation/YES20",
"Observation/YES21",
"Observation/YES22",
"Observation/YES23"
));
}
private void createObservationWithEffective(String theId, String theEffective) {
Observation obs = new Observation();
obs.setId(theId);
obs.setEffective(new DateTimeType(theEffective));
myObservationDao.update(obs);
ourLog.info("Obs {} has time {}", theId, obs.getEffectiveDateTimeType().getValue().toString());
}
/** /**
* See #744 * See #744
*/ */

View File

@ -144,8 +144,8 @@ public class RetriggeringDstu3Test extends BaseResourceProviderDstu3Test {
.withParameter(Parameters.class, SubscriptionRetriggeringProvider.RESOURCE_ID, new UriType(obsId.toUnqualifiedVersionless().getValue())) .withParameter(Parameters.class, SubscriptionRetriggeringProvider.RESOURCE_ID, new UriType(obsId.toUnqualifiedVersionless().getValue()))
.execute(); .execute();
OperationOutcome oo = (OperationOutcome) response.getParameter().get(0).getResource(); String responseValue = response.getParameter().get(0).getValue().primitiveValue();
assertEquals("Triggered resource " + obsId.getValue() + " for subscription", oo.getIssue().get(0).getDiagnostics()); assertEquals("Triggered resource " + obsId.getValue() + " for subscription", responseValue);
waitForQueueToDrain(); waitForQueueToDrain();
waitForSize(0, ourCreatedObservations); waitForSize(0, ourCreatedObservations);
@ -181,8 +181,8 @@ public class RetriggeringDstu3Test extends BaseResourceProviderDstu3Test {
.withParameter(Parameters.class, SubscriptionRetriggeringProvider.RESOURCE_ID, new UriType(obsId.toUnqualifiedVersionless().getValue())) .withParameter(Parameters.class, SubscriptionRetriggeringProvider.RESOURCE_ID, new UriType(obsId.toUnqualifiedVersionless().getValue()))
.execute(); .execute();
OperationOutcome oo = (OperationOutcome) response.getParameter().get(0).getResource(); String responseValue = response.getParameter().get(0).getValue().primitiveValue();
assertEquals("Triggered resource " + obsId.getValue() + " for subscription", oo.getIssue().get(0).getDiagnostics()); assertEquals("Triggered resource " + obsId.getValue() + " for subscription", responseValue);
waitForQueueToDrain(); waitForQueueToDrain();
waitForSize(0, ourCreatedObservations); waitForSize(0, ourCreatedObservations);

View File

@ -39,6 +39,14 @@ public class DateRangeParamTest {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();
} }
@Test
public void testGetLowerRange() {
ourLog.info("Time is {}", new Date());
DateRangeParam param = new DateRangeParam(new DateParam("2011-01-02"));
ourLog.info("Adjusted time is " + param.getLowerBoundAsInstant().toString());
}
private static DateRangeParam create(String theLower, String theUpper) throws InvalidRequestException { private static DateRangeParam create(String theLower, String theUpper) throws InvalidRequestException {
DateRangeParam p = new DateRangeParam(); DateRangeParam p = new DateRangeParam();
List<QualifiedParamList> tokens = new ArrayList<QualifiedParamList>(); List<QualifiedParamList> tokens = new ArrayList<QualifiedParamList>();

View File

@ -65,6 +65,12 @@
<![CDATA[<code>DatConfig#setSearchPreFetchThresholds()</code>]]> <![CDATA[<code>DatConfig#setSearchPreFetchThresholds()</code>]]>
for configuration of this feature. for configuration of this feature.
</action> </action>
<action type="add">
When performing a JPA server using a date parameter, if a time is not specified in
the query URL, the date range is expanded slightly to include all possible
timezones where the date that could apply. This makes the search slightly more
inclusive, which errs on the side of caution.
</action>
</release> </release>
<release version="3.5.0" date="2018-09-17"> <release version="3.5.0" date="2018-09-17">