
001package ca.uhn.fhir.jpa.dao.predicate; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; 027import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; 028import ca.uhn.fhir.model.api.IQueryParameterType; 029import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 030import ca.uhn.fhir.rest.param.DateParam; 031import ca.uhn.fhir.rest.param.DateRangeParam; 032import ca.uhn.fhir.rest.param.ParamPrefixEnum; 033import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036import org.springframework.context.annotation.Scope; 037import org.springframework.stereotype.Component; 038 039import javax.persistence.criteria.CriteriaBuilder; 040import javax.persistence.criteria.From; 041import javax.persistence.criteria.Predicate; 042import java.util.ArrayList; 043import java.util.Date; 044import java.util.List; 045import java.util.Map; 046 047@Component 048@Scope("prototype") 049public class PredicateBuilderDate extends BasePredicateBuilder implements IPredicateBuilder { 050 private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class); 051 052 053 public PredicateBuilderDate(LegacySearchBuilder theSearchBuilder) { 054 super(theSearchBuilder); 055 } 056 057 @Override 058 public Predicate addPredicate(String theResourceName, 059 RuntimeSearchParam theSearchParam, 060 List<? extends IQueryParameterType> theList, 061 SearchFilterParser.CompareOperation operation, 062 RequestPartitionId theRequestPartitionId) { 063 064 String paramName = theSearchParam.getName(); 065 boolean newJoin = false; 066 067 Map<String, From<?, ResourceIndexedSearchParamDate>> joinMap = myQueryStack.getJoinMap(); 068 String key = theResourceName + " " + paramName; 069 070 From<?, ResourceIndexedSearchParamDate> join = joinMap.get(key); 071 072 if (join == null) { 073 join = myQueryStack.createJoin(SearchBuilderJoinEnum.DATE, paramName); 074 joinMap.put(key, join); 075 newJoin = true; 076 } 077 078 if (theList.get(0).getMissing() != null) { 079 Boolean missing = theList.get(0).getMissing(); 080 addPredicateParamMissingForNonReference(theResourceName, paramName, missing, join, theRequestPartitionId); 081 return null; 082 } 083 084 List<Predicate> codePredicates = new ArrayList<>(); 085 086 for (IQueryParameterType nextOr : theList) { 087 Predicate p = createPredicateDate(nextOr, 088 myCriteriaBuilder, 089 join, 090 operation 091 ); 092 codePredicates.add(p); 093 } 094 095 Predicate orPredicates = myCriteriaBuilder.or(toArray(codePredicates)); 096 097 if (newJoin) { 098 Predicate identityAndValuePredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, paramName, join, orPredicates, theRequestPartitionId); 099 myQueryStack.addPredicateWithImplicitTypeSelection(identityAndValuePredicate); 100 } else { 101 myQueryStack.addPredicateWithImplicitTypeSelection(orPredicates); 102 } 103 104 return orPredicates; 105 } 106 107 public Predicate createPredicateDate(IQueryParameterType theParam, 108 String theResourceName, 109 String theParamName, 110 CriteriaBuilder theBuilder, 111 From<?, ResourceIndexedSearchParamDate> theFrom, 112 RequestPartitionId theRequestPartitionId) { 113 Predicate predicateDate = createPredicateDate(theParam, 114 theBuilder, 115 theFrom, 116 null 117 ); 118 return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, predicateDate, theRequestPartitionId); 119 } 120 121 private Predicate createPredicateDate(IQueryParameterType theParam, 122 CriteriaBuilder theBuilder, 123 From<?, ResourceIndexedSearchParamDate> theFrom, 124 SearchFilterParser.CompareOperation theOperation) { 125 126 Predicate p; 127 if (theParam instanceof DateParam) { 128 DateParam date = (DateParam) theParam; 129 if (!date.isEmpty()) { 130 if (theOperation == SearchFilterParser.CompareOperation.ne) { 131 date = new DateParam(ParamPrefixEnum.EQUAL, date.getValueAsString()); 132 } 133 DateRangeParam range = new DateRangeParam(date); 134 p = createPredicateDateFromRange(theBuilder, 135 theFrom, 136 range, 137 theOperation); 138 } else { 139 // TODO: handle missing date param? 140 p = null; 141 } 142 } else if (theParam instanceof DateRangeParam) { 143 DateRangeParam range = (DateRangeParam) theParam; 144 p = createPredicateDateFromRange(theBuilder, 145 theFrom, 146 range, 147 theOperation); 148 } else { 149 throw new IllegalArgumentException(Msg.code(1001) + "Invalid token type: " + theParam.getClass()); 150 } 151 152 return p; 153 } 154 155 private boolean isNullOrDayPrecision(DateParam theDateParam) { 156 return theDateParam == null || theDateParam.getPrecision().ordinal() == TemporalPrecisionEnum.DAY.ordinal(); 157 } 158 159 @SuppressWarnings("unchecked") 160 private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, 161 From<?, ResourceIndexedSearchParamDate> theFrom, 162 DateRangeParam theRange, 163 SearchFilterParser.CompareOperation operation) { 164 Date lowerBoundInstant = theRange.getLowerBoundAsInstant(); 165 Date upperBoundInstant = theRange.getUpperBoundAsInstant(); 166 167 DateParam lowerBound = theRange.getLowerBound(); 168 DateParam upperBound = theRange.getUpperBound(); 169 Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger(); 170 Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger(); 171 Comparable genericLowerBound; 172 Comparable genericUpperBound; 173 /** 174 * If all present search parameters are of DAY precision, and {@link DaoConfig#getUseOrdinalDatesForDayPrecisionSearches()} is true, 175 * then we attempt to use the ordinal field for date comparisons instead of the date field. 176 */ 177 boolean isOrdinalComparison = isNullOrDayPrecision(lowerBound) && isNullOrDayPrecision(upperBound) && myDaoConfig.getModelConfig().getUseOrdinalDatesForDayPrecisionSearches(); 178 179 Predicate lt = null; 180 Predicate gt = null; 181 Predicate lb = null; 182 Predicate ub = null; 183 String lowValueField; 184 String highValueField; 185 186 if (isOrdinalComparison) { 187 lowValueField = "myValueLowDateOrdinal"; 188 highValueField = "myValueHighDateOrdinal"; 189 genericLowerBound = lowerBoundAsOrdinal; 190 genericUpperBound = upperBoundAsOrdinal; 191 } else { 192 lowValueField = "myValueLow"; 193 highValueField = "myValueHigh"; 194 genericLowerBound = lowerBoundInstant; 195 genericUpperBound = upperBoundInstant; 196 } 197 198 if (operation == SearchFilterParser.CompareOperation.lt) { 199 // use lower bound first 200 if (lowerBoundInstant != null) { 201 // the value has been reduced one in this case 202 lb = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound); 203 if (myDaoConfig.isAccountForDateIndexNulls()) { 204 lb = theBuilder.or(lb, theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericLowerBound)); 205 } 206 } else { 207 if (upperBoundInstant != null) { 208 ub = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound); 209 if (myDaoConfig.isAccountForDateIndexNulls()) { 210 ub = theBuilder.or(ub, theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound)); 211 } 212 } else { 213 throw new InvalidRequestException(Msg.code(1002) + "lowerBound and upperBound value not correctly specified for compare theOperation"); 214 } 215 } 216 } else if (operation == SearchFilterParser.CompareOperation.le) { 217 // use lower bound first 218 if (lowerBoundInstant != null) { 219 lb = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound); 220 if (myDaoConfig.isAccountForDateIndexNulls()) { 221 lb = theBuilder.or(lb, theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericLowerBound)); 222 } 223 } else { 224 if (upperBoundInstant != null) { 225 ub = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound); 226 if (myDaoConfig.isAccountForDateIndexNulls()) { 227 ub = theBuilder.or(ub, theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound)); 228 } 229 } else { 230 throw new InvalidRequestException(Msg.code(1003) + "lowerBound and upperBound value not correctly specified for compare theOperation"); 231 } 232 } 233 } else if (operation == SearchFilterParser.CompareOperation.gt) { 234 // use upper bound first, e.g value between 6 and 10 235 // gt7 true, 10>7, gt11 false, 10>11 false, gt5 true, 10>5 236 if (upperBoundInstant != null) { 237 ub = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericUpperBound); 238 if (myDaoConfig.isAccountForDateIndexNulls()) { 239 ub = theBuilder.or(ub, theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound)); 240 } 241 } else { 242 if (lowerBoundInstant != null) { 243 lb = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound); 244 if (myDaoConfig.isAccountForDateIndexNulls()) { 245 lb = theBuilder.or(lb, theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound)); 246 } 247 } else { 248 throw new InvalidRequestException(Msg.code(1004) + "upperBound and lowerBound value not correctly specified for compare theOperation"); 249 } 250 } 251 } else if (operation == SearchFilterParser.CompareOperation.ge) { 252 // use upper bound first, e.g value between 6 and 10 253 // gt7 true, 10>7, gt11 false, 10>11 false, gt5 true, 10>5 254 if (upperBoundInstant != null) { 255 ub = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericUpperBound); 256 if (myDaoConfig.isAccountForDateIndexNulls()) { 257 ub = theBuilder.or(ub, theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound)); 258 } 259 } else { 260 if (lowerBoundInstant != null) { 261 lb = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound); 262 if (myDaoConfig.isAccountForDateIndexNulls()) { 263 lb = theBuilder.or(lb, theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound)); 264 } 265 } else { 266 throw new InvalidRequestException(Msg.code(1005) + "upperBound and lowerBound value not correctly specified for compare theOperation"); 267 } 268 } 269 } else if (operation == SearchFilterParser.CompareOperation.ne) { 270 if ((lowerBoundInstant == null) || 271 (upperBoundInstant == null)) { 272 throw new InvalidRequestException(Msg.code(1006) + "lowerBound and/or upperBound value not correctly specified for compare operation"); 273 } 274 lt = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound); 275 gt = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound); 276 lb = theBuilder.or(lt, gt); 277 } else if ((operation == SearchFilterParser.CompareOperation.eq) || (operation == null)) { 278 if (lowerBoundInstant != null) { 279 gt = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound); 280 lt = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound); 281 if (lowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER || lowerBound.getPrefix() == ParamPrefixEnum.EQUAL) { 282 lb = gt; 283 } else { 284 lb = theBuilder.or(gt, lt); 285 } 286 } 287 288 if (upperBoundInstant != null) { 289 gt = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound); 290 lt = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound); 291 292 293 if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { 294 ub = lt; 295 } else { 296 ub = theBuilder.or(gt, lt); 297 } 298 } 299 } else { 300 throw new InvalidRequestException(Msg.code(1007) + String.format("Unsupported operator specified, operator=%s", 301 operation.name())); 302 } 303 if (isOrdinalComparison) { 304 ourLog.trace("Ordinal date range is {} - {} ", lowerBoundAsOrdinal, upperBoundAsOrdinal); 305 } else { 306 ourLog.trace("Date range is {} - {}", lowerBoundInstant, upperBoundInstant); 307 } 308 309 if (lb != null && ub != null) { 310 return (theBuilder.and(lb, ub)); 311 } else if (lb != null) { 312 return (lb); 313 } else { 314 return (ub); 315 } 316 } 317}