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