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