
001package ca.uhn.fhir.jpa.search.builder.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.jpa.api.config.DaoConfig; 025import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 026import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 027import ca.uhn.fhir.model.api.IQueryParameterType; 028import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 029import ca.uhn.fhir.rest.param.DateParam; 030import ca.uhn.fhir.rest.param.DateRangeParam; 031import ca.uhn.fhir.rest.param.ParamPrefixEnum; 032import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 033import ca.uhn.fhir.util.DateUtils; 034import com.google.common.annotations.VisibleForTesting; 035import com.healthmarketscience.sqlbuilder.ComboCondition; 036import com.healthmarketscience.sqlbuilder.Condition; 037import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040import org.springframework.beans.factory.annotation.Autowired; 041 042import java.util.Date; 043 044public class DatePredicateBuilder extends BaseSearchParamPredicateBuilder { 045 046 private static final Logger ourLog = LoggerFactory.getLogger(DatePredicateBuilder.class); 047 private final DbColumn myColumnValueHigh; 048 private final DbColumn myColumnValueLow; 049 private final DbColumn myColumnValueLowDateOrdinal; 050 private final DbColumn myColumnValueHighDateOrdinal; 051 @Autowired 052 private DaoConfig myDaoConfig; 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 setDaoConfigForUnitTest(DaoConfig theDaoConfig) { 068 myDaoConfig = theDaoConfig; 069 } 070 071 public Condition createPredicateDateWithoutIdentityPredicate(IQueryParameterType theParam, 072 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(DateRangeParam theRange, 098 SearchFilterParser.CompareOperation theOperation) { 099 100 101 Date lowerBoundInstant = theRange.getLowerBoundAsInstant(); 102 Date upperBoundInstant = theRange.getUpperBoundAsInstant(); 103 104 DateParam lowerBound = theRange.getLowerBound(); 105 DateParam upperBound = theRange.getUpperBound(); 106 Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger(); 107 Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger(); 108 Comparable<?> genericLowerBound; 109 Comparable<?> genericUpperBound; 110 111 /* 112 * If all present search parameters are of DAY precision, and {@link ca.uhn.fhir.jpa.model.entity.ModelConfig#getUseOrdinalDatesForDayPrecisionSearches()} is true, 113 * then we attempt to use the ordinal field for date comparisons instead of the date field. 114 */ 115 boolean isOrdinalComparison = isNullOrDatePrecision(lowerBound) && isNullOrDatePrecision(upperBound) && myDaoConfig.getModelConfig().getUseOrdinalDatesForDayPrecisionSearches(); 116 117 Condition lt; 118 Condition gt; 119 Condition lb = null; 120 Condition ub = null; 121 DatePredicateBuilder.ColumnEnum lowValueField; 122 DatePredicateBuilder.ColumnEnum highValueField; 123 124 if (isOrdinalComparison) { 125 lowValueField = DatePredicateBuilder.ColumnEnum.LOW_DATE_ORDINAL; 126 highValueField = DatePredicateBuilder.ColumnEnum.HIGH_DATE_ORDINAL; 127 genericLowerBound = lowerBoundAsOrdinal; 128 genericUpperBound = upperBoundAsOrdinal; 129 if (upperBound != null && upperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 130 genericUpperBound = Integer.parseInt(DateUtils.getCompletedDate(upperBound.getValueAsString()).getRight().replace("-", "")); 131 } 132 } else { 133 lowValueField = DatePredicateBuilder.ColumnEnum.LOW; 134 highValueField = DatePredicateBuilder.ColumnEnum.HIGH; 135 genericLowerBound = lowerBoundInstant; 136 genericUpperBound = upperBoundInstant; 137 if (upperBound != null && upperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.MONTH.ordinal()) { 138 String theCompleteDateStr = DateUtils.getCompletedDate(upperBound.getValueAsString()).getRight().replace("-", ""); 139 genericUpperBound = DateUtils.parseDate(theCompleteDateStr); 140 } 141 } 142 143 if (theOperation == SearchFilterParser.CompareOperation.lt || theOperation == SearchFilterParser.CompareOperation.le) { 144 // use lower bound first 145 if (lowerBoundInstant != null) { 146 lb = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericLowerBound); 147 if (myDaoConfig.isAccountForDateIndexNulls()) { 148 lb = ComboCondition.or(lb, this.createPredicate(highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericLowerBound)); 149 } 150 } else if (upperBoundInstant != null) { 151 ub = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound); 152 if (myDaoConfig.isAccountForDateIndexNulls()) { 153 ub = ComboCondition.or(ub, this.createPredicate(highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound)); 154 } 155 } else { 156 throw new InvalidRequestException(Msg.code(1252) + "lowerBound and upperBound value not correctly specified for comparing " + theOperation); 157 } 158 } else if (theOperation == SearchFilterParser.CompareOperation.gt || theOperation == SearchFilterParser.CompareOperation.ge) { 159 // use upper bound first, e.g value between 6 and 10 160 if (upperBoundInstant != null) { 161 ub = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericUpperBound); 162 if (myDaoConfig.isAccountForDateIndexNulls()) { 163 ub = ComboCondition.or(ub, this.createPredicate(lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericUpperBound)); 164 } 165 } else if (lowerBoundInstant != null) { 166 lb = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound); 167 if (myDaoConfig.isAccountForDateIndexNulls()) { 168 lb = ComboCondition.or(lb, this.createPredicate(lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound)); 169 } 170 } else { 171 throw new InvalidRequestException(Msg.code(1253) + "upperBound and lowerBound value not correctly specified for compare theOperation"); 172 } 173 } else if (theOperation == SearchFilterParser.CompareOperation.ne) { 174 if ((lowerBoundInstant == null) || 175 (upperBoundInstant == null)) { 176 throw new InvalidRequestException(Msg.code(1254) + "lowerBound and/or upperBound value not correctly specified for compare theOperation"); 177 } 178 lt = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN, genericLowerBound); 179 gt = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN, genericUpperBound); 180 lb = ComboCondition.or(lt, gt); 181 } else if ((theOperation == SearchFilterParser.CompareOperation.eq) 182 || (theOperation == SearchFilterParser.CompareOperation.sa) 183 || (theOperation == SearchFilterParser.CompareOperation.eb) 184 || (theOperation == null)) { 185 if (lowerBoundInstant != null) { 186 gt = this.createPredicate(lowValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound); 187 lt = this.createPredicate(highValueField, ParamPrefixEnum.GREATERTHAN_OR_EQUALS, genericLowerBound); 188 189 if (lowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER || lowerBound.getPrefix() == ParamPrefixEnum.EQUAL) { 190 lb = gt; 191 } else { 192 lb = ComboCondition.or(gt, lt); 193 } 194 } 195 196 if (upperBoundInstant != null) { 197 gt = this.createPredicate(lowValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound); 198 lt = this.createPredicate(highValueField, ParamPrefixEnum.LESSTHAN_OR_EQUALS, genericUpperBound); 199 200 201 if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { 202 ub = lt; 203 } else { 204 ub = ComboCondition.or(gt, lt); 205 } 206 } 207 } else { 208 throw new InvalidRequestException(Msg.code(1255) + String.format("Unsupported operator specified, operator=%s", 209 theOperation.name())); 210 } 211 if (isOrdinalComparison) { 212 ourLog.trace("Ordinal date range is {} - {} ", lowerBoundAsOrdinal, upperBoundAsOrdinal); 213 } else { 214 ourLog.trace("Date range is {} - {}", lowerBoundInstant, upperBoundInstant); 215 } 216 217 if (lb != null && ub != null) { 218 return (ComboCondition.and(lb, ub)); 219 } else if (lb != null) { 220 return (lb); 221 } else { 222 return (ub); 223 } 224 } 225 226 public DbColumn getColumnValueLow() { 227 return myColumnValueLow; 228 } 229 230 private boolean isNullOrDatePrecision(DateParam theDateParam) { 231 return theDateParam == null || theDateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal(); 232 } 233 234 private Condition createPredicate(ColumnEnum theColumn, ParamPrefixEnum theComparator, Object theValue) { 235 236 DbColumn column; 237 switch (theColumn) { 238 case LOW: 239 column = myColumnValueLow; 240 break; 241 case LOW_DATE_ORDINAL: 242 column = myColumnValueLowDateOrdinal; 243 break; 244 case HIGH: 245 column = myColumnValueHigh; 246 break; 247 case HIGH_DATE_ORDINAL: 248 column = myColumnValueHighDateOrdinal; 249 break; 250 default: 251 throw new IllegalArgumentException(Msg.code(1256)); 252 } 253 254 return createConditionForValueWithComparator(theComparator, column, theValue); 255 256 } 257 258 259 public enum ColumnEnum { 260 261 LOW, 262 LOW_DATE_ORDINAL, 263 HIGH, 264 HIGH_DATE_ORDINAL 265 266 } 267 268}