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.dao.search; 021 022import ca.uhn.fhir.context.RuntimeSearchParam; 023import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 024import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 025import ca.uhn.fhir.model.api.IQueryParameterType; 026import ca.uhn.fhir.rest.api.Constants; 027import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 028import ca.uhn.fhir.rest.param.CompositeParam; 029import ca.uhn.fhir.rest.param.DateParam; 030import ca.uhn.fhir.rest.param.NumberParam; 031import ca.uhn.fhir.rest.param.QuantityParam; 032import ca.uhn.fhir.rest.param.ReferenceParam; 033import ca.uhn.fhir.rest.param.StringParam; 034import ca.uhn.fhir.rest.param.TokenParam; 035import ca.uhn.fhir.rest.param.UriParam; 036import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 037import com.google.common.collect.Lists; 038import com.google.common.collect.Sets; 039import org.apache.commons.collections4.CollectionUtils; 040import org.apache.commons.lang3.BooleanUtils; 041import org.apache.commons.lang3.StringUtils; 042 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.List; 046import java.util.Set; 047 048import static ca.uhn.fhir.rest.api.Constants.PARAMQUALIFIER_MISSING; 049 050/** 051 * Search builder for HSearch for token, string, and reference parameters. 052 */ 053public class ExtendedHSearchSearchBuilder { 054 public static final String EMPTY_MODIFIER = ""; 055 056 /** 057 * These params have complicated semantics, or are best resolved at the JPA layer for now. 058 */ 059 public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta"); 060 061 /** 062 * Are any of the queries supported by our indexing? 063 */ 064 public boolean isSupportsSomeOf(SearchParameterMap myParams) { 065 return myParams.getSort() != null 066 || myParams.getLastUpdated() != null 067 || myParams.entrySet().stream() 068 .filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey())) 069 // each and clause may have a different modifier, so split down to the ORs 070 .flatMap(andList -> andList.getValue().stream()) 071 .flatMap(Collection::stream) 072 .anyMatch(this::isParamTypeSupported); 073 } 074 075 /** 076 * Are all the queries supported by our indexing? 077 */ 078 public boolean isSupportsAllOf(SearchParameterMap myParams) { 079 return CollectionUtils.isEmpty(myParams.getRevIncludes()) 080 && // ??? 081 CollectionUtils.isEmpty(myParams.getIncludes()) 082 && // ??? 083 myParams.getEverythingMode() == null 084 && // ??? 085 BooleanUtils.isFalse(myParams.isDeleteExpunge()) 086 && // ??? 087 088 // not yet supported in HSearch 089 myParams.getNearDistanceParam() == null 090 && // ??? 091 092 // not yet supported in HSearch 093 myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE 094 && // ??? 095 myParams.entrySet().stream() 096 .filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey())) 097 // each and clause may have a different modifier, so split down to the ORs 098 .flatMap(andList -> andList.getValue().stream()) 099 .flatMap(Collection::stream) 100 .allMatch(this::isParamTypeSupported); 101 } 102 103 /** 104 * Do we support this query param type+modifier? 105 * <p> 106 * NOTE - keep this in sync with addAndConsumeAdvancedQueryClauses() below. 107 */ 108 private boolean isParamTypeSupported(IQueryParameterType param) { 109 String modifier = StringUtils.defaultString(param.getQueryParameterQualifier(), EMPTY_MODIFIER); 110 if (param instanceof TokenParam) { 111 switch (modifier) { 112 case Constants.PARAMQUALIFIER_TOKEN_TEXT: 113 case "": 114 // we support plain token and token:text 115 return true; 116 default: 117 return false; 118 } 119 } else if (param instanceof StringParam) { 120 switch (modifier) { 121 // we support string:text, string:contains, string:exact, and unmodified string. 122 case Constants.PARAMQUALIFIER_STRING_TEXT: 123 case Constants.PARAMQUALIFIER_STRING_EXACT: 124 case Constants.PARAMQUALIFIER_STRING_CONTAINS: 125 case EMPTY_MODIFIER: 126 return true; 127 default: 128 return false; 129 } 130 } else if (param instanceof QuantityParam) { 131 return modifier.equals(EMPTY_MODIFIER); 132 133 } else if (param instanceof CompositeParam) { 134 switch (modifier) { 135 case PARAMQUALIFIER_MISSING: 136 return false; 137 default: 138 return true; 139 } 140 141 } else if (param instanceof ReferenceParam) { 142 // We cannot search by chain. 143 if (((ReferenceParam) param).getChain() != null) { 144 return false; 145 } 146 switch (modifier) { 147 case EMPTY_MODIFIER: 148 return true; 149 case Constants.PARAMQUALIFIER_MDM: 150 case Constants.PARAMQUALIFIER_NICKNAME: 151 default: 152 return false; 153 } 154 } else if (param instanceof DateParam) { 155 return modifier.equals(EMPTY_MODIFIER); 156 157 } else if (param instanceof UriParam) { 158 return modifier.equals(EMPTY_MODIFIER); 159 160 } else if (param instanceof NumberParam) { 161 return modifier.equals(EMPTY_MODIFIER); 162 163 } else { 164 return false; 165 } 166 } 167 168 public void addAndConsumeAdvancedQueryClauses( 169 ExtendedHSearchClauseBuilder builder, 170 String theResourceType, 171 SearchParameterMap theParams, 172 ISearchParamRegistry theSearchParamRegistry) { 173 // copy the keys to avoid concurrent modification error 174 ArrayList<String> paramNames = compileParamNames(theParams); 175 for (String nextParam : paramNames) { 176 if (ourUnsafeSearchParmeters.contains(nextParam)) { 177 continue; 178 } 179 RuntimeSearchParam activeParam = theSearchParamRegistry.getActiveSearchParam(theResourceType, nextParam); 180 if (activeParam == null) { 181 // ignore magic params handled in JPA 182 continue; 183 } 184 185 // NOTE - keep this in sync with isParamSupported() above. 186 switch (activeParam.getParamType()) { 187 case TOKEN: 188 List<List<IQueryParameterType>> tokenTextAndOrTerms = 189 theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT); 190 builder.addStringTextSearch(nextParam, tokenTextAndOrTerms); 191 192 List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms = 193 theParams.removeByNameUnmodified(nextParam); 194 builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms); 195 break; 196 197 case STRING: 198 List<List<IQueryParameterType>> stringTextAndOrTerms = 199 theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT); 200 builder.addStringTextSearch(nextParam, stringTextAndOrTerms); 201 202 List<List<IQueryParameterType>> stringExactAndOrTerms = 203 theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_EXACT); 204 builder.addStringExactSearch(nextParam, stringExactAndOrTerms); 205 206 List<List<IQueryParameterType>> stringContainsAndOrTerms = 207 theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS); 208 builder.addStringContainsSearch(nextParam, stringContainsAndOrTerms); 209 210 List<List<IQueryParameterType>> stringAndOrTerms = theParams.removeByNameUnmodified(nextParam); 211 builder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms); 212 break; 213 214 case QUANTITY: 215 List<List<IQueryParameterType>> quantityAndOrTerms = theParams.removeByNameUnmodified(nextParam); 216 builder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms); 217 break; 218 219 case REFERENCE: 220 List<List<IQueryParameterType>> referenceAndOrTerms = theParams.removeByNameUnmodified(nextParam); 221 builder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms); 222 break; 223 224 case DATE: 225 List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated") 226 ? getLastUpdatedAndOrList(theParams) 227 : theParams.removeByNameUnmodified(nextParam); 228 builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms); 229 break; 230 231 case COMPOSITE: 232 List<List<IQueryParameterType>> compositeAndOrTerms = theParams.removeByNameUnmodified(nextParam); 233 // RuntimeSearchParam only points to the subs by reference. Resolve here while we have 234 // ISearchParamRegistry 235 List<RuntimeSearchParam> subSearchParams = 236 JpaParamUtil.resolveCompositeComponentsDeclaredOrder(theSearchParamRegistry, activeParam); 237 builder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms); 238 break; 239 240 case URI: 241 List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = 242 theParams.removeByNameUnmodified(nextParam); 243 builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms); 244 break; 245 246 case NUMBER: 247 List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = theParams.remove(nextParam); 248 builder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms); 249 break; 250 251 default: 252 // ignore unsupported param types/modifiers. They will be processed up in SearchBuilder. 253 } 254 } 255 } 256 257 private List<List<IQueryParameterType>> getLastUpdatedAndOrList(SearchParameterMap theParams) { 258 DateParam activeBound = theParams.getLastUpdated().getLowerBound() != null 259 ? theParams.getLastUpdated().getLowerBound() 260 : theParams.getLastUpdated().getUpperBound(); 261 262 List<List<IQueryParameterType>> result = List.of(List.of(activeBound)); 263 264 // indicate parameter was processed 265 theParams.setLastUpdated(null); 266 267 return result; 268 } 269 270 /** 271 * Param name list is not only the params.keySet, but also the "special" parameters extracted from input 272 * (as _lastUpdated when the input myLastUpdated field is not null, etc). 273 */ 274 private ArrayList<String> compileParamNames(SearchParameterMap theParams) { 275 ArrayList<String> nameList = Lists.newArrayList(theParams.keySet()); 276 277 if (theParams.getLastUpdated() != null) { 278 nameList.add("_lastUpdated"); 279 } 280 281 return nameList; 282 } 283}