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}