
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; 053 054import java.util.ArrayList; 055import java.util.Collection; 056import java.util.Collections; 057import java.util.Date; 058import java.util.HashSet; 059import java.util.List; 060import java.util.Set; 061import java.util.function.Predicate; 062 063import static org.apache.commons.lang3.StringUtils.compare; 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 thePartsChoices.sort((o1, o2) -> { 587 String str1 = null; 588 String str2 = null; 589 if (o1.size() > 0) { 590 str1 = o1.get(0); 591 } 592 if (o2.size() > 0) { 593 str2 = o2.get(0); 594 } 595 return compare(str1, str2); 596 }); 597 598 List<String> values = new ArrayList<>(); 599 Set<String> queryStringsToPopulate = new HashSet<>(); 600 extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate); 601 602 values.removeIf(StringUtils::isBlank); 603 604 return queryStringsToPopulate; 605 } 606 607 private static void extractCompositeStringUniquesValueChains( 608 String theResourceType, 609 List<List<String>> thePartsChoices, 610 List<String> theValues, 611 Set<String> theQueryStringsToPopulate) { 612 if (thePartsChoices.size() > 0) { 613 List<String> nextList = thePartsChoices.get(0); 614 Collections.sort(nextList); 615 for (String nextChoice : nextList) { 616 theValues.add(nextChoice); 617 extractCompositeStringUniquesValueChains( 618 theResourceType, 619 thePartsChoices.subList(1, thePartsChoices.size()), 620 theValues, 621 theQueryStringsToPopulate); 622 theValues.remove(theValues.size() - 1); 623 } 624 } else { 625 if (theValues.size() > 0) { 626 StringBuilder uniqueString = new StringBuilder(); 627 uniqueString.append(theResourceType); 628 629 for (int i = 0; i < theValues.size(); i++) { 630 uniqueString.append(i == 0 ? "?" : "&"); 631 uniqueString.append(theValues.get(i)); 632 } 633 634 theQueryStringsToPopulate.add(uniqueString.toString()); 635 } 636 } 637 } 638 639 /** 640 * Create a new instance that uses Sets as the internal collection 641 * type in order to defend against duplicates. This should be used 642 * when calculating the set of indexes for a resource that is 643 * about to be stored. 644 */ 645 public static ResourceIndexedSearchParams withSets() { 646 return new ResourceIndexedSearchParams(Mode.SET); 647 } 648 649 /** 650 * Create an empty and immutable structure. 651 */ 652 public static ResourceIndexedSearchParams empty() { 653 return new ResourceIndexedSearchParams(Mode.EMPTY); 654 } 655 656 /** 657 * Create a new instance that holds all the existing indexes 658 * in lists so that any duplicates are preserved. 659 */ 660 public static ResourceIndexedSearchParams withLists(ResourceTable theResourceTable) { 661 return new ResourceIndexedSearchParams(theResourceTable, Mode.LIST); 662 } 663 664 private enum Mode { 665 LIST { 666 @Override 667 public <T> Collection<T> newCollection() { 668 return new ArrayList<>(); 669 } 670 }, 671 SET { 672 @Override 673 public <T> Collection<T> newCollection() { 674 return new HashSet<>(); 675 } 676 }, 677 EMPTY { 678 @Override 679 public <T> Collection<T> newCollection() { 680 return List.of(); 681 } 682 }; 683 684 public abstract <T> Collection<T> newCollection(); 685 } 686}