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