
001/*- 002 * #%L 003 * HAPI FHIR Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2023 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.searchparam.extractor; 021 022import ca.uhn.fhir.context.RuntimeSearchParam; 023import ca.uhn.fhir.jpa.model.config.PartitionSettings; 024import ca.uhn.fhir.jpa.model.entity.StorageSettings; 025import ca.uhn.fhir.jpa.model.entity.*; 026import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; 027import ca.uhn.fhir.jpa.searchparam.util.RuntimeSearchParamHelper; 028import ca.uhn.fhir.model.api.IQueryParameterType; 029import ca.uhn.fhir.rest.api.Constants; 030import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 031import ca.uhn.fhir.rest.param.QuantityParam; 032import ca.uhn.fhir.rest.param.ReferenceParam; 033import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 034import org.apache.commons.lang3.StringUtils; 035 036import java.util.ArrayList; 037import java.util.Collection; 038import java.util.Collections; 039import java.util.Date; 040import java.util.HashSet; 041import java.util.List; 042import java.util.Set; 043import java.util.function.Predicate; 044import javax.annotation.Nonnull; 045 046import static org.apache.commons.lang3.StringUtils.compare; 047import static org.apache.commons.lang3.StringUtils.isNotBlank; 048 049public final class ResourceIndexedSearchParams { 050 public final Collection<ResourceIndexedSearchParamString> myStringParams = new ArrayList<>(); 051 public final Collection<ResourceIndexedSearchParamToken> myTokenParams = new HashSet<>(); 052 public final Collection<ResourceIndexedSearchParamNumber> myNumberParams = new ArrayList<>(); 053 public final Collection<ResourceIndexedSearchParamQuantity> myQuantityParams = new ArrayList<>(); 054 public final Collection<ResourceIndexedSearchParamQuantityNormalized> myQuantityNormalizedParams = 055 new ArrayList<>(); 056 public final Collection<ResourceIndexedSearchParamDate> myDateParams = new ArrayList<>(); 057 public final Collection<ResourceIndexedSearchParamUri> myUriParams = new ArrayList<>(); 058 public final Collection<ResourceIndexedSearchParamCoords> myCoordsParams = new ArrayList<>(); 059 060 public final Collection<ResourceIndexedComboStringUnique> myComboStringUniques = new HashSet<>(); 061 public final Collection<ResourceIndexedComboTokenNonUnique> myComboTokenNonUnique = new HashSet<>(); 062 public final Collection<ResourceLink> myLinks = new HashSet<>(); 063 public final Set<String> myPopulatedResourceLinkParameters = new HashSet<>(); 064 public final Collection<SearchParamPresentEntity> mySearchParamPresentEntities = new HashSet<>(); 065 public final Collection<ResourceIndexedSearchParamComposite> myCompositeParams = new HashSet<>(); 066 067 private static final Set<String> myIgnoredParams = Set.of(Constants.PARAM_TEXT, Constants.PARAM_CONTENT); 068 069 public ResourceIndexedSearchParams() {} 070 071 public ResourceIndexedSearchParams(ResourceTable theEntity) { 072 if (theEntity.isParamsStringPopulated()) { 073 myStringParams.addAll(theEntity.getParamsString()); 074 } 075 if (theEntity.isParamsTokenPopulated()) { 076 myTokenParams.addAll(theEntity.getParamsToken()); 077 } 078 if (theEntity.isParamsNumberPopulated()) { 079 myNumberParams.addAll(theEntity.getParamsNumber()); 080 } 081 if (theEntity.isParamsQuantityPopulated()) { 082 myQuantityParams.addAll(theEntity.getParamsQuantity()); 083 } 084 if (theEntity.isParamsQuantityNormalizedPopulated()) { 085 myQuantityNormalizedParams.addAll(theEntity.getParamsQuantityNormalized()); 086 } 087 if (theEntity.isParamsDatePopulated()) { 088 myDateParams.addAll(theEntity.getParamsDate()); 089 } 090 if (theEntity.isParamsUriPopulated()) { 091 myUriParams.addAll(theEntity.getParamsUri()); 092 } 093 if (theEntity.isParamsCoordsPopulated()) { 094 myCoordsParams.addAll(theEntity.getParamsCoords()); 095 } 096 if (theEntity.isHasLinks()) { 097 myLinks.addAll(theEntity.getResourceLinks()); 098 } 099 100 if (theEntity.isParamsComboStringUniquePresent()) { 101 myComboStringUniques.addAll(theEntity.getParamsComboStringUnique()); 102 } 103 if (theEntity.isParamsComboTokensNonUniquePresent()) { 104 myComboTokenNonUnique.addAll(theEntity.getmyParamsComboTokensNonUnique()); 105 } 106 } 107 108 public Collection<ResourceLink> getResourceLinks() { 109 return myLinks; 110 } 111 112 public void populateResourceTableSearchParamsPresentFlags(ResourceTable theEntity) { 113 theEntity.setParamsStringPopulated(myStringParams.isEmpty() == false); 114 theEntity.setParamsTokenPopulated(myTokenParams.isEmpty() == false); 115 theEntity.setParamsNumberPopulated(myNumberParams.isEmpty() == false); 116 theEntity.setParamsQuantityPopulated(myQuantityParams.isEmpty() == false); 117 theEntity.setParamsQuantityNormalizedPopulated(myQuantityNormalizedParams.isEmpty() == false); 118 theEntity.setParamsDatePopulated(myDateParams.isEmpty() == false); 119 theEntity.setParamsUriPopulated(myUriParams.isEmpty() == false); 120 theEntity.setParamsCoordsPopulated(myCoordsParams.isEmpty() == false); 121 theEntity.setParamsComboStringUniquePresent(myComboStringUniques.isEmpty() == false); 122 theEntity.setParamsComboTokensNonUniquePresent(myComboTokenNonUnique.isEmpty() == false); 123 theEntity.setHasLinks(myLinks.isEmpty() == false); 124 } 125 126 public void populateResourceTableParamCollections(ResourceTable theEntity) { 127 theEntity.setParamsString(myStringParams); 128 theEntity.setParamsToken(myTokenParams); 129 theEntity.setParamsNumber(myNumberParams); 130 theEntity.setParamsQuantity(myQuantityParams); 131 theEntity.setParamsQuantityNormalized(myQuantityNormalizedParams); 132 theEntity.setParamsDate(myDateParams); 133 theEntity.setParamsUri(myUriParams); 134 theEntity.setParamsCoords(myCoordsParams); 135 theEntity.setResourceLinks(myLinks); 136 } 137 138 public void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, String theSpnamePrefix) { 139 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myNumberParams, theSpnamePrefix); 140 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityParams, theSpnamePrefix); 141 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityNormalizedParams, theSpnamePrefix); 142 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myDateParams, theSpnamePrefix); 143 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myUriParams, theSpnamePrefix); 144 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myTokenParams, theSpnamePrefix); 145 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myStringParams, theSpnamePrefix); 146 updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myCoordsParams, theSpnamePrefix); 147 } 148 149 public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) { 150 for (ResourceLink param : myLinks) { 151 // The resource link already has the resource type of the contained resource at the head of the path. 152 // We need to replace this with the name of the containing type, and extend the search path. 153 int index = param.getSourcePath().indexOf('.'); 154 if (index > -1) { 155 param.setSourcePath(theSpNamePrefix + param.getSourcePath().substring(index)); 156 } else { 157 // Can this ever happen? 158 param.setSourcePath(theSpNamePrefix + "." + param.getSourcePath()); 159 } 160 param.calculateHashes(); // re-calculateHashes 161 } 162 } 163 164 void setUpdatedTime(Date theUpdateTime) { 165 setUpdatedTime(myStringParams, theUpdateTime); 166 setUpdatedTime(myNumberParams, theUpdateTime); 167 setUpdatedTime(myQuantityParams, theUpdateTime); 168 setUpdatedTime(myQuantityNormalizedParams, theUpdateTime); 169 setUpdatedTime(myDateParams, theUpdateTime); 170 setUpdatedTime(myUriParams, theUpdateTime); 171 setUpdatedTime(myCoordsParams, theUpdateTime); 172 setUpdatedTime(myTokenParams, theUpdateTime); 173 } 174 175 private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) { 176 for (BaseResourceIndexedSearchParam nextSearchParam : theParams) { 177 nextSearchParam.setUpdated(theUpdateTime); 178 } 179 } 180 181 private void updateSpnamePrefixForIndexOnUpliftedChain( 182 String theContainingType, 183 Collection<? extends BaseResourceIndexedSearchParam> theParams, 184 @Nonnull String theSpnamePrefix) { 185 186 for (BaseResourceIndexedSearchParam param : theParams) { 187 param.setResourceType(theContainingType); 188 param.setParamName(theSpnamePrefix + "." + param.getParamName()); 189 190 // re-calculate hashes 191 param.calculateHashes(); 192 } 193 } 194 195 public Set<String> getPopulatedResourceLinkParameters() { 196 return myPopulatedResourceLinkParameters; 197 } 198 199 public boolean matchParam( 200 StorageSettings theStorageSettings, 201 String theResourceName, 202 String theParamName, 203 RuntimeSearchParam theParamDef, 204 IQueryParameterType theValue) { 205 206 if (theParamDef == null) { 207 return false; 208 } 209 Collection<? extends BaseResourceIndexedSearchParam> resourceParams = null; 210 IQueryParameterType value = theValue; 211 switch (theParamDef.getParamType()) { 212 case TOKEN: 213 resourceParams = myTokenParams; 214 break; 215 case QUANTITY: 216 if (theStorageSettings 217 .getNormalizedQuantitySearchLevel() 218 .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) { 219 QuantityParam quantity = QuantityParam.toQuantityParam(theValue); 220 QuantityParam normalized = UcumServiceUtil.toCanonicalQuantityOrNull(quantity); 221 if (normalized != null) { 222 resourceParams = myQuantityNormalizedParams; 223 value = normalized; 224 } 225 } 226 227 if (resourceParams == null) { 228 resourceParams = myQuantityParams; 229 } 230 break; 231 case STRING: 232 resourceParams = myStringParams; 233 break; 234 case NUMBER: 235 resourceParams = myNumberParams; 236 break; 237 case URI: 238 resourceParams = myUriParams; 239 break; 240 case DATE: 241 resourceParams = myDateParams; 242 break; 243 case REFERENCE: 244 return matchResourceLinks( 245 theStorageSettings, 246 theResourceName, 247 theParamName, 248 value, 249 theParamDef.getPathsSplitForResourceType(theResourceName)); 250 case COMPOSITE: 251 case HAS: 252 case SPECIAL: 253 default: 254 resourceParams = null; 255 } 256 if (resourceParams == null) { 257 return false; 258 } 259 260 for (BaseResourceIndexedSearchParam nextParam : resourceParams) { 261 if (nextParam.getParamName().equalsIgnoreCase(theParamName)) { 262 if (nextParam.matches(value)) { 263 return true; 264 } 265 } 266 } 267 268 return false; 269 } 270 271 /** 272 * @deprecated Replace with the method below 273 */ 274 // KHS This needs to be public as libraries outside of hapi call it directly 275 @Deprecated 276 public boolean matchResourceLinks( 277 String theResourceName, String theParamName, IQueryParameterType theParam, String theParamPath) { 278 return matchResourceLinks(new StorageSettings(), theResourceName, theParamName, theParam, theParamPath); 279 } 280 281 public boolean matchResourceLinks( 282 StorageSettings theStorageSettings, 283 String theResourceName, 284 String theParamName, 285 IQueryParameterType theParam, 286 List<String> theParamPaths) { 287 for (String nextPath : theParamPaths) { 288 if (matchResourceLinks(theStorageSettings, theResourceName, theParamName, theParam, nextPath)) { 289 return true; 290 } 291 } 292 return false; 293 } 294 295 // KHS This needs to be public as libraries outside of hapi call it directly 296 public boolean matchResourceLinks( 297 StorageSettings theStorageSettings, 298 String theResourceName, 299 String theParamName, 300 IQueryParameterType theParam, 301 String theParamPath) { 302 ReferenceParam reference = (ReferenceParam) theParam; 303 304 Predicate<ResourceLink> namedParamPredicate = 305 resourceLink -> searchParameterPathMatches(theResourceName, resourceLink, theParamName, theParamPath) 306 && resourceIdMatches(theStorageSettings, resourceLink, reference); 307 308 return myLinks.stream().anyMatch(namedParamPredicate); 309 } 310 311 private boolean resourceIdMatches( 312 StorageSettings theStorageSettings, ResourceLink theResourceLink, ReferenceParam theReference) { 313 String baseUrl = theReference.getBaseUrl(); 314 if (isNotBlank(baseUrl)) { 315 if (!theStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl)) { 316 return false; 317 } 318 } 319 320 String targetType = theResourceLink.getTargetResourceType(); 321 String targetId = theResourceLink.getTargetResourceId(); 322 323 assert isNotBlank(targetType); 324 assert isNotBlank(targetId); 325 326 if (theReference.hasResourceType()) { 327 if (!theReference.getResourceType().equals(targetType)) { 328 return false; 329 } 330 } 331 332 if (!targetId.equals(theReference.getIdPart())) { 333 return false; 334 } 335 336 return true; 337 } 338 339 private boolean searchParameterPathMatches( 340 String theResourceName, ResourceLink theResourceLink, String theParamName, String theParamPath) { 341 String sourcePath = theResourceLink.getSourcePath(); 342 return sourcePath.equalsIgnoreCase(theParamPath); 343 } 344 345 @Override 346 public String toString() { 347 return "ResourceIndexedSearchParams{" + "stringParams=" 348 + myStringParams + ", tokenParams=" 349 + myTokenParams + ", numberParams=" 350 + myNumberParams + ", quantityParams=" 351 + myQuantityParams + ", quantityNormalizedParams=" 352 + myQuantityNormalizedParams + ", dateParams=" 353 + myDateParams + ", uriParams=" 354 + myUriParams + ", coordsParams=" 355 + myCoordsParams + ", comboStringUniques=" 356 + myComboStringUniques + ", comboTokenNonUniques=" 357 + myComboTokenNonUnique + ", links=" 358 + myLinks + '}'; 359 } 360 361 public void findMissingSearchParams( 362 PartitionSettings thePartitionSettings, 363 StorageSettings theStorageSettings, 364 ResourceTable theEntity, 365 ResourceSearchParams theActiveSearchParams) { 366 findMissingSearchParams( 367 thePartitionSettings, 368 theStorageSettings, 369 theEntity, 370 theActiveSearchParams, 371 RestSearchParameterTypeEnum.STRING, 372 myStringParams); 373 findMissingSearchParams( 374 thePartitionSettings, 375 theStorageSettings, 376 theEntity, 377 theActiveSearchParams, 378 RestSearchParameterTypeEnum.NUMBER, 379 myNumberParams); 380 findMissingSearchParams( 381 thePartitionSettings, 382 theStorageSettings, 383 theEntity, 384 theActiveSearchParams, 385 RestSearchParameterTypeEnum.QUANTITY, 386 myQuantityParams); 387 findMissingSearchParams( 388 thePartitionSettings, 389 theStorageSettings, 390 theEntity, 391 theActiveSearchParams, 392 RestSearchParameterTypeEnum.DATE, 393 myDateParams); 394 findMissingSearchParams( 395 thePartitionSettings, 396 theStorageSettings, 397 theEntity, 398 theActiveSearchParams, 399 RestSearchParameterTypeEnum.URI, 400 myUriParams); 401 findMissingSearchParams( 402 thePartitionSettings, 403 theStorageSettings, 404 theEntity, 405 theActiveSearchParams, 406 RestSearchParameterTypeEnum.TOKEN, 407 myTokenParams); 408 findMissingSearchParams( 409 thePartitionSettings, 410 theStorageSettings, 411 theEntity, 412 theActiveSearchParams, 413 RestSearchParameterTypeEnum.SPECIAL, 414 myCoordsParams); 415 } 416 417 @SuppressWarnings("unchecked") 418 private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams( 419 PartitionSettings thePartitionSettings, 420 StorageSettings theStorageSettings, 421 ResourceTable theEntity, 422 ResourceSearchParams activeSearchParams, 423 RestSearchParameterTypeEnum type, 424 Collection<RT> paramCollection) { 425 for (String nextParamName : activeSearchParams.getSearchParamNames()) { 426 if (nextParamName == null || myIgnoredParams.contains(nextParamName)) { 427 continue; 428 } 429 430 RuntimeSearchParam searchParam = activeSearchParams.get(nextParamName); 431 if (RuntimeSearchParamHelper.isResourceLevel(searchParam)) { 432 continue; 433 } 434 435 if (searchParam.getParamType() == type) { 436 boolean haveParam = false; 437 for (BaseResourceIndexedSearchParam nextParam : paramCollection) { 438 if (nextParam.getParamName().equals(nextParamName)) { 439 haveParam = true; 440 break; 441 } 442 } 443 444 if (!haveParam) { 445 BaseResourceIndexedSearchParam param; 446 switch (type) { 447 case DATE: 448 param = new ResourceIndexedSearchParamDate(); 449 break; 450 case NUMBER: 451 param = new ResourceIndexedSearchParamNumber(); 452 break; 453 case QUANTITY: 454 param = new ResourceIndexedSearchParamQuantity(); 455 break; 456 case STRING: 457 param = new ResourceIndexedSearchParamString().setStorageSettings(theStorageSettings); 458 break; 459 case TOKEN: 460 param = new ResourceIndexedSearchParamToken(); 461 break; 462 case URI: 463 param = new ResourceIndexedSearchParamUri(); 464 break; 465 case SPECIAL: 466 if (BaseSearchParamExtractor.COORDS_INDEX_PATHS.contains(searchParam.getPath())) { 467 param = new ResourceIndexedSearchParamCoords(); 468 break; 469 } else { 470 continue; 471 } 472 case COMPOSITE: 473 case HAS: 474 case REFERENCE: 475 default: 476 continue; 477 } 478 param.setPartitionSettings(thePartitionSettings); 479 param.setResource(theEntity); 480 param.setMissing(true); 481 param.setParamName(nextParamName); 482 param.calculateHashes(); 483 paramCollection.add((RT) param); 484 } 485 } 486 } 487 } 488 489 /** 490 * This method is used to create a set of all possible combinations of 491 * parameters across a set of search parameters. An example of why 492 * this is needed: 493 * <p> 494 * Let's say we have a unique index on (Patient:gender AND Patient:name). 495 * Then we pass in <code>SMITH, John</code> with a gender of <code>male</code>. 496 * </p> 497 * <p> 498 * In this case, because the name parameter matches both first and last name, 499 * we now need two unique indexes: 500 * <ul> 501 * <li>Patient?gender=male&name=SMITH</li> 502 * <li>Patient?gender=male&name=JOHN</li> 503 * </ul> 504 * </p> 505 * <p> 506 * So this recursive algorithm calculates those 507 * </p> 508 * 509 * @param theResourceType E.g. <code>Patient 510 * @param thePartsChoices E.g. <code>[[gender=male], [name=SMITH, name=JOHN]]</code> 511 */ 512 public static Set<String> extractCompositeStringUniquesValueChains( 513 String theResourceType, List<List<String>> thePartsChoices) { 514 515 for (List<String> next : thePartsChoices) { 516 next.removeIf(StringUtils::isBlank); 517 if (next.isEmpty()) { 518 return Collections.emptySet(); 519 } 520 } 521 522 if (thePartsChoices.isEmpty()) { 523 return Collections.emptySet(); 524 } 525 526 thePartsChoices.sort((o1, o2) -> { 527 String str1 = null; 528 String str2 = null; 529 if (o1.size() > 0) { 530 str1 = o1.get(0); 531 } 532 if (o2.size() > 0) { 533 str2 = o2.get(0); 534 } 535 return compare(str1, str2); 536 }); 537 538 List<String> values = new ArrayList<>(); 539 Set<String> queryStringsToPopulate = new HashSet<>(); 540 extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate); 541 542 values.removeIf(StringUtils::isBlank); 543 544 return queryStringsToPopulate; 545 } 546 547 private static void extractCompositeStringUniquesValueChains( 548 String theResourceType, 549 List<List<String>> thePartsChoices, 550 List<String> theValues, 551 Set<String> theQueryStringsToPopulate) { 552 if (thePartsChoices.size() > 0) { 553 List<String> nextList = thePartsChoices.get(0); 554 Collections.sort(nextList); 555 for (String nextChoice : nextList) { 556 theValues.add(nextChoice); 557 extractCompositeStringUniquesValueChains( 558 theResourceType, 559 thePartsChoices.subList(1, thePartsChoices.size()), 560 theValues, 561 theQueryStringsToPopulate); 562 theValues.remove(theValues.size() - 1); 563 } 564 } else { 565 if (theValues.size() > 0) { 566 StringBuilder uniqueString = new StringBuilder(); 567 uniqueString.append(theResourceType); 568 569 for (int i = 0; i < theValues.size(); i++) { 570 uniqueString.append(i == 0 ? "?" : "&"); 571 uniqueString.append(theValues.get(i)); 572 } 573 574 theQueryStringsToPopulate.add(uniqueString.toString()); 575 } 576 } 577 } 578}