001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.search.builder.predicate; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 024import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 025import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 026import ca.uhn.fhir.model.api.IQueryParameterType; 027import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 028import ca.uhn.fhir.rest.param.DateParam; 029import ca.uhn.fhir.rest.param.DateRangeParam; 030import ca.uhn.fhir.rest.param.ParamPrefixEnum; 031import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 032import ca.uhn.fhir.util.DateUtils; 033import com.google.common.annotations.VisibleForTesting; 034import com.healthmarketscience.sqlbuilder.ComboCondition; 035import com.healthmarketscience.sqlbuilder.Condition; 036import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039import org.springframework.beans.factory.annotation.Autowired; 040 041import java.util.Date; 042 043public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder { 044 045 private static final Logger ourLog = LoggerFactory.getLogger(DatePredicateBuilder.class); 046 private final DbColumn myColumnValueHigh; 047 private final DbColumn myColumnValueLow; 048 private final DbColumn myColumnValueLowDateOrdinal; 049 private final DbColumn myColumnValueHighDateOrdinal; 050 051 @Autowired 052 private JpaStorageSettings myStorageSettings; 053 054 /** 055 * Constructor 056 */ 057 public DatePredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) { 058 super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_DATE")); 059 060 myColumnValueLow = getTable().addColumn("SP_VALUE_LOW"); 061 myColumnValueHigh = getTable().addColumn("SP_VALUE_HIGH"); 062 myColumnValueLowDateOrdinal = getTable().addColumn("SP_VALUE_LOW_DATE_ORDINAL"); 063 myColumnValueHighDateOrdinal = getTable().addColumn("SP_VALUE_HIGH_DATE_ORDINAL"); 064 } 065 066 @VisibleForTesting 067 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 068 myStorageSettings = theStorageSettings; 069 } 070 071 public Condition createPredicateDateWithoutIdentityPredicate( 072 IQueryParameterType theParam, SearchFilterParser.CompareOperation theOperation) { 073 074 Condition p; 075 if (theParam instanceof DateParam) { 076 DateParam date = (DateParam) theParam; 077 if (!date.isEmpty()) { 078 if (theOperation == SearchFilterParser.CompareOperation.ne) { 079 date = new DateParam(ParamPrefixEnum.EQUAL, date.getValueAsString()); 080 } 081 DateRangeParam range = new DateRangeParam(date); 082 p = createPredicateDateFromRange(range, theOperation); 083 } else { 084 // TODO: handle missing date param? 085 p = null; 086 } 087 } else if (theParam instanceof DateRangeParam) { 088 DateRangeParam range = (DateRangeParam) theParam; 089 p = createPredicateDateFromRange(range, theOperation); 090 } else { 091 throw new IllegalArgumentException(Msg.code(1251) + "Invalid token type: " + theParam.getClass()); 092 } 093 094 return p; 095 } 096 097 private Condition createPredicateDateFromRange( 098 DateRangeParam theRange, SearchFilterParser.CompareOperation theOperation) { 099 100 DatePredicateBounds datePredicateBounds = new DatePredicateBounds(theRange); 101 102 return datePredicateBounds.calculate(theOperation); 103 } 104 105 public DbColumn getColumnValueLow() { 106 return myColumnValueLow; 107 } 108 109 private boolean isNullOrDatePrecision(DateParam theDateParam) { 110 return theDateParam == null || theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal(); 111 } 112 113 private Condition createPredicate(ColumnEnum theColumn, ParamPrefixEnum theComparator, Object theValue) { 114 115 DbColumn column; 116 switch (theColumn) { 117 case LOW: 118 column = myColumnValueLow; 119 break; 120 case LOW_DATE_ORDINAL: 121 column = myColumnValueLowDateOrdinal; 122 break; 123 case HIGH: 124 column = myColumnValueHigh; 125 break; 126 case HIGH_DATE_ORDINAL: 127 column = myColumnValueHighDateOrdinal; 128 break; 129 default: 130 throw new IllegalArgumentException(Msg.code(1256)); 131 } 132 133 return createConditionForValueWithComparator(theComparator, column, theValue); 134 } 135 136 public enum ColumnEnum { 137 LOW, 138 LOW_DATE_ORDINAL, 139 HIGH, 140 HIGH_DATE_ORDINAL 141 } 142 143 public class DatePredicateBounds { 144 private DatePredicateBuilder.ColumnEnum myLowValueField; 145 private DatePredicateBuilder.ColumnEnum myHighValueField; 146 147 private Condition myLowerBoundCondition = null; 148 private Condition myUpperBoundCondition = null; 149 150 private final Date myLowerBoundInstant; 151 private final Date myUpperBoundInstant; 152 153 private final DateParam myLowerBound; 154 private final DateParam myUpperBound; 155 156 private final Integer myLowerBoundAsOrdinal; 157 private final Integer myUpperBoundAsOrdinal; 158 private Comparable<?> myGenericLowerBound; 159 private Comparable<?> myGenericUpperBound; 160 161 public DatePredicateBounds(DateRangeParam theRange) { 162 myLowerBoundInstant = theRange.getLowerBoundAsInstant(); 163 myUpperBoundInstant = theRange.getUpperBoundAsInstant(); 164 165 myLowerBound = theRange.getLowerBound(); 166 myUpperBound = theRange.getUpperBound(); 167 myLowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger(); 168 myUpperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger(); 169 170 init(); 171 } 172 173 public Condition calculate(SearchFilterParser.CompareOperation theOperation) { 174 if (theOperation == SearchFilterParser.CompareOperation.lt 175 || theOperation == SearchFilterParser.CompareOperation.le) { 176 // use lower bound first 177 handleLessThanAndLessThanOrEqualTo(); 178 } else if (theOperation == SearchFilterParser.CompareOperation.gt 179 || theOperation == SearchFilterParser.CompareOperation.ge) { 180 // use upper bound first, e.g value between 6 and 10 181 handleGreaterThanAndGreaterThanOrEqualTo(); 182 } else if (theOperation == SearchFilterParser.CompareOperation.ne) { 183 if ((myLowerBoundInstant == null) || (myUpperBoundInstant == null)) { 184 throw new InvalidRequestException(Msg.code(1254) 185 + "lowerBound and/or upperBound value not correctly specified for compare theOperation"); 186 } 187 Condition lessThan = DatePredicateBuilder.this.createPredicate( 188 myLowValueField, ParamPrefixEnum.LESSTHAN, myGenericLowerBound); 189 Condition greaterThan = DatePredicateBuilder.this.createPredicate( 190 myHighValueField, ParamPrefixEnum.GREATERTHAN, myGenericUpperBound); 191 myLowerBoundCondition = ComboCondition.or(lessThan, greaterThan); 192 } else if ((theOperation == SearchFilterParser.CompareOperation.eq) 193 || (theOperation == SearchFilterParser.CompareOperation.sa) 194 || (theOperation == SearchFilterParser.CompareOperation.eb) 195 || (theOperation == null)) { 196 197 handleEqualToCompareOperator(); 198 } else { 199 throw new InvalidRequestException(Msg.code(1255) 200 + String.format("Unsupported operator specified, operator=%s", theOperation.name())); 201 } 202 203 if (isOrdinalComparison()) { 204 ourLog.trace("Ordinal date range is {} - {} ", myLowerBoundAsOrdinal, myUpperBoundAsOrdinal); 205 } else { 206 ourLog.trace("Date range is {} - {}", myLowerBoundInstant, myUpperBoundInstant); 207 } 208 209 if (myLowerBoundCondition != null && myUpperBoundCondition != null) { 210 return (ComboCondition.and(myLowerBoundCondition, myUpperBoundCondition)); 211 } else if (myLowerBoundCondition != null) { 212 return (myLowerBoundCondition); 213 } else { 214 return (myUpperBoundCondition); 215 } 216 } 217 218 private void handleEqualToCompareOperator() { 219 Condition lessThan; 220 Condition greaterThan; 221 if (myLowerBoundInstant != null && myUpperBoundInstant != null) { 222 // both upper and lower bound 223 // lowerbound; :lowerbound <= low_field <= :upperbound 224 greaterThan = ComboCondition.and( 225 DatePredicateBuilder.this.createPredicate( 226 myLowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericLowerBound), 227 DatePredicateBuilder.this.createPredicate( 228 myLowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericUpperBound)); 229 // upperbound; :lowerbound <= high_field <= :upperbound 230 lessThan = ComboCondition.and( 231 DatePredicateBuilder.this.createPredicate( 232 myHighValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericUpperBound), 233 DatePredicateBuilder.this.createPredicate( 234 myHighValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericLowerBound)); 235 236 myLowerBoundCondition = greaterThan; 237 myUpperBoundCondition = lessThan; 238 } else if (myLowerBoundInstant != null) { 239 // lower bound only 240 greaterThan = DatePredicateBuilder.this.createPredicate( 241 myLowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericLowerBound); 242 lessThan = DatePredicateBuilder.this.createPredicate( 243 myHighValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericLowerBound); 244 245 if (myLowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER 246 || myLowerBound.getPrefix() == ParamPrefixEnum.EQUAL) { 247 myLowerBoundCondition = greaterThan; 248 } else { 249 myLowerBoundCondition = ComboCondition.or(greaterThan, lessThan); 250 } 251 } else { 252 // only upper bound provided 253 greaterThan = DatePredicateBuilder.this.createPredicate( 254 myLowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericUpperBound); 255 lessThan = DatePredicateBuilder.this.createPredicate( 256 myHighValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericUpperBound); 257 258 if (myUpperBound.getPrefix() == ParamPrefixEnum.ENDS_BEFORE 259 || myUpperBound.getPrefix() == ParamPrefixEnum.EQUAL) { 260 myUpperBoundCondition = lessThan; 261 } else { 262 myUpperBoundCondition = ComboCondition.or(greaterThan, lessThan); 263 } 264 } 265 } 266 267 private void handleGreaterThanAndGreaterThanOrEqualTo() { 268 if (myUpperBoundInstant != null) { 269 // upper bound only 270 myUpperBoundCondition = DatePredicateBuilder.this.createPredicate( 271 myHighValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericUpperBound); 272 if (myStorageSettings.isAccountForDateIndexNulls()) { 273 myUpperBoundCondition = ComboCondition.or( 274 myUpperBoundCondition, 275 DatePredicateBuilder.this.createPredicate( 276 myLowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericUpperBound)); 277 } 278 } else if (myLowerBoundInstant != null) { 279 // lower bound only 280 myLowerBoundCondition = DatePredicateBuilder.this.createPredicate( 281 myHighValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericLowerBound); 282 if (myStorageSettings.isAccountForDateIndexNulls()) { 283 myLowerBoundCondition = ComboCondition.or( 284 myLowerBoundCondition, 285 DatePredicateBuilder.this.createPredicate( 286 myLowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, myGenericLowerBound)); 287 } 288 } else { 289 throw new InvalidRequestException( 290 Msg.code(1253) 291 + "upperBound and lowerBound value not correctly specified for greater than (or equal to) compare operator"); 292 } 293 } 294 295 /** 296 * Handle (LOW|HIGH)_FIELD <(=) value 297 */ 298 private void handleLessThanAndLessThanOrEqualTo() { 299 if (myLowerBoundInstant != null) { 300 // lower bound only provided 301 myLowerBoundCondition = DatePredicateBuilder.this.createPredicate( 302 myLowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericLowerBound); 303 304 if (myStorageSettings.isAccountForDateIndexNulls()) { 305 myLowerBoundCondition = ComboCondition.or( 306 myLowerBoundCondition, 307 DatePredicateBuilder.this.createPredicate( 308 myHighValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericLowerBound)); 309 } 310 } else if (myUpperBoundInstant != null) { 311 // upper bound only provided 312 myUpperBoundCondition = DatePredicateBuilder.this.createPredicate( 313 myLowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericUpperBound); 314 if (myStorageSettings.isAccountForDateIndexNulls()) { 315 myUpperBoundCondition = ComboCondition.or( 316 myUpperBoundCondition, 317 DatePredicateBuilder.this.createPredicate( 318 myHighValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, myGenericUpperBound)); 319 } 320 } else { 321 throw new InvalidRequestException( 322 Msg.code(1252) 323 + "lowerBound and upperBound value not correctly specified for comparing using lower than (or equal to) compare operator"); 324 } 325 } 326 327 private void init() { 328 if (isOrdinalComparison()) { 329 myLowValueField = DatePredicateBuilder.ColumnEnum.LOW_DATE_ORDINAL; 330 myHighValueField = DatePredicateBuilder.ColumnEnum.HIGH_DATE_ORDINAL; 331 myGenericLowerBound = myLowerBoundAsOrdinal; 332 myGenericUpperBound = myUpperBoundAsOrdinal; 333 if (myUpperBound != null 334 && myUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 335 myGenericUpperBound = Integer.parseInt(DateUtils.getCompletedDate(myUpperBound.getValueAsString()) 336 .getRight() 337 .replace("-", "")); 338 } 339 } else { 340 myLowValueField = DatePredicateBuilder.ColumnEnum.LOW; 341 myHighValueField = DatePredicateBuilder.ColumnEnum.HIGH; 342 myGenericLowerBound = myLowerBoundInstant; 343 myGenericUpperBound = myUpperBoundInstant; 344 if (myUpperBound != null 345 && myUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 346 String theCompleteDateStr = DateUtils.getCompletedDate(myUpperBound.getValueAsString()) 347 .getRight() 348 .replace("-", ""); 349 myGenericUpperBound = DateUtils.parseDate(theCompleteDateStr); 350 } 351 } 352 } 353 354 /** 355 * If all present search parameters are of DAY precision, and {@link ca.uhn.fhir.jpa.model.entity.StorageSettings#getUseOrdinalDatesForDayPrecisionSearches()} is true, 356 * then we attempt to use the ordinal field for date comparisons instead of the date field. 357 */ 358 private boolean isOrdinalComparison() { 359 return isNullOrDatePrecision(myLowerBound) 360 && isNullOrDatePrecision(myUpperBound) 361 && myStorageSettings.getUseOrdinalDatesForDayPrecisionSearches(); 362 } 363 } 364}