Merge pull request #1730 from jamesagnew/date-match-bug
Add integer date fields to ResourceIndexedSearchParamDate
This commit is contained in:
commit
8fce86bba5
|
@ -6,7 +6,7 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
|||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import ca.uhn.fhir.util.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import java.util.*;
|
||||
|
@ -263,6 +263,67 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current lower bound as an integer representative of the date.
|
||||
*
|
||||
* e.g. 2019-02-22T04:22:00-0500 -> 20120922
|
||||
*/
|
||||
public Integer getLowerBoundAsDateInteger() {
|
||||
if (myLowerBound == null || myLowerBound.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
int retVal = DateUtils.convertDatetoDayInteger(myLowerBound.getValue());
|
||||
|
||||
if (myLowerBound.getPrefix() != null) {
|
||||
switch (myLowerBound.getPrefix()) {
|
||||
case GREATERTHAN:
|
||||
case STARTS_AFTER:
|
||||
retVal += 1;
|
||||
break;
|
||||
case EQUAL:
|
||||
case GREATERTHAN_OR_EQUALS:
|
||||
break;
|
||||
case LESSTHAN:
|
||||
case APPROXIMATE:
|
||||
case LESSTHAN_OR_EQUALS:
|
||||
case ENDS_BEFORE:
|
||||
case NOT_EQUAL:
|
||||
throw new IllegalStateException("Invalid lower bound comparator: " + myLowerBound.getPrefix());
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current upper bound as an integer representative of the date
|
||||
*
|
||||
* e.g. 2019-02-22T04:22:00-0500 -> 2019122
|
||||
*/
|
||||
public Integer getUpperBoundAsDateInteger() {
|
||||
if (myUpperBound == null || myUpperBound.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
int retVal = DateUtils.convertDatetoDayInteger(myUpperBound.getValue());
|
||||
if (myUpperBound.getPrefix() != null) {
|
||||
switch (myUpperBound.getPrefix()) {
|
||||
case LESSTHAN:
|
||||
case ENDS_BEFORE:
|
||||
retVal -= 1;
|
||||
break;
|
||||
case EQUAL:
|
||||
case LESSTHAN_OR_EQUALS:
|
||||
break;
|
||||
case GREATERTHAN_OR_EQUALS:
|
||||
case GREATERTHAN:
|
||||
case APPROXIMATE:
|
||||
case NOT_EQUAL:
|
||||
case STARTS_AFTER:
|
||||
throw new IllegalStateException("Invalid upper bound comparator: " + myUpperBound.getPrefix());
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public Date getLowerBoundAsInstant() {
|
||||
if (myLowerBound == null || myLowerBound.getValue() == null) {
|
||||
return null;
|
||||
|
@ -270,10 +331,7 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
|||
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();
|
||||
retVal = DateUtils.getLowestInstantFromDate(retVal);
|
||||
}
|
||||
|
||||
if (myLowerBound.getPrefix() != null) {
|
||||
|
@ -335,10 +393,7 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
|||
Date retVal = myUpperBound.getValue();
|
||||
|
||||
if (myUpperBound.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();
|
||||
retVal = DateUtils.getHighestInstantFromDate(retVal);
|
||||
}
|
||||
|
||||
if (myUpperBound.getPrefix() != null) {
|
||||
|
|
|
@ -65,6 +65,8 @@ public final class DateUtils {
|
|||
@SuppressWarnings("WeakerAccess")
|
||||
public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
|
||||
|
||||
private static final String PATTERN_INTEGER_DATE = "yyyyMMdd";
|
||||
|
||||
private static final String[] DEFAULT_PATTERNS = new String[]{
|
||||
PATTERN_RFC1123,
|
||||
PATTERN_RFC1036,
|
||||
|
@ -153,6 +155,35 @@ public final class DateUtils {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static Date getHighestInstantFromDate(Date theDateValue) {
|
||||
return getInstantFromDateWithTimezone(theDateValue, TimeZone.getTimeZone("GMT+11:30"));
|
||||
|
||||
}
|
||||
public static Date getLowestInstantFromDate(Date theDateValue) {
|
||||
return getInstantFromDateWithTimezone(theDateValue, TimeZone.getTimeZone("GMT-11:30"));
|
||||
}
|
||||
|
||||
public static Date getInstantFromDateWithTimezone(Date theDateValue, TimeZone theTimezone) {
|
||||
Calendar cal = org.apache.commons.lang3.time.DateUtils.toCalendar(theDateValue);
|
||||
cal.setTimeZone(theTimezone);
|
||||
cal = org.apache.commons.lang3.time.DateUtils.truncate(cal, Calendar.DATE);
|
||||
return cal.getTime();
|
||||
}
|
||||
|
||||
public static int convertDatetoDayInteger(final Date theDateValue) {
|
||||
notNull(theDateValue, "Date value");
|
||||
SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE);
|
||||
String theDateString = format.format(theDateValue);
|
||||
return Integer.parseInt(theDateString);
|
||||
}
|
||||
|
||||
public static String convertDateToIso8601String(final Date theDateValue){
|
||||
notNull(theDateValue, "Date value");
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||
return format.format(theDateValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given date according to the RFC 1123 pattern.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 1499
|
||||
title: When performing a search with a DateParam that has DAY precision, rely on new ordinal date field for comparison
|
||||
instead of attempting to find oldest and newest instant that could be valid.
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
The HAPI FHIR JPA Server fully implements most [FHIR search](https://www.hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR:
|
||||
|
||||
### Date searches without timestamp
|
||||
|
||||
Searching by date with no timestamp currently doesn't match all records it should. See [Issue 1499](https://github.com/jamesagnew/hapi-fhir/issues/1499).
|
||||
|
||||
### Chains within _has
|
||||
|
||||
Chains within _has are not currently supported for performance reasons. For example, this search is not currently supported
|
||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder;
|
|||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
||||
|
@ -150,67 +151,95 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
|||
return p;
|
||||
}
|
||||
|
||||
private boolean isNullOrDayPrecision(DateParam theDateParam) {
|
||||
return theDateParam == null || theDateParam.getPrecision().ordinal() == TemporalPrecisionEnum.DAY.ordinal();
|
||||
}
|
||||
|
||||
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,
|
||||
From<?, ResourceIndexedSearchParamDate> theFrom,
|
||||
DateRangeParam theRange,
|
||||
SearchFilterParser.CompareOperation operation) {
|
||||
Date lowerBound = theRange.getLowerBoundAsInstant();
|
||||
Date upperBound = theRange.getUpperBoundAsInstant();
|
||||
Predicate lt;
|
||||
Predicate gt;
|
||||
Date lowerBoundInstant = theRange.getLowerBoundAsInstant();
|
||||
Date upperBoundInstant = theRange.getUpperBoundAsInstant();
|
||||
|
||||
DateParam lowerBound = theRange.getLowerBound();
|
||||
DateParam upperBound = theRange.getUpperBound();
|
||||
Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger();
|
||||
Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger();
|
||||
Comparable genericLowerBound;
|
||||
Comparable genericUpperBound;
|
||||
/**
|
||||
* If all present search parameters are of DAY precision, and {@link DaoConfig#getUseOrdinalDatesForDayPrecisionSearches()} is true,
|
||||
* then we attempt to use the ordinal field for date comparisons instead of the date field.
|
||||
*/
|
||||
boolean isOrdinalComparison = isNullOrDayPrecision(lowerBound) && isNullOrDayPrecision(upperBound) && myDaoConfig.getModelConfig().getUseOrdinalDatesForDayPrecisionSearches();
|
||||
|
||||
Predicate lt = null;
|
||||
Predicate gt = null;
|
||||
Predicate lb = null;
|
||||
Predicate ub = null;
|
||||
String lowValueField;
|
||||
String highValueField;
|
||||
|
||||
if (isOrdinalComparison) {
|
||||
lowValueField = "myValueLowDateOrdinal";
|
||||
highValueField = "myValueHighDateOrdinal";
|
||||
genericLowerBound = lowerBoundAsOrdinal;
|
||||
genericUpperBound = upperBoundAsOrdinal;
|
||||
} else {
|
||||
lowValueField = "myValueLow";
|
||||
highValueField = "myValueHigh";
|
||||
genericLowerBound = lowerBoundInstant;
|
||||
genericUpperBound = upperBoundInstant;
|
||||
}
|
||||
|
||||
if (operation == SearchFilterParser.CompareOperation.lt) {
|
||||
if (lowerBound == null) {
|
||||
if (lowerBoundInstant == null) {
|
||||
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
|
||||
}
|
||||
lb = theBuilder.lessThan(theFrom.get("myValueLow"), lowerBound);
|
||||
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
|
||||
lb = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound);
|
||||
} else if (operation == SearchFilterParser.CompareOperation.le) {
|
||||
if (upperBound == null) {
|
||||
if (upperBoundInstant == null) {
|
||||
throw new InvalidRequestException("upperBound value not correctly specified for compare operation");
|
||||
}
|
||||
lb = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
|
||||
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
|
||||
lb = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);
|
||||
} else if (operation == SearchFilterParser.CompareOperation.gt) {
|
||||
if (upperBound == null) {
|
||||
if (upperBoundInstant == null) {
|
||||
throw new InvalidRequestException("upperBound value not correctly specified for compare operation");
|
||||
}
|
||||
lb = theBuilder.greaterThan(theFrom.get("myValueHigh"), upperBound);
|
||||
} else if (operation == SearchFilterParser.CompareOperation.ge) {
|
||||
if (lowerBound == null) {
|
||||
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
|
||||
}
|
||||
lb = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
|
||||
} else if (operation == SearchFilterParser.CompareOperation.ne) {
|
||||
if ((lowerBound == null) ||
|
||||
(upperBound == null)) {
|
||||
lb = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound);
|
||||
} else if (operation == SearchFilterParser.CompareOperation.ge) {
|
||||
if (lowerBoundInstant == null) {
|
||||
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
|
||||
}
|
||||
lb = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
|
||||
} else if (operation == SearchFilterParser.CompareOperation.ne) {
|
||||
if ((lowerBoundInstant == null) ||
|
||||
(upperBoundInstant == null)) {
|
||||
throw new InvalidRequestException("lowerBound and/or upperBound value not correctly specified for compare operation");
|
||||
}
|
||||
/*Predicate*/
|
||||
lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
|
||||
/*Predicate*/
|
||||
gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
|
||||
lt = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound);
|
||||
gt = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound);
|
||||
lb = theBuilder.or(lt,
|
||||
gt);
|
||||
} else if ((operation == SearchFilterParser.CompareOperation.eq) ||
|
||||
(operation == null)) {
|
||||
if (lowerBound != null) {
|
||||
/*Predicate*/
|
||||
gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
|
||||
/*Predicate*/
|
||||
lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound);
|
||||
if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) {
|
||||
} else if ((operation == SearchFilterParser.CompareOperation.eq) || (operation == null)) {
|
||||
if (lowerBoundInstant != null) {
|
||||
gt = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
|
||||
lt = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound);
|
||||
if (lowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER || lowerBound.getPrefix() == ParamPrefixEnum.EQUAL) {
|
||||
lb = gt;
|
||||
} else {
|
||||
lb = theBuilder.or(gt, lt);
|
||||
}
|
||||
}
|
||||
|
||||
if (upperBound != null) {
|
||||
/*Predicate*/
|
||||
gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound);
|
||||
/*Predicate*/
|
||||
lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
|
||||
if (upperBoundInstant != null) {
|
||||
gt = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound);
|
||||
lt = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);
|
||||
|
||||
|
||||
if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) {
|
||||
ub = lt;
|
||||
} else {
|
||||
|
@ -221,8 +250,11 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
|||
throw new InvalidRequestException(String.format("Unsupported operator specified, operator=%s",
|
||||
operation.name()));
|
||||
}
|
||||
|
||||
ourLog.trace("Date range is {} - {}", lowerBound, upperBound);
|
||||
if (isOrdinalComparison) {
|
||||
ourLog.trace("Ordinal date range is {} - {} ", lowerBoundAsOrdinal, upperBoundAsOrdinal);
|
||||
} else {
|
||||
ourLog.trace("Date range is {} - {}", lowerBoundInstant, upperBoundInstant);
|
||||
}
|
||||
|
||||
if (lb != null && ub != null) {
|
||||
return (theBuilder.and(lb, ub));
|
||||
|
|
|
@ -4353,15 +4353,14 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
assertEquals(1, results.getResources(0, 10).size());
|
||||
// We expect a new one because we don't cache the search URL for very long search URLs
|
||||
assertEquals(2, mySearchEntityDao.count());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateSearchParametersShouldBeTimezoneIndependent() {
|
||||
|
||||
createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30");
|
||||
createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00");
|
||||
createObservationWithEffective("NO1", "2011-01-03T00:00:00+01:00");
|
||||
|
||||
createObservationWithEffective("YES00", "2011-01-02T23:00:00-11:30");
|
||||
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");
|
||||
|
@ -4394,6 +4393,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
List<String> values = toUnqualifiedVersionlessIdValues(results);
|
||||
Collections.sort(values);
|
||||
assertThat(values.toString(), values, contains(
|
||||
"Observation/YES00",
|
||||
"Observation/YES01",
|
||||
"Observation/YES02",
|
||||
"Observation/YES03",
|
||||
|
@ -4420,6 +4420,68 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateSearchParametersShouldBeHourIndependent() {
|
||||
|
||||
createObservationWithEffective("YES01", "2011-01-02T00:00:00");
|
||||
createObservationWithEffective("YES02", "2011-01-02T01:00:00");
|
||||
createObservationWithEffective("YES03", "2011-01-02T02:00:00");
|
||||
createObservationWithEffective("YES04", "2011-01-02T03:00:00");
|
||||
createObservationWithEffective("YES05", "2011-01-02T04:00:00");
|
||||
createObservationWithEffective("YES06", "2011-01-02T05:00:00");
|
||||
createObservationWithEffective("YES07", "2011-01-02T06:00:00");
|
||||
createObservationWithEffective("YES08", "2011-01-02T07:00:00");
|
||||
createObservationWithEffective("YES09", "2011-01-02T08:00:00");
|
||||
createObservationWithEffective("YES10", "2011-01-02T09:00:00");
|
||||
createObservationWithEffective("YES11", "2011-01-02T10:00:00");
|
||||
createObservationWithEffective("YES12", "2011-01-02T11:00:00");
|
||||
createObservationWithEffective("YES13", "2011-01-02T12:00:00");
|
||||
createObservationWithEffective("YES14", "2011-01-02T13:00:00");
|
||||
createObservationWithEffective("YES15", "2011-01-02T14:00:00");
|
||||
createObservationWithEffective("YES16", "2011-01-02T15:00:00");
|
||||
createObservationWithEffective("YES17", "2011-01-02T16:00:00");
|
||||
createObservationWithEffective("YES18", "2011-01-02T17:00:00");
|
||||
createObservationWithEffective("YES19", "2011-01-02T18:00:00");
|
||||
createObservationWithEffective("YES20", "2011-01-02T19:00:00");
|
||||
createObservationWithEffective("YES21", "2011-01-02T20:00:00");
|
||||
createObservationWithEffective("YES22", "2011-01-02T21:00:00");
|
||||
createObservationWithEffective("YES23", "2011-01-02T22:00:00");
|
||||
createObservationWithEffective("YES24", "2011-01-02T23:00: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",
|
||||
"Observation/YES24"
|
||||
));
|
||||
}
|
||||
|
||||
private void createObservationWithEffective(String theId, String theEffective) {
|
||||
Observation obs = new Observation();
|
||||
obs.setId(theId);
|
||||
|
|
|
@ -3280,8 +3280,8 @@ public class FhirResourceDaoR4SearchNoHashesTest 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("NO1", "2011-01-01T23:00:00-11:30");
|
||||
createObservationWithEffective("NO2", "2011-01-03T23:00:00+01:30");
|
||||
|
||||
createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30");
|
||||
createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00");
|
||||
|
|
|
@ -887,10 +887,10 @@ public class InMemorySubscriptionMatcherR4Test {
|
|||
public void testDateSearchParametersShouldBeTimezoneIndependent() {
|
||||
|
||||
List<Observation> nlist = new ArrayList<>();
|
||||
nlist.add(createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30"));
|
||||
nlist.add(createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00"));
|
||||
|
||||
List<Observation> ylist = new ArrayList<>();
|
||||
nlist.add(createObservationWithEffective("YES00", "2011-01-02T23:00:00-11:30"));
|
||||
ylist.add(createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30"));
|
||||
ylist.add(createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00"));
|
||||
ylist.add(createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00"));
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
package ca.uhn.fhir.jpa.migrate.taskdef;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server - Migration
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.util.StopWatch;
|
||||
import ca.uhn.fhir.util.VersionEnum;
|
||||
import com.google.common.collect.ForwardingMap;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.ColumnMapRowMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowCallbackHandler;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class BaseColumnCalculatorTask extends BaseTableColumnTask {
|
||||
|
||||
protected static final Logger ourLog = LoggerFactory.getLogger(BaseColumnCalculatorTask.class);
|
||||
private int myBatchSize = 10000;
|
||||
private ThreadPoolExecutor myExecutor;
|
||||
|
||||
public void setBatchSize(int theBatchSize) {
|
||||
myBatchSize = theBatchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public BaseColumnCalculatorTask(VersionEnum theRelease, String theVersion) {
|
||||
super(theRelease.toString(), theVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows concrete implementations to decide if they should be skipped.
|
||||
* @return a boolean indicating whether or not to skip execution of the task.
|
||||
*/
|
||||
protected abstract boolean shouldSkipTask();
|
||||
|
||||
@Override
|
||||
public synchronized void doExecute() throws SQLException {
|
||||
if (isDryRun() || shouldSkipTask()) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeExecutor();
|
||||
|
||||
try {
|
||||
|
||||
while(true) {
|
||||
MyRowCallbackHandler rch = new MyRowCallbackHandler();
|
||||
getTxTemplate().execute(t -> {
|
||||
JdbcTemplate jdbcTemplate = newJdbcTemplate();
|
||||
jdbcTemplate.setMaxRows(100000);
|
||||
|
||||
String sql = "SELECT * FROM " + getTableName() + " WHERE " + getWhereClause();
|
||||
logInfo(ourLog, "Finding up to {} rows in {} that requires calculations, using query: {}", myBatchSize, getTableName(), sql);
|
||||
|
||||
jdbcTemplate.query(sql, rch);
|
||||
rch.done();
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
rch.submitNext();
|
||||
List<Future<?>> futures = rch.getFutures();
|
||||
if (futures.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
logInfo(ourLog, "Waiting for {} tasks to complete", futures.size());
|
||||
for (Future<?> next : futures) {
|
||||
try {
|
||||
next.get();
|
||||
} catch (Exception e) {
|
||||
throw new SQLException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} finally {
|
||||
destroyExecutor();
|
||||
}
|
||||
}
|
||||
|
||||
private void destroyExecutor() {
|
||||
myExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
private void initializeExecutor() {
|
||||
int maximumPoolSize = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(maximumPoolSize);
|
||||
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
|
||||
.namingPattern("worker-" + "-%d")
|
||||
.daemon(false)
|
||||
.priority(Thread.NORM_PRIORITY)
|
||||
.build();
|
||||
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
|
||||
@Override
|
||||
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
|
||||
logInfo(ourLog, "Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
|
||||
StopWatch sw = new StopWatch();
|
||||
try {
|
||||
executorQueue.put(theRunnable);
|
||||
} catch (InterruptedException theE) {
|
||||
throw new RejectedExecutionException("Task " + theRunnable.toString() +
|
||||
" rejected from " + theE.toString());
|
||||
}
|
||||
logInfo(ourLog, "Slot become available after {}ms", sw.getMillis());
|
||||
}
|
||||
};
|
||||
myExecutor = new ThreadPoolExecutor(
|
||||
1,
|
||||
maximumPoolSize,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
executorQueue,
|
||||
threadFactory,
|
||||
rejectedExecutionHandler);
|
||||
}
|
||||
|
||||
private Future<?> updateRows(List<Map<String, Object>> theRows) {
|
||||
Runnable task = () -> {
|
||||
StopWatch sw = new StopWatch();
|
||||
getTxTemplate().execute(t -> {
|
||||
|
||||
// Loop through rows
|
||||
assert theRows != null;
|
||||
for (Map<String, Object> nextRow : theRows) {
|
||||
|
||||
Map<String, Object> newValues = new HashMap<>();
|
||||
MandatoryKeyMap<String, Object> nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow);
|
||||
|
||||
// Apply calculators
|
||||
for (Map.Entry<String, Function<MandatoryKeyMap<String, Object>, Object>> nextCalculatorEntry : myCalculators.entrySet()) {
|
||||
String nextColumn = nextCalculatorEntry.getKey();
|
||||
Function<MandatoryKeyMap<String, Object>, Object> nextCalculator = nextCalculatorEntry.getValue();
|
||||
Object value = nextCalculator.apply(nextRowMandatoryKeyMap);
|
||||
newValues.put(nextColumn, value);
|
||||
}
|
||||
|
||||
// Generate update SQL
|
||||
StringBuilder sqlBuilder = new StringBuilder();
|
||||
List<Object> arguments = new ArrayList<>();
|
||||
sqlBuilder.append("UPDATE ");
|
||||
sqlBuilder.append(getTableName());
|
||||
sqlBuilder.append(" SET ");
|
||||
for (Map.Entry<String, Object> nextNewValueEntry : newValues.entrySet()) {
|
||||
if (arguments.size() > 0) {
|
||||
sqlBuilder.append(", ");
|
||||
}
|
||||
sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?");
|
||||
arguments.add(nextNewValueEntry.getValue());
|
||||
}
|
||||
sqlBuilder.append(" WHERE SP_ID = ?");
|
||||
arguments.add((Number) nextRow.get("SP_ID"));
|
||||
|
||||
// Apply update SQL
|
||||
newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray());
|
||||
}
|
||||
return theRows.size();
|
||||
});
|
||||
logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString());
|
||||
};
|
||||
return myExecutor.submit(task);
|
||||
}
|
||||
|
||||
private class MyRowCallbackHandler implements RowCallbackHandler {
|
||||
|
||||
private List<Map<String, Object>> myRows = new ArrayList<>();
|
||||
private List<Future<?>> myFutures = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void processRow(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> row = new ColumnMapRowMapper().mapRow(rs, 0);
|
||||
myRows.add(row);
|
||||
|
||||
if (myRows.size() >= myBatchSize) {
|
||||
submitNext();
|
||||
}
|
||||
}
|
||||
|
||||
private void submitNext() {
|
||||
if (myRows.size() > 0) {
|
||||
myFutures.add(updateRows(myRows));
|
||||
myRows = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Future<?>> getFutures() {
|
||||
return myFutures;
|
||||
}
|
||||
|
||||
public void done() {
|
||||
if (myRows.size() > 0) {
|
||||
submitNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class MandatoryKeyMap<K, V> extends ForwardingMap<K, V> {
|
||||
|
||||
private final Map<K, V> myWrap;
|
||||
|
||||
public MandatoryKeyMap(Map<K, V> theWrap) {
|
||||
myWrap = theWrap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object theKey) {
|
||||
if (!containsKey(theKey)) {
|
||||
throw new IllegalArgumentException("No key: " + theKey);
|
||||
}
|
||||
return super.get(theKey);
|
||||
}
|
||||
|
||||
public String getString(String theKey) {
|
||||
return (String) get(theKey);
|
||||
}
|
||||
|
||||
public Date getDate(String theKey) {
|
||||
return (Date) get(theKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<K, V> delegate() {
|
||||
return myWrap;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return getString("RES_TYPE");
|
||||
}
|
||||
|
||||
public String getParamName() {
|
||||
return getString("SP_NAME");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,11 +25,17 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
|
|||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class BaseTableColumnTask extends BaseTableTask {
|
||||
|
||||
private String myColumnName;
|
||||
protected Map<String, Function<BaseColumnCalculatorTask.MandatoryKeyMap<String, Object>, Object>> myCalculators = new HashMap<>();
|
||||
protected String myColumnName;
|
||||
//If a concrete class decides to, they can define a custom WHERE clause for the task.
|
||||
protected String myWhereClause;
|
||||
|
||||
public BaseTableColumnTask(String theProductVersion, String theSchemaVersion) {
|
||||
super(theProductVersion, theSchemaVersion);
|
||||
|
@ -40,11 +46,21 @@ public abstract class BaseTableColumnTask extends BaseTableTask {
|
|||
return this;
|
||||
}
|
||||
|
||||
|
||||
public String getColumnName() {
|
||||
return myColumnName;
|
||||
}
|
||||
|
||||
protected void setWhereClause(String theWhereClause) {
|
||||
this.myWhereClause = theWhereClause;
|
||||
}
|
||||
protected String getWhereClause() {
|
||||
if (myWhereClause == null) {
|
||||
return getColumnName() + " IS NULL";
|
||||
} else {
|
||||
return myWhereClause;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
super.validate();
|
||||
|
@ -63,4 +79,10 @@ public abstract class BaseTableColumnTask extends BaseTableTask {
|
|||
super.generateHashCode(theBuilder);
|
||||
theBuilder.append(myColumnName);
|
||||
}
|
||||
|
||||
public BaseTableColumnTask addCalculator(String theColumnName, Function<BaseColumnCalculatorTask.MandatoryKeyMap<String, Object>, Object> theConsumer) {
|
||||
Validate.isTrue(myCalculators.containsKey(theColumnName) == false);
|
||||
myCalculators.put(theColumnName, theConsumer);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
|
|||
public abstract class BaseTableTask extends BaseTask {
|
||||
private String myTableName;
|
||||
|
||||
|
||||
|
||||
public BaseTableTask(String theProductVersion, String theSchemaVersion) {
|
||||
super(theProductVersion, theSchemaVersion);
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ public abstract class BaseTask {
|
|||
doExecute();
|
||||
}
|
||||
|
||||
public abstract void doExecute() throws SQLException;
|
||||
protected abstract void doExecute() throws SQLException;
|
||||
|
||||
public void setFailureAllowed(boolean theFailureAllowed) {
|
||||
myFailureAllowed = theFailureAllowed;
|
||||
|
|
|
@ -34,253 +34,34 @@ import org.springframework.jdbc.core.RowCallbackHandler;
|
|||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class CalculateHashesTask extends BaseTableColumnTask {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(CalculateHashesTask.class);
|
||||
private int myBatchSize = 10000;
|
||||
private Map<String, Function<MandatoryKeyMap<String, Object>, Long>> myCalculators = new HashMap<>();
|
||||
private ThreadPoolExecutor myExecutor;
|
||||
public class CalculateHashesTask extends BaseColumnCalculatorTask {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public CalculateHashesTask(VersionEnum theRelease, String theVersion) {
|
||||
super(theRelease.toString(), theVersion);
|
||||
super(theRelease, theVersion);
|
||||
setDescription("Calculate resource search parameter index hashes");
|
||||
}
|
||||
|
||||
public void setBatchSize(int theBatchSize) {
|
||||
myBatchSize = theBatchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CalculateHashesTask setColumnName(String theColumnName) {
|
||||
super.setColumnName(theColumnName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void doExecute() throws SQLException {
|
||||
if (isDryRun()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> tableNames = JdbcUtils.getTableNames(getConnectionProperties());
|
||||
// This table was added shortly after hash indexes were added, so it is a reasonable indicator for whether this
|
||||
// migration has already been run
|
||||
if (tableNames.contains("HFJ_RES_REINDEX_JOB")) {
|
||||
logInfo(ourLog, "The table HFJ_RES_REINDEX_JOB already exists. Skipping calculate hashes task.");
|
||||
return;
|
||||
}
|
||||
|
||||
initializeExecutor();
|
||||
protected boolean shouldSkipTask() {
|
||||
try {
|
||||
|
||||
while (true) {
|
||||
MyRowCallbackHandler rch = new MyRowCallbackHandler();
|
||||
getTxTemplate().execute(t -> {
|
||||
JdbcTemplate jdbcTemplate = newJdbcTemplate();
|
||||
jdbcTemplate.setMaxRows(100000);
|
||||
String sql = "SELECT * FROM " + getTableName() + " WHERE " + getColumnName() + " IS NULL";
|
||||
logInfo(ourLog, "Finding up to {} rows in {} that requires hashes", myBatchSize, getTableName());
|
||||
|
||||
jdbcTemplate.query(sql, rch);
|
||||
rch.done();
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
rch.submitNext();
|
||||
List<Future<?>> futures = rch.getFutures();
|
||||
if (futures.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
logInfo(ourLog, "Waiting for {} tasks to complete", futures.size());
|
||||
for (Future<?> next : futures) {
|
||||
try {
|
||||
next.get();
|
||||
} catch (Exception e) {
|
||||
throw new SQLException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> tableNames = JdbcUtils.getTableNames(getConnectionProperties());
|
||||
boolean shouldSkip = tableNames.contains("HFJ_RES_REINDEX_JOB");
|
||||
// This table was added shortly after hash indexes were added, so it is a reasonable indicator for whether this
|
||||
// migration has already been run
|
||||
if (shouldSkip) {
|
||||
logInfo(ourLog, "The table HFJ_RES_REINDEX_JOB already exists. Skipping calculate hashes task.");
|
||||
}
|
||||
|
||||
} finally {
|
||||
destroyExecutor();
|
||||
}
|
||||
}
|
||||
|
||||
private void destroyExecutor() {
|
||||
myExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
private void initializeExecutor() {
|
||||
int maximumPoolSize = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(maximumPoolSize);
|
||||
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
|
||||
.namingPattern("worker-" + "-%d")
|
||||
.daemon(false)
|
||||
.priority(Thread.NORM_PRIORITY)
|
||||
.build();
|
||||
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
|
||||
@Override
|
||||
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
|
||||
logInfo(ourLog, "Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
|
||||
StopWatch sw = new StopWatch();
|
||||
try {
|
||||
executorQueue.put(theRunnable);
|
||||
} catch (InterruptedException theE) {
|
||||
throw new RejectedExecutionException("Task " + theRunnable.toString() +
|
||||
" rejected from " + theE.toString());
|
||||
}
|
||||
logInfo(ourLog, "Slot become available after {}ms", sw.getMillis());
|
||||
}
|
||||
};
|
||||
myExecutor = new ThreadPoolExecutor(
|
||||
1,
|
||||
maximumPoolSize,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
executorQueue,
|
||||
threadFactory,
|
||||
rejectedExecutionHandler);
|
||||
}
|
||||
|
||||
private Future<?> updateRows(List<Map<String, Object>> theRows) {
|
||||
Runnable task = () -> {
|
||||
StopWatch sw = new StopWatch();
|
||||
getTxTemplate().execute(t -> {
|
||||
|
||||
// Loop through rows
|
||||
assert theRows != null;
|
||||
for (Map<String, Object> nextRow : theRows) {
|
||||
|
||||
Map<String, Long> newValues = new HashMap<>();
|
||||
MandatoryKeyMap<String, Object> nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow);
|
||||
|
||||
// Apply calculators
|
||||
for (Map.Entry<String, Function<MandatoryKeyMap<String, Object>, Long>> nextCalculatorEntry : myCalculators.entrySet()) {
|
||||
String nextColumn = nextCalculatorEntry.getKey();
|
||||
Function<MandatoryKeyMap<String, Object>, Long> nextCalculator = nextCalculatorEntry.getValue();
|
||||
Long value = nextCalculator.apply(nextRowMandatoryKeyMap);
|
||||
newValues.put(nextColumn, value);
|
||||
}
|
||||
|
||||
// Generate update SQL
|
||||
StringBuilder sqlBuilder = new StringBuilder();
|
||||
List<Number> arguments = new ArrayList<>();
|
||||
sqlBuilder.append("UPDATE ");
|
||||
sqlBuilder.append(getTableName());
|
||||
sqlBuilder.append(" SET ");
|
||||
for (Map.Entry<String, Long> nextNewValueEntry : newValues.entrySet()) {
|
||||
if (arguments.size() > 0) {
|
||||
sqlBuilder.append(", ");
|
||||
}
|
||||
sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?");
|
||||
arguments.add(nextNewValueEntry.getValue());
|
||||
}
|
||||
sqlBuilder.append(" WHERE SP_ID = ?");
|
||||
arguments.add((Number) nextRow.get("SP_ID"));
|
||||
|
||||
// Apply update SQL
|
||||
newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray());
|
||||
|
||||
}
|
||||
|
||||
return theRows.size();
|
||||
});
|
||||
logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString());
|
||||
};
|
||||
return myExecutor.submit(task);
|
||||
}
|
||||
|
||||
public CalculateHashesTask addCalculator(String theColumnName, Function<MandatoryKeyMap<String, Object>, Long> theConsumer) {
|
||||
Validate.isTrue(myCalculators.containsKey(theColumnName) == false);
|
||||
myCalculators.put(theColumnName, theConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
private class MyRowCallbackHandler implements RowCallbackHandler {
|
||||
|
||||
private List<Map<String, Object>> myRows = new ArrayList<>();
|
||||
private List<Future<?>> myFutures = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void processRow(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> row = new ColumnMapRowMapper().mapRow(rs, 0);
|
||||
myRows.add(row);
|
||||
|
||||
if (myRows.size() >= myBatchSize) {
|
||||
submitNext();
|
||||
}
|
||||
}
|
||||
|
||||
private void submitNext() {
|
||||
if (myRows.size() > 0) {
|
||||
myFutures.add(updateRows(myRows));
|
||||
myRows = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Future<?>> getFutures() {
|
||||
return myFutures;
|
||||
}
|
||||
|
||||
public void done() {
|
||||
if (myRows.size() > 0) {
|
||||
submitNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class MandatoryKeyMap<K, V> extends ForwardingMap<K, V> {
|
||||
|
||||
private final Map<K, V> myWrap;
|
||||
|
||||
public MandatoryKeyMap(Map<K, V> theWrap) {
|
||||
myWrap = theWrap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object theKey) {
|
||||
if (!containsKey(theKey)) {
|
||||
throw new IllegalArgumentException("No key: " + theKey);
|
||||
}
|
||||
return super.get(theKey);
|
||||
}
|
||||
|
||||
public String getString(String theKey) {
|
||||
return (String) get(theKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<K, V> delegate() {
|
||||
return myWrap;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return getString("RES_TYPE");
|
||||
}
|
||||
|
||||
public String getParamName() {
|
||||
return getString("SP_NAME");
|
||||
return shouldSkip;
|
||||
} catch (SQLException e) {
|
||||
logInfo(ourLog, "Error retrieving table names, skipping task");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package ca.uhn.fhir.jpa.migrate.taskdef;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server - Migration
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 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.util.VersionEnum;
|
||||
|
||||
public class CalculateOrdinalDatesTask extends BaseColumnCalculatorTask {
|
||||
|
||||
public CalculateOrdinalDatesTask(VersionEnum theRelease, String theVersion) {
|
||||
super(theRelease, theVersion);
|
||||
setDescription("Calculate SP_LOW_VALUE_DATE_ORDINAL and SP_HIGH_VALUE_DATE_ORDINAL based on existing SP_VALUE_LOW and SP_VALUE_HIGH date values in Date Search Params");
|
||||
setWhereClause("(SP_VALUE_LOW_DATE_ORDINAL IS NULL AND SP_VALUE_LOW IS NOT NULL) OR (SP_VALUE_HIGH_DATE_ORDINAL IS NULL AND SP_VALUE_HIGH IS NOT NULL)");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldSkipTask() {
|
||||
return false; // TODO Is there a case where we should just not do this?
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.AddColumnTask;
|
|||
import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask;
|
||||
import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableColumnTypeTask;
|
||||
import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask;
|
||||
import ca.uhn.fhir.jpa.migrate.taskdef.CalculateOrdinalDatesTask;
|
||||
import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks;
|
||||
import ca.uhn.fhir.jpa.migrate.tasks.api.Builder;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
|
@ -69,6 +70,19 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
|
|||
version.onTable("HFJ_RES_VER").dropColumn("20200218.2", "FORCED_ID_PID");
|
||||
version.onTable("HFJ_RES_VER").addForeignKey("20200218.3", "FK_RESOURCE_HISTORY_RESOURCE").toColumn("RES_ID").references("HFJ_RESOURCE", "RES_ID");
|
||||
version.onTable("HFJ_RES_VER").modifyColumn("20200220.1", "RES_ID").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG);
|
||||
//
|
||||
|
||||
// Add support for integer comparisons during day-precision date search.
|
||||
Builder.BuilderWithTableName spidxDate = version.onTable("HFJ_SPIDX_DATE");
|
||||
spidxDate.addColumn("20200225.1", "SP_VALUE_LOW_DATE_ORDINAL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT);
|
||||
spidxDate.addColumn("20200225.2", "SP_VALUE_HIGH_DATE_ORDINAL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT);
|
||||
|
||||
spidxDate.addTask(new CalculateOrdinalDatesTask(VersionEnum.V4_3_0, "20200225.3")
|
||||
.addCalculator("SP_VALUE_LOW_DATE_ORDINAL", t -> ResourceIndexedSearchParamDate.calculateOrdinalValue(t.getDate("SP_VALUE_LOW")))
|
||||
.addCalculator("SP_VALUE_HIGH_DATE_ORDINAL", t -> ResourceIndexedSearchParamDate.calculateOrdinalValue(t.getDate("SP_VALUE_HIGH")))
|
||||
.setColumnName("SP_VALUE_LOW_DATE_ORDINAL") //It doesn't matter which of the two we choose as they will both be null.
|
||||
);
|
||||
//
|
||||
|
||||
// Drop unused column
|
||||
version.onTable("HFJ_RESOURCE").dropIndex("20200419.1", "IDX_RES_PROFILE");
|
||||
|
@ -527,8 +541,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
|
|||
.withColumns("HASH_IDENTITY", "SP_LATITUDE", "SP_LONGITUDE");
|
||||
spidxCoords
|
||||
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.5")
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -550,8 +564,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
|
|||
.dropIndex("20180903.9", "IDX_SP_DATE");
|
||||
spidxDate
|
||||
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.10")
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -571,8 +585,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
|
|||
.withColumns("HASH_IDENTITY", "SP_VALUE");
|
||||
spidxNumber
|
||||
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.14")
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -608,10 +622,10 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
|
|||
.withColumns("HASH_IDENTITY_SYS_UNITS", "SP_VALUE");
|
||||
spidxQuantity
|
||||
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.22")
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
|
||||
.addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS")))
|
||||
.addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS")))
|
||||
.setColumnName("HASH_IDENTITY")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
|
|||
|
||||
public abstract IQueryParameterType toQueryParameterType();
|
||||
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
throw new UnsupportedOperationException("No parameter matcher for " + theParam);
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,10 @@ public class ModelConfig {
|
|||
private Set<Subscription.SubscriptionChannelType> mySupportedSubscriptionTypes = new HashSet<>();
|
||||
private String myEmailFromAddress = "noreply@unknown.com";
|
||||
private String myWebsocketContextPath = DEFAULT_WEBSOCKET_CONTEXT_PATH;
|
||||
/**
|
||||
* Update setter javadoc if default changes.
|
||||
*/
|
||||
private boolean myUseOrdinalDatesForDayPrecisionSearches = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -258,7 +262,6 @@ public class ModelConfig {
|
|||
myTreatReferencesAsLogical = new HashSet<>();
|
||||
}
|
||||
myTreatReferencesAsLogical.add(theTreatReferencesAsLogical);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -368,6 +371,43 @@ public class ModelConfig {
|
|||
myWebsocketContextPath = theWebsocketContextPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in
|
||||
* {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using
|
||||
* precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}.
|
||||
*
|
||||
* For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an
|
||||
* ordinal {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()}
|
||||
* and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()}
|
||||
* </p>
|
||||
* Default is {@literal true} beginning in HAPI FHIR 4.3.
|
||||
* </p>
|
||||
*
|
||||
* @since 4.3
|
||||
*/
|
||||
public void setUseOrdinalDatesForDayPrecisionSearches(boolean theUseOrdinalDates) {
|
||||
myUseOrdinalDatesForDayPrecisionSearches = theUseOrdinalDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in
|
||||
* {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using
|
||||
* precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}.
|
||||
*
|
||||
* For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an
|
||||
* integer representing the ordinal date {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()}
|
||||
* and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()}
|
||||
* </p>
|
||||
* Default is {@literal true} beginning in HAPI FHIR 4.3.
|
||||
* </p>
|
||||
*
|
||||
* @since 4.3
|
||||
*/
|
||||
public boolean getUseOrdinalDatesForDayPrecisionSearches() {
|
||||
return myUseOrdinalDatesForDayPrecisionSearches;
|
||||
}
|
||||
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
|
||||
Validate.notBlank(theUrl, "Base URL must not be null or empty");
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
|||
import ca.uhn.fhir.model.primitive.InstantDt;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.util.DateUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
@ -55,6 +57,16 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
|
|||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Field
|
||||
public Date myValueLow;
|
||||
|
||||
/**
|
||||
* Field which stores an integer representation of YYYYMDD as calculated by Calendar
|
||||
* e.g. 2019-01-20 -> 20190120
|
||||
*/
|
||||
@Column(name="SP_VALUE_LOW_DATE_ORDINAL")
|
||||
public Integer myValueLowDateOrdinal;
|
||||
@Column(name="SP_VALUE_HIGH_DATE_ORDINAL")
|
||||
public Integer myValueHighDateOrdinal;
|
||||
|
||||
@Transient
|
||||
private transient String myOriginalValue;
|
||||
@Id
|
||||
|
@ -78,21 +90,58 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
|
|||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, Date theHigh, String theOriginalValue) {
|
||||
public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, String theLowString, Date theHigh, String theHighString, String theOriginalValue) {
|
||||
setPartitionSettings(thePartitionSettings);
|
||||
setResourceType(theResourceType);
|
||||
setParamName(theParamName);
|
||||
setValueLow(theLow);
|
||||
setValueHigh(theHigh);
|
||||
if (theHigh != null && theHighString == null) {
|
||||
theHighString = DateUtils.convertDateToIso8601String(theHigh);
|
||||
}
|
||||
if (theLow != null && theLowString == null) {
|
||||
theLowString = DateUtils.convertDateToIso8601String(theLow);
|
||||
}
|
||||
computeValueHighDateOrdinal(theHighString);
|
||||
computeValueLowDateOrdinal(theLowString);
|
||||
myOriginalValue = theOriginalValue;
|
||||
}
|
||||
|
||||
private void computeValueHighDateOrdinal(String theHigh) {
|
||||
if (!StringUtils.isBlank(theHigh)) {
|
||||
this.myValueHighDateOrdinal = generateOrdinalDateInteger(theHigh);
|
||||
}
|
||||
}
|
||||
private int generateOrdinalDateInteger(String theDateString){
|
||||
if (theDateString.contains("T")) {
|
||||
theDateString = theDateString.substring(0, theDateString.indexOf("T"));
|
||||
}
|
||||
theDateString = theDateString.replace("-", "");
|
||||
return Integer.valueOf(theDateString);
|
||||
}
|
||||
|
||||
private void computeValueLowDateOrdinal(String theLow) {
|
||||
if (StringUtils.isNotBlank(theLow)) {
|
||||
this.myValueLowDateOrdinal = generateOrdinalDateInteger(theLow);
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getValueLowDateOrdinal() {
|
||||
return myValueLowDateOrdinal;
|
||||
}
|
||||
|
||||
public Integer getValueHighDateOrdinal() {
|
||||
return myValueHighDateOrdinal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
|
||||
super.copyMutableValuesFrom(theSource);
|
||||
ResourceIndexedSearchParamDate source = (ResourceIndexedSearchParamDate) theSource;
|
||||
myValueHigh = source.myValueHigh;
|
||||
myValueLow = source.myValueLow;
|
||||
myValueHighDateOrdinal = source.myValueHighDateOrdinal;
|
||||
myValueLowDateOrdinal = source.myValueLowDateOrdinal;
|
||||
myHashIdentity = source.myHashIdentity;
|
||||
}
|
||||
|
||||
|
@ -203,20 +252,34 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
|
|||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (!(theParam instanceof DateParam)) {
|
||||
return false;
|
||||
}
|
||||
DateParam date = (DateParam) theParam;
|
||||
DateRangeParam range = new DateRangeParam(date);
|
||||
DateParam dateParam = (DateParam) theParam;
|
||||
DateRangeParam range = new DateRangeParam(dateParam);
|
||||
|
||||
|
||||
|
||||
|
||||
boolean result;
|
||||
if (theUseOrdinalDatesForDayComparison) {
|
||||
result = matchesOrdinalDateBounds(range);
|
||||
result = matchesDateBounds(range);
|
||||
} else {
|
||||
result = matchesDateBounds(range);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean matchesDateBounds(DateRangeParam range) {
|
||||
Date lowerBound = range.getLowerBoundAsInstant();
|
||||
Date upperBound = range.getUpperBoundAsInstant();
|
||||
|
||||
if (lowerBound == null && upperBound == null) {
|
||||
// should never happen
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
if (lowerBound != null) {
|
||||
result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
|
||||
|
@ -229,4 +292,31 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
|
|||
return result;
|
||||
}
|
||||
|
||||
private boolean matchesOrdinalDateBounds(DateRangeParam range) {
|
||||
boolean result = true;
|
||||
Integer lowerBoundAsDateInteger = range.getLowerBoundAsDateInteger();
|
||||
Integer upperBoundAsDateInteger = range.getUpperBoundAsDateInteger();
|
||||
if (upperBoundAsDateInteger == null && lowerBoundAsDateInteger == null) {
|
||||
return false;
|
||||
}
|
||||
if (lowerBoundAsDateInteger != null) {
|
||||
//TODO as we run into equality issues
|
||||
result &= (myValueLowDateOrdinal.equals(lowerBoundAsDateInteger) || myValueLowDateOrdinal > lowerBoundAsDateInteger);
|
||||
result &= (myValueHighDateOrdinal.equals(lowerBoundAsDateInteger) || myValueHighDateOrdinal > lowerBoundAsDateInteger);
|
||||
}
|
||||
if (upperBoundAsDateInteger != null) {
|
||||
result &= (myValueHighDateOrdinal.equals(upperBoundAsDateInteger) || myValueHighDateOrdinal < upperBoundAsDateInteger);
|
||||
result &= (myValueLowDateOrdinal.equals(upperBoundAsDateInteger) || myValueLowDateOrdinal < upperBoundAsDateInteger);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static Long calculateOrdinalValue(Date theDate) {
|
||||
if (theDate == null) {
|
||||
return null;
|
||||
}
|
||||
return (long) DateUtils.convertDatetoDayInteger(theDate);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (!(theParam instanceof NumberParam)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -253,7 +253,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (!(theParam instanceof QuantityParam)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -270,7 +270,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (!(theParam instanceof StringParam)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (!(theParam instanceof TokenParam)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (!(theParam instanceof UriParam)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
|
@ -36,8 +37,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsTrueForMatchingNullDates() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, null, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", null, null, null, null, "SomeValue");
|
||||
|
||||
assertTrue(param.equals(param2));
|
||||
assertTrue(param2.equals(param));
|
||||
|
@ -46,8 +47,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsTrueForMatchingDates() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1B, date2B, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1B, null, date2B, null, "SomeValue");
|
||||
|
||||
assertTrue(param.equals(param2));
|
||||
assertTrue(param2.equals(param));
|
||||
|
@ -56,8 +57,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsTrueForMatchingTimeStampsThatMatch() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1B, timestamp2B, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1B, null, timestamp2B, null, "SomeValue");
|
||||
|
||||
assertTrue(param.equals(param2));
|
||||
assertTrue(param2.equals(param));
|
||||
|
@ -68,8 +69,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
// other will be equivalent but will be a java.sql.Timestamp. Equals should work in both directions.
|
||||
@Test
|
||||
public void equalsIsTrueForMixedTimestampsAndDates() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue");
|
||||
|
||||
assertTrue(param.equals(param2));
|
||||
assertTrue(param2.equals(param));
|
||||
|
@ -78,8 +79,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsFalseForNonMatchingDates() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date2A, date1A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date2A, null, date1A, null, "SomeValue");
|
||||
|
||||
assertFalse(param.equals(param2));
|
||||
assertFalse(param2.equals(param));
|
||||
|
@ -88,8 +89,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsFalseForNonMatchingDatesNullCase() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", null, null, null, null, "SomeValue");
|
||||
|
||||
assertFalse(param.equals(param2));
|
||||
assertFalse(param2.equals(param));
|
||||
|
@ -98,8 +99,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsFalseForNonMatchingTimeStamps() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp2A, timestamp1A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp2A, null, timestamp1A, null, "SomeValue");
|
||||
|
||||
assertFalse(param.equals(param2));
|
||||
assertFalse(param2.equals(param));
|
||||
|
@ -108,8 +109,8 @@ public class ResourceIndexedSearchParamDateTest {
|
|||
|
||||
@Test
|
||||
public void equalsIsFalseForMixedTimestampsAndDatesThatDoNotMatch() {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp2A, timestamp1A, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
|
||||
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp2A, null, timestamp1A, null, "SomeValue");
|
||||
|
||||
assertFalse(param.equals(param2));
|
||||
assertFalse(param2.equals(param));
|
||||
|
|
|
@ -584,9 +584,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
Date start = extractValueAsDate(myPeriodStartValueChild, theValue);
|
||||
String startAsString = extractValueAsString(myPeriodStartValueChild, theValue);
|
||||
Date end = extractValueAsDate(myPeriodEndValueChild, theValue);
|
||||
String endAsString = extractValueAsString(myPeriodEndValueChild, theValue);
|
||||
|
||||
if (start != null || end != null) {
|
||||
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), start, end, startAsString);
|
||||
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings ,theResourceType, theSearchParam.getName(), start, startAsString, end, endAsString, startAsString);
|
||||
theParams.add(nextEntity);
|
||||
}
|
||||
}
|
||||
|
@ -595,13 +596,16 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
List<IPrimitiveType<Date>> values = extractValuesAsFhirDates(myTimingEventValueChild, theValue);
|
||||
|
||||
TreeSet<Date> dates = new TreeSet<>();
|
||||
TreeSet<String> dateStrings = new TreeSet<>();
|
||||
String firstValue = null;
|
||||
String finalValue = null;
|
||||
for (IPrimitiveType<Date> nextEvent : values) {
|
||||
if (nextEvent.getValue() != null) {
|
||||
dates.add(nextEvent.getValue());
|
||||
if (firstValue == null) {
|
||||
firstValue = nextEvent.getValueAsString();
|
||||
}
|
||||
finalValue = nextEvent.getValueAsString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -613,14 +617,20 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
if ("Period".equals(boundsType)) {
|
||||
Date start = extractValueAsDate(myPeriodStartValueChild, bounds.get());
|
||||
Date end = extractValueAsDate(myPeriodEndValueChild, bounds.get());
|
||||
String endString = extractValueAsString(myPeriodEndValueChild, bounds.get());
|
||||
dates.add(start);
|
||||
dates.add(end);
|
||||
//TODO Check if this logic is valid. Does the start of the first period indicate a lower bound??
|
||||
if (firstValue == null) {
|
||||
firstValue = extractValueAsString(myPeriodStartValueChild, bounds.get());
|
||||
}
|
||||
finalValue = endString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!dates.isEmpty()) {
|
||||
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), dates.first(), dates.last(), firstValue);
|
||||
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), dates.first(), firstValue, dates.last(), finalValue, firstValue);
|
||||
theParams.add(nextEntity);
|
||||
}
|
||||
}
|
||||
|
@ -811,7 +821,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
private void addDateTimeTypes(String theResourceType, Set<ResourceIndexedSearchParamDate> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
|
||||
IPrimitiveType<Date> nextBaseDateTime = (IPrimitiveType<Date>) theValue;
|
||||
if (nextBaseDateTime.getValue() != null) {
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), nextBaseDateTime.getValue(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString());
|
||||
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(myPartitionSettings ,theResourceType, theSearchParam.getName(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString(), nextBaseDateTime.getValueAsString());
|
||||
theParams.add(param);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ public final class ResourceIndexedSearchParams {
|
|||
return myPopulatedResourceLinkParameters;
|
||||
}
|
||||
|
||||
public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
|
||||
public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
|
||||
if (theParamDef == null) {
|
||||
return false;
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ public final class ResourceIndexedSearchParams {
|
|||
}
|
||||
Predicate<BaseResourceIndexedSearchParam> namedParamPredicate = param ->
|
||||
param.getParamName().equalsIgnoreCase(theParamName) &&
|
||||
param.matches(theParam);
|
||||
param.matches(theParam, theUseOrdinalDatesForDayComparison);
|
||||
|
||||
return resourceParams.stream().anyMatch(namedParamPredicate);
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@ public class InMemoryResourceMatcher {
|
|||
if (theSearchParams == null) {
|
||||
return InMemoryMatchResult.successfulMatch();
|
||||
} else {
|
||||
return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
|
||||
return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams, myModelConfig.getUseOrdinalDatesForDayPrecisionSearches())));
|
||||
}
|
||||
case COMPOSITE:
|
||||
case HAS:
|
||||
|
@ -216,8 +216,8 @@ public class InMemoryResourceMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams) {
|
||||
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token));
|
||||
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams,boolean theUseOrdinalDatesForDayComparison) {
|
||||
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token, theUseOrdinalDatesForDayComparison));
|
||||
}
|
||||
|
||||
private boolean hasChain(List<List<IQueryParameterType>> theAndOrParams) {
|
||||
|
|
|
@ -149,8 +149,8 @@ public class InMemoryResourceMatcherR5Test {
|
|||
|
||||
@Test
|
||||
public void testDateSupportedOps() {
|
||||
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false);
|
||||
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN, true, false, false);
|
||||
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false);
|
||||
testDateSupportedOp(ParamPrefixEnum.EQUAL, false, true, false);
|
||||
testDateSupportedOp(ParamPrefixEnum.LESSTHAN_OR_EQUALS, false, true, true);
|
||||
testDateSupportedOp(ParamPrefixEnum.LESSTHAN, false, false, true);
|
||||
|
@ -166,7 +166,7 @@ public class InMemoryResourceMatcherR5Test {
|
|||
{
|
||||
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + OBSERVATION_DATE, myObservation, mySearchParams);
|
||||
assertTrue(result.getUnsupportedReason(), result.supported());
|
||||
assertEquals(result.matched(), theSame);
|
||||
assertEquals(theSame, result.matched());
|
||||
}
|
||||
{
|
||||
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + LATE_DATE, myObservation, mySearchParams);
|
||||
|
@ -209,7 +209,7 @@ public class InMemoryResourceMatcherR5Test {
|
|||
private ResourceIndexedSearchParams extractDateSearchParam(Observation theObservation) {
|
||||
ResourceIndexedSearchParams retval = new ResourceIndexedSearchParams();
|
||||
BaseDateTimeType dateValue = (BaseDateTimeType) theObservation.getEffective();
|
||||
ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValue(), dateValue.getValueAsString());
|
||||
ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValueAsString());
|
||||
retval.myDateParams.add(dateParam);
|
||||
return retval;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue