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}