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