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}