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