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