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}