001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 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.util;
021
022import ca.uhn.fhir.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
024import ca.uhn.fhir.jpa.entity.Search;
025import ca.uhn.fhir.jpa.entity.SearchInclude;
026import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
027import ca.uhn.fhir.jpa.model.entity.ResourceTable;
028import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
029import ca.uhn.fhir.jpa.search.builder.sql.ColumnTupleObject;
030import ca.uhn.fhir.jpa.search.builder.sql.JpaPidValueTuples;
031import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
032import ca.uhn.fhir.model.api.Include;
033import ca.uhn.fhir.model.primitive.InstantDt;
034import ca.uhn.fhir.rest.param.DateRangeParam;
035import ca.uhn.fhir.rest.param.ParamPrefixEnum;
036import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
037import com.healthmarketscience.sqlbuilder.BinaryCondition;
038import com.healthmarketscience.sqlbuilder.ComboCondition;
039import com.healthmarketscience.sqlbuilder.Condition;
040import com.healthmarketscience.sqlbuilder.InCondition;
041import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
042import jakarta.annotation.Nonnull;
043import jakarta.annotation.Nullable;
044import jakarta.persistence.criteria.CriteriaBuilder;
045import jakarta.persistence.criteria.From;
046import jakarta.persistence.criteria.Predicate;
047import org.apache.commons.collections4.BidiMap;
048import org.apache.commons.collections4.bidimap.DualHashBidiMap;
049import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
050import org.apache.commons.lang3.ObjectUtils;
051import org.apache.commons.lang3.StringUtils;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055import java.util.ArrayList;
056import java.util.Arrays;
057import java.util.Date;
058import java.util.List;
059import java.util.Objects;
060import java.util.stream.Collectors;
061
062import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
063
064public class QueryParameterUtils {
065        private static final Logger ourLog = LoggerFactory.getLogger(QueryParameterUtils.class);
066        public static final int DEFAULT_SYNC_SIZE = 250;
067
068        private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
069        public static final Condition[] EMPTY_CONDITION_ARRAY = new Condition[0];
070
071        static {
072                DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix =
073                                new DualHashBidiMap<>();
074                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ap, ParamPrefixEnum.APPROXIMATE);
075                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eq, ParamPrefixEnum.EQUAL);
076                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.gt, ParamPrefixEnum.GREATERTHAN);
077                compareOperationToParamPrefix.put(
078                                SearchFilterParser.CompareOperation.ge, ParamPrefixEnum.GREATERTHAN_OR_EQUALS);
079                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.lt, ParamPrefixEnum.LESSTHAN);
080                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.le, ParamPrefixEnum.LESSTHAN_OR_EQUALS);
081                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ne, ParamPrefixEnum.NOT_EQUAL);
082                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eb, ParamPrefixEnum.ENDS_BEFORE);
083                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.sa, ParamPrefixEnum.STARTS_AFTER);
084                ourCompareOperationToParamPrefix = UnmodifiableBidiMap.unmodifiableBidiMap(compareOperationToParamPrefix);
085        }
086
087        @Nullable
088        public static Condition toAndPredicate(List<Condition> theAndPredicates) {
089                List<Condition> andPredicates =
090                                theAndPredicates.stream().filter(Objects::nonNull).collect(Collectors.toList());
091                if (andPredicates.size() == 0) {
092                        return null;
093                } else if (andPredicates.size() == 1) {
094                        return andPredicates.get(0);
095                } else {
096                        return ComboCondition.and(andPredicates.toArray(EMPTY_CONDITION_ARRAY));
097                }
098        }
099
100        @Nullable
101        public static Condition toOrPredicate(List<Condition> theOrPredicates) {
102                List<Condition> orPredicates =
103                                theOrPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
104                if (orPredicates.size() == 0) {
105                        return null;
106                } else if (orPredicates.size() == 1) {
107                        return orPredicates.get(0);
108                } else {
109                        return ComboCondition.or(orPredicates.toArray(EMPTY_CONDITION_ARRAY));
110                }
111        }
112
113        @Nullable
114        public static Condition toOrPredicate(Condition... theOrPredicates) {
115                return toOrPredicate(Arrays.asList(theOrPredicates));
116        }
117
118        @Nullable
119        public static Condition toAndPredicate(Condition... theAndPredicates) {
120                return toAndPredicate(Arrays.asList(theAndPredicates));
121        }
122
123        @Nonnull
124        public static Condition toInPredicate(
125                        ColumnTupleObject theColumns, JpaPidValueTuples theValues, boolean theInverse) {
126                return new InCondition(theColumns, theValues).setNegate(theInverse);
127        }
128
129        @Nonnull
130        public static Condition toEqualToOrInPredicate(
131                        DbColumn theColumn, List<String> theValuePlaceholders, boolean theInverse) {
132                if (theInverse) {
133                        return toNotEqualToOrNotInPredicate(theColumn, theValuePlaceholders);
134                } else {
135                        return toEqualToOrInPredicate(theColumn, theValuePlaceholders);
136                }
137        }
138
139        @Nonnull
140        public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
141                if (theValuePlaceholders.size() == 1) {
142                        return BinaryCondition.equalTo(theColumn, theValuePlaceholders.get(0));
143                }
144                return new InCondition(theColumn, theValuePlaceholders);
145        }
146
147        @Nonnull
148        public static Condition toNotEqualToOrNotInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
149                if (theValuePlaceholders.size() == 1) {
150                        return BinaryCondition.notEqualTo(theColumn, theValuePlaceholders.get(0));
151                }
152                return new InCondition(theColumn, theValuePlaceholders).setNegate(true);
153        }
154
155        public static SearchFilterParser.CompareOperation toOperation(ParamPrefixEnum thePrefix) {
156                SearchFilterParser.CompareOperation retVal = null;
157                if (thePrefix != null && ourCompareOperationToParamPrefix.containsValue(thePrefix)) {
158                        retVal = ourCompareOperationToParamPrefix.getKey(thePrefix);
159                }
160                return ObjectUtils.defaultIfNull(retVal, SearchFilterParser.CompareOperation.eq);
161        }
162
163        public static ParamPrefixEnum fromOperation(SearchFilterParser.CompareOperation thePrefix) {
164                ParamPrefixEnum retVal = null;
165                if (thePrefix != null && ourCompareOperationToParamPrefix.containsKey(thePrefix)) {
166                        retVal = ourCompareOperationToParamPrefix.get(thePrefix);
167                }
168                return ObjectUtils.defaultIfNull(retVal, ParamPrefixEnum.EQUAL);
169        }
170
171        public static String getChainedPart(String parameter) {
172                return parameter.substring(parameter.indexOf(".") + 1);
173        }
174
175        public static String getParamNameWithPrefix(String theSpnamePrefix, String theParamName) {
176
177                if (StringUtils.isBlank(theSpnamePrefix)) return theParamName;
178
179                return theSpnamePrefix + "." + theParamName;
180        }
181
182        public static Predicate[] toPredicateArray(List<Predicate> thePredicates) {
183                return thePredicates.toArray(new Predicate[0]);
184        }
185
186        private static List<Predicate> createLastUpdatedPredicates(
187                        final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
188                List<Predicate> lastUpdatedPredicates = new ArrayList<>();
189                if (theLastUpdated != null) {
190                        if (theLastUpdated.getLowerBoundAsInstant() != null) {
191                                ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
192                                Predicate predicateLower =
193                                                builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
194                                lastUpdatedPredicates.add(predicateLower);
195                        }
196                        if (theLastUpdated.getUpperBoundAsInstant() != null) {
197                                Predicate predicateUpper =
198                                                builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
199                                lastUpdatedPredicates.add(predicateUpper);
200                        }
201                }
202                return lastUpdatedPredicates;
203        }
204
205        public static void verifySearchHasntFailedOrThrowInternalErrorException(Search theSearch) {
206                if (theSearch.getStatus() == SearchStatusEnum.FAILED) {
207                        Integer status = theSearch.getFailureCode();
208                        status = defaultIfNull(status, 500);
209
210                        String message = theSearch.getFailureMessage();
211                        throw BaseServerResponseException.newInstance(status, message);
212                }
213        }
214
215        public static void populateSearchEntity(
216                        SearchParameterMap theParams,
217                        String theResourceType,
218                        String theSearchUuid,
219                        String theQueryString,
220                        Search theSearch,
221                        RequestPartitionId theRequestPartitionId) {
222                theSearch.setDeleted(false);
223                theSearch.setUuid(theSearchUuid);
224                theSearch.setCreated(new Date());
225                theSearch.setTotalCount(null);
226                theSearch.setNumFound(0);
227                theSearch.setPreferredPageSize(theParams.getCount());
228                theSearch.setSearchType(
229                                theParams.getEverythingMode() != null ? SearchTypeEnum.EVERYTHING : SearchTypeEnum.SEARCH);
230                theSearch.setLastUpdated(theParams.getLastUpdated());
231                theSearch.setResourceType(theResourceType);
232                theSearch.setStatus(SearchStatusEnum.LOADING);
233                theSearch.setSearchQueryString(theQueryString, theRequestPartitionId);
234
235                if (theParams.hasIncludes()) {
236                        for (Include next : theParams.getIncludes()) {
237                                theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), false, next.isRecurse()));
238                        }
239                }
240
241                for (Include next : theParams.getRevIncludes()) {
242                        theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), true, next.isRecurse()));
243                }
244        }
245}