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&amp;name=SMITH</li>
538         * <li>Patient?gender=male&amp;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}