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