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 Date lowerBoundInstant = theRange.getLowerBoundAsInstant(); 101 Date upperBoundInstant = theRange.getUpperBoundAsInstant(); 102 103 DateParam lowerBound = theRange.getLowerBound(); 104 DateParam upperBound = theRange.getUpperBound(); 105 Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger(); 106 Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger(); 107 Comparable<?> genericLowerBound; 108 Comparable<?> genericUpperBound; 109 110 /* 111 * If all present search parameters are of DAY precision, and {@link ca.uhn.fhir.jpa.model.entity.StorageSettings#getUseOrdinalDatesForDayPrecisionSearches()} is true, 112 * then we attempt to use the ordinal field for date comparisons instead of the date field. 113 */ 114 boolean isOrdinalComparison = isNullOrDatePrecision(lowerBound) 115 && isNullOrDatePrecision(upperBound) 116 && myStorageSettings.getUseOrdinalDatesForDayPrecisionSearches(); 117 118 Condition lt; 119 Condition gt; 120 Condition lb = null; 121 Condition ub = null; 122 DatePredicateBuilder.ColumnEnum lowValueField; 123 DatePredicateBuilder.ColumnEnum highValueField; 124 125 if (isOrdinalComparison) { 126 lowValueField = DatePredicateBuilder.ColumnEnum.LOW_DATE_ORDINAL; 127 highValueField = DatePredicateBuilder.ColumnEnum.HIGH_DATE_ORDINAL; 128 genericLowerBound = lowerBoundAsOrdinal; 129 genericUpperBound = upperBoundAsOrdinal; 130 if (upperBound != null && upperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 131 genericUpperBound = Integer.parseInt(DateUtils.getCompletedDate(upperBound.getValueAsString()) 132 .getRight() 133 .replace("-", "")); 134 } 135 } else { 136 lowValueField = DatePredicateBuilder.ColumnEnum.LOW; 137 highValueField = DatePredicateBuilder.ColumnEnum.HIGH; 138 genericLowerBound = lowerBoundInstant; 139 genericUpperBound = upperBoundInstant; 140 if (upperBound != null && upperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 141 String theCompleteDateStr = DateUtils.getCompletedDate(upperBound.getValueAsString()) 142 .getRight() 143 .replace("-", ""); 144 genericUpperBound = DateUtils.parseDate(theCompleteDateStr); 145 } 146 } 147 148 if (theOperation == SearchFilterParser.CompareOperation.lt 149 || theOperation == SearchFilterParser.CompareOperation.le) { 150 // use lower bound first 151 if (lowerBoundInstant != null) { 152 lb = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericLowerBound); 153 if (myStorageSettings.isAccountForDateIndexNulls()) { 154 lb = ComboCondition.or( 155 lb, 156 this.createPredicate( 157 highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericLowerBound)); 158 } 159 } else if (upperBoundInstant != null) { 160 ub = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound); 161 if (myStorageSettings.isAccountForDateIndexNulls()) { 162 ub = ComboCondition.or( 163 ub, 164 this.createPredicate( 165 highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound)); 166 } 167 } else { 168 throw new InvalidRequestException(Msg.code(1252) 169 + "lowerBound and upperBound value not correctly specified for comparing " + theOperation); 170 } 171 } else if (theOperation == SearchFilterParser.CompareOperation.gt 172 || theOperation == SearchFilterParser.CompareOperation.ge) { 173 // use upper bound first, e.g value between 6 and 10 174 if (upperBoundInstant != null) { 175 ub = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericUpperBound); 176 if (myStorageSettings.isAccountForDateIndexNulls()) { 177 ub = ComboCondition.or( 178 ub, 179 this.createPredicate( 180 lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericUpperBound)); 181 } 182 } else if (lowerBoundInstant != null) { 183 lb = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound); 184 if (myStorageSettings.isAccountForDateIndexNulls()) { 185 lb = ComboCondition.or( 186 lb, 187 this.createPredicate( 188 lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound)); 189 } 190 } else { 191 throw new InvalidRequestException(Msg.code(1253) 192 + "upperBound and lowerBound value not correctly specified for compare theOperation"); 193 } 194 } else if (theOperation == SearchFilterParser.CompareOperation.ne) { 195 if ((lowerBoundInstant == null) || (upperBoundInstant == null)) { 196 throw new InvalidRequestException(Msg.code(1254) 197 + "lowerBound and/or upperBound value not correctly specified for compare theOperation"); 198 } 199 lt = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN, genericLowerBound); 200 gt = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN, genericUpperBound); 201 lb = ComboCondition.or(lt, gt); 202 } else if ((theOperation == SearchFilterParser.CompareOperation.eq) 203 || (theOperation == SearchFilterParser.CompareOperation.sa) 204 || (theOperation == SearchFilterParser.CompareOperation.eb) 205 || (theOperation == null)) { 206 if (lowerBoundInstant != null) { 207 gt = this.createPredicate(lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound); 208 lt = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound); 209 210 if (lowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER 211 || lowerBound.getPrefix() == ParamPrefixEnum.EQUAL) { 212 lb = gt; 213 } else { 214 lb = ComboCondition.or(gt, lt); 215 } 216 } 217 218 if (upperBoundInstant != null) { 219 gt = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound); 220 lt = this.createPredicate(highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound); 221 222 if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE 223 || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { 224 ub = lt; 225 } else { 226 ub = ComboCondition.or(gt, lt); 227 } 228 } 229 } else { 230 throw new InvalidRequestException( 231 Msg.code(1255) + String.format("Unsupported operator specified, operator=%s", theOperation.name())); 232 } 233 if (isOrdinalComparison) { 234 ourLog.trace("Ordinal date range is {} - {} ", lowerBoundAsOrdinal, upperBoundAsOrdinal); 235 } else { 236 ourLog.trace("Date range is {} - {}", lowerBoundInstant, upperBoundInstant); 237 } 238 239 if (lb != null && ub != null) { 240 return (ComboCondition.and(lb, ub)); 241 } else if (lb != null) { 242 return (lb); 243 } else { 244 return (ub); 245 } 246 } 247 248 public DbColumn getColumnValueLow() { 249 return myColumnValueLow; 250 } 251 252 private boolean isNullOrDatePrecision(DateParam theDateParam) { 253 return theDateParam == null || theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal(); 254 } 255 256 private Condition createPredicate(ColumnEnum theColumn, ParamPrefixEnum theComparator, Object theValue) { 257 258 DbColumn column; 259 switch (theColumn) { 260 case LOW: 261 column = myColumnValueLow; 262 break; 263 case LOW_DATE_ORDINAL: 264 column = myColumnValueLowDateOrdinal; 265 break; 266 case HIGH: 267 column = myColumnValueHigh; 268 break; 269 case HIGH_DATE_ORDINAL: 270 column = myColumnValueHighDateOrdinal; 271 break; 272 default: 273 throw new IllegalArgumentException(Msg.code(1256)); 274 } 275 276 return createConditionForValueWithComparator(theComparator, column, theValue); 277 } 278 279 public enum ColumnEnum { 280 LOW, 281 LOW_DATE_ORDINAL, 282 HIGH, 283 HIGH_DATE_ORDINAL 284 } 285}