Merge pull request #1480 from jamesagnew/ks-in-memory-date-compare

%now and in-memory date compare
This commit is contained in:
James Agnew 2019-09-10 13:25:51 -04:00 committed by GitHub
commit 5020cef56b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 270 additions and 61 deletions

View File

@ -20,21 +20,20 @@ package ca.uhn.fhir.model.primitive;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import ca.uhn.fhir.model.api.BasePrimitive;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.DataFormatException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import ca.uhn.fhir.model.api.BasePrimitive;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.DataFormatException;
import static org.apache.commons.lang3.StringUtils.isBlank;
public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
static final long NANOS_PER_MILLIS = 1000000L;
@ -42,7 +41,8 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
private static final FastDateFormat ourXmlDateTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss");
public static final String NOW_DATE_CONSTANT = "%now";
private String myFractionalSeconds;
private TemporalPrecisionEnum myPrecision = null;
private TimeZone myTimeZone;
@ -635,7 +635,12 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
@Override
public void setValueAsString(String theValue) throws DataFormatException {
clearTimeZone();
super.setValueAsString(theValue);
if (NOW_DATE_CONSTANT.equalsIgnoreCase(theValue)) {
super.setValueAsString(ourXmlDateTimeFormat.format(new Date()));
} else {
super.setValueAsString(theValue);
}
}
/**

View File

@ -47,7 +47,7 @@ public abstract class BaseParamWithPrefix<T extends BaseParam> extends BaseParam
break;
} else {
char nextChar = theString.charAt(offset);
if (nextChar == '-' || Character.isDigit(nextChar)) {
if (nextChar == '-' || nextChar == '%' || Character.isDigit(nextChar)) {
break;
}
}

View File

@ -5,7 +5,11 @@ import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.util.UrlUtil;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.Observation;
import org.hl7.fhir.r5.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
@ -84,6 +88,20 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
}
@Test
public void testDateNowSyntax() {
Observation observation = new Observation();
observation.setEffective(new DateTimeType("1965-08-09"));
IIdType oid = myObservationDao.create(observation).getId().toUnqualified();
String nowParam = UrlUtil.escapeUrlParam("%now");
Bundle output = ourClient
.search()
.byUrl("Observation?date=lt" + nowParam)
.returnBundle(Bundle.class)
.execute();
List<IIdType> ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualified()).collect(Collectors.toList());
assertThat(ids, containsInAnyOrder(oid));
}
@AfterClass
public static void afterClassClearContext() {

View File

@ -107,6 +107,16 @@
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -23,7 +23,6 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
public class InMemoryMatchResult {
public static final String PARSE_FAIL = "Failed to translate parse query string";
public static final String STANDARD_PARAMETER = "Standard parameters not supported";
public static final String PREFIX = "Prefixes not supported";
public static final String CHAIN = "Chained references are not supported";
public static final String PARAM = "Param not supported";

View File

@ -29,7 +29,9 @@ import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -42,7 +44,6 @@ import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Service
public class InMemoryResourceMatcher {
@ -57,11 +58,10 @@ public class InMemoryResourceMatcher {
/**
* This method is called in two different scenarios. With a null theResource, it determines whether database matching might be required.
* Otherwise, it tries to perform the match in-memory, returning UNSUPPORTED if it's not possible.
*
* <p>
* Note that there will be cases where it returns UNSUPPORTED with a null resource, but when a non-null resource it returns supported and no match.
* This is because an earlier parameter may be matchable in-memory in which case processing stops and we never get to the parameter
* that would have required a database call.
*
*/
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
@ -86,7 +86,7 @@ public class InMemoryResourceMatcher {
String theParamName = entry.getKey();
List<List<IQueryParameterType>> theAndOrParams = entry.getValue();
InMemoryMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, resourceDefinition, theResource, theSearchParams);
if (!result.matched()){
if (!result.matched()) {
return result;
}
}
@ -102,14 +102,18 @@ public class InMemoryResourceMatcher {
if (hasQualifiers(theAndOrParams)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.STANDARD_PARAMETER);
}
if (hasPrefixes(theAndOrParams)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PREFIX);
}
if (hasChain(theAndOrParams)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.CHAIN);
}
String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
InMemoryMatchResult checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, paramDef, theAndOrParams);
if (!checkUnsupportedResult.supported()) {
return checkUnsupportedResult;
}
switch (theParamName) {
case IAnyResource.SP_RES_ID:
@ -125,8 +129,7 @@ public class InMemoryResourceMatcher {
default:
String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
}
}
@ -137,11 +140,12 @@ public class InMemoryResourceMatcher {
}
return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource));
}
private boolean matchIdsOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
}
return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam)param).getValue(), theResource.getIdElement()));
return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam) param).getValue(), theResource.getIdElement()));
}
private boolean matchId(String theValue, IIdType theId) {
@ -183,16 +187,48 @@ public class InMemoryResourceMatcher {
}
private boolean hasChain(List<List<IQueryParameterType>> theAndOrParams) {
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param instanceof ReferenceParam && ((ReferenceParam)param).getChain() != null);
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param instanceof ReferenceParam && ((ReferenceParam) param).getChain() != null);
}
private boolean hasQualifiers(List<List<IQueryParameterType>> theAndOrParams) {
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param.getQueryParameterQualifier() != null);
}
private boolean hasPrefixes(List<List<IQueryParameterType>> theAndOrParams) {
Predicate<IQueryParameterType> hasPrefixPredicate = param -> param instanceof BaseParamWithPrefix &&
((BaseParamWithPrefix) param).getPrefix() != null;
return theAndOrParams.stream().flatMap(List::stream).anyMatch(hasPrefixPredicate);
private InMemoryMatchResult checkUnsupportedPrefixes(String theParamName, RuntimeSearchParam theParamDef, List<List<IQueryParameterType>> theAndOrParams) {
if (theParamDef != null) {
for (List<IQueryParameterType> theAndOrParam : theAndOrParams) {
for (IQueryParameterType param : theAndOrParam) {
if (param instanceof BaseParamWithPrefix) {
ParamPrefixEnum prefix = ((BaseParamWithPrefix) param).getPrefix();
RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
if (!supportedPrefix(prefix, paramType)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, String.format("The prefix %s is not supported for param type %s", prefix, paramType));
}
}
}
}
}
return InMemoryMatchResult.successfulMatch();
}
private boolean supportedPrefix(ParamPrefixEnum theParam, RestSearchParameterTypeEnum theParamType) {
if (theParam == null || theParamType == null) {
return true;
}
switch (theParamType) {
case DATE:
switch (theParam) {
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
case EQUAL:
return true;
}
break;
default:
return false;
}
return false;
}
}

View File

@ -0,0 +1,157 @@
package ca.uhn.fhir.jpa.searchparam.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.model.primitive.BaseDateTimeDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import org.hl7.fhir.r5.model.BaseDateTimeType;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.Observation;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(SpringRunner.class)
public class InMemoryResourceMatcherR5Test {
public static final String OBSERVATION_DATE = "1970-10-17";
private static final String EARLY_DATE = "1965-08-09";
private static final String LATE_DATE = "2000-06-29";
@Autowired
private
InMemoryResourceMatcher myInMemoryResourceMatcher;
@MockBean
ISearchParamRegistry mySearchParamRegistry;
private Observation myObservation;
private ResourceIndexedSearchParams mySearchParams;
@Configuration
public static class SpringConfig {
@Bean
InMemoryResourceMatcher inMemoryResourceMatcher() {
return new InMemoryResourceMatcher();
}
@Bean
MatchUrlService matchUrlService() {
return new MatchUrlService();
}
@Bean
FhirContext fhirContext() {
return FhirContext.forR5();
}
}
@Before
public void before() {
RuntimeSearchParam searchParams = new RuntimeSearchParam(null, null, null, null, "Observation.effective", RestSearchParameterTypeEnum.DATE, null, null, null, RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE);
when(mySearchParamRegistry.getSearchParamByName(any(), any())).thenReturn(searchParams);
when(mySearchParamRegistry.getActiveSearchParam("Observation", "date")).thenReturn(searchParams);
myObservation = new Observation();
myObservation.setEffective(new DateTimeType(OBSERVATION_DATE));
mySearchParams = extractDateSearchParam(myObservation);
}
@Test
public void testDateUnsupportedOps() {
testDateUnsupportedOp(ParamPrefixEnum.APPROXIMATE);
testDateUnsupportedOp(ParamPrefixEnum.STARTS_AFTER);
testDateUnsupportedOp(ParamPrefixEnum.ENDS_BEFORE);
testDateUnsupportedOp(ParamPrefixEnum.NOT_EQUAL);
}
private void testDateUnsupportedOp(ParamPrefixEnum theOperator) {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=" + theOperator.getValue() + OBSERVATION_DATE, myObservation, mySearchParams);
assertFalse(result.supported());
assertEquals("Parameter: <date> Reason: The prefix " + theOperator + " is not supported for param type DATE", result.getUnsupportedReason());
}
@Test
public void testDateSupportedOps() {
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false);
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN, true, false, false);
testDateSupportedOp(ParamPrefixEnum.EQUAL, false, true, false);
testDateSupportedOp(ParamPrefixEnum.LESSTHAN_OR_EQUALS, false, true, true);
testDateSupportedOp(ParamPrefixEnum.LESSTHAN, false, false, true);
}
private void testDateSupportedOp(ParamPrefixEnum theOperator, boolean theEarly, boolean theSame, boolean theLater) {
String equation = "date=" + theOperator.getValue();
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + EARLY_DATE, myObservation, mySearchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertEquals(result.matched(), theEarly);
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + OBSERVATION_DATE, myObservation, mySearchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertEquals(result.matched(), theSame);
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + LATE_DATE, myObservation, mySearchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertEquals(result.matched(), theLater);
}
}
@Test
public void testNowPast() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=lt" + BaseDateTimeDt.NOW_DATE_CONSTANT, myObservation, mySearchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertTrue(result.matched());
}
@Test
public void testNowNextWeek() {
Observation futureObservation = new Observation();
Instant nextWeek = Instant.now().plus(Duration.ofDays(7));
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertTrue(result.matched());
}
@Test
public void testNowNextMinute() {
Observation futureObservation = new Observation();
Instant nextMinute = Instant.now().plus(Duration.ofMinutes(1));
futureObservation.setEffective(new DateTimeType(Date.from(nextMinute)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertTrue(result.matched());
}
private ResourceIndexedSearchParams extractDateSearchParam(Observation theObservation) {
ResourceIndexedSearchParams retval = new ResourceIndexedSearchParams();
BaseDateTimeType dateValue = (BaseDateTimeType) theObservation.getEffective();
ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate("date", dateValue.getValue(), dateValue.getValue(), dateValue.getValueAsString());
retval.myDateParams.add(dateParam);
return retval;
}
}

View File

@ -549,7 +549,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
CommunicationRequest cr = new CommunicationRequest();
cr.getRequester().getAgent().setReference("Organization/O1276");
cr.setOccurrence(new DateTimeType("2019-02-08T00:01:00-05:00"));
assertUnsupported(cr, criteria);
assertMatched(cr, criteria);
}

View File

@ -5,7 +5,6 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.validation.IValidationContext;
import ca.uhn.fhir.validation.IValidatorModule;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.apache.commons.lang3.Validate;

View File

@ -1,12 +1,6 @@
package org.hl7.fhir.instance.hapi.validation;
import static org.apache.commons.lang3.StringUtils.isBlank;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.validation.IValidationContext;
@ -14,11 +8,7 @@ import ca.uhn.fhir.validation.IValidatorModule;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
@ -27,16 +17,10 @@ import org.fhir.ucum.UcumService;
import org.hl7.fhir.convertors.NullVersionConverterAdvisor50;
import org.hl7.fhir.convertors.VersionConvertorAdvisor50;
import org.hl7.fhir.convertors.VersionConvertor_10_50;
import org.hl7.fhir.convertors.VersionConvertor_10_50;
import org.hl7.fhir.dstu2.model.*;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.TerminologyServiceException;
import org.hl7.fhir.dstu2.model.CodeableConcept;
import org.hl7.fhir.dstu2.model.Coding;
import org.hl7.fhir.dstu2.model.Questionnaire;
import org.hl7.fhir.dstu2.model.Resource;
import org.hl7.fhir.dstu2.model.StructureDefinition;
import org.hl7.fhir.dstu2.model.ValueSet;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.formats.IParser;
import org.hl7.fhir.r5.formats.ParserType;
@ -58,20 +42,17 @@ import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule {

View File

@ -30,9 +30,6 @@ import org.hl7.fhir.utilities.TerminologyServiceOptions;
import org.hl7.fhir.utilities.TranslationServices;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

View File

@ -562,7 +562,7 @@
<properties>
<fhir_core_version>4.0.8-SNAPSHOT</fhir_core_version>
<fhir_core_version>4.0.10-SNAPSHOT</fhir_core_version>
<ucum_version>1.0.2</ucum_version>
<!-- configure timestamp in MANIFEST.MF for maven-war-provider -->

View File

@ -121,6 +121,13 @@
The GraphQL provider did not wrap the respone in a "data" element as described in the FHIR
specification. This has been corrected.
</action>
<action type="add">
Added support for comparing resource dates to the current time via a new variable %now. E.g.
Procedure?date=gt%now would match future procedures.
</action>
<action type="add">
Add support for in-memory matching on date comparisons ge,gt,eq,lt,le.
</action>
<action type="fix">
When using the Consent Service and denying a resource via the "Will See Resource" method, the resource ID
and version were still returned to the user. This has been corrected so that no details about