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