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;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.model.api.IQueryParameterAnd;
024import ca.uhn.fhir.model.api.IQueryParameterOr;
025import ca.uhn.fhir.model.api.IQueryParameterType;
026import ca.uhn.fhir.model.api.Include;
027import ca.uhn.fhir.rest.api.Constants;
028import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
029import ca.uhn.fhir.rest.api.SearchIncludeDeletedEnum;
030import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
031import ca.uhn.fhir.rest.api.SortOrderEnum;
032import ca.uhn.fhir.rest.api.SortSpec;
033import ca.uhn.fhir.rest.api.SummaryEnum;
034import ca.uhn.fhir.rest.param.DateParam;
035import ca.uhn.fhir.rest.param.DateRangeParam;
036import ca.uhn.fhir.rest.param.ParamPrefixEnum;
037import ca.uhn.fhir.rest.param.QuantityParam;
038import ca.uhn.fhir.rest.param.TokenParamModifier;
039import ca.uhn.fhir.util.UrlUtil;
040import com.fasterxml.jackson.annotation.JsonIgnore;
041import jakarta.annotation.Nonnull;
042import org.apache.commons.lang3.Strings;
043import org.apache.commons.lang3.Validate;
044import org.apache.commons.lang3.builder.CompareToBuilder;
045import org.apache.commons.lang3.builder.ToStringBuilder;
046import org.apache.commons.lang3.builder.ToStringStyle;
047
048import java.io.Serial;
049import java.io.Serializable;
050import java.util.ArrayList;
051import java.util.Collection;
052import java.util.Collections;
053import java.util.Comparator;
054import java.util.HashMap;
055import java.util.HashSet;
056import java.util.LinkedHashMap;
057import java.util.List;
058import java.util.Map;
059import java.util.Objects;
060import java.util.Set;
061
062import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
063import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
064import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL;
065import static org.apache.commons.lang3.StringUtils.defaultString;
066import static org.apache.commons.lang3.StringUtils.isBlank;
067import static org.apache.commons.lang3.StringUtils.isNotBlank;
068
069public class SearchParameterMap implements Serializable {
070        public static final Integer INTEGER_0 = 0;
071        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class);
072
073        @Serial
074        private static final long serialVersionUID = 1L;
075
076        private final HashMap<String, List<List<IQueryParameterType>>> mySearchParameterMap = new LinkedHashMap<>();
077        private Integer myCount;
078        private Integer myOffset;
079        private EverythingModeEnum myEverythingMode = null;
080        private Set<Include> myIncludes;
081        private DateRangeParam myLastUpdated;
082        private boolean myLoadSynchronous;
083        private Integer myLoadSynchronousUpTo;
084        private Set<Include> myRevIncludes;
085        private SortSpec mySort;
086        private SummaryEnum mySummaryMode;
087        private SearchTotalModeEnum mySearchTotalMode;
088        private QuantityParam myNearDistanceParam;
089        private boolean myLastN;
090        private Integer myLastNMax;
091        private boolean myDeleteExpunge;
092        private SearchContainedModeEnum mySearchContainedMode = SearchContainedModeEnum.FALSE;
093        private SearchIncludeDeletedEnum mySearchIncludeDeletedMode;
094
095        /**
096         * Constructor
097         */
098        public SearchParameterMap() {
099                super();
100        }
101
102        /**
103         * Constructor
104         */
105        public SearchParameterMap(String theName, IQueryParameterType theParam) {
106                add(theName, theParam);
107        }
108
109        /**
110         * Creates and returns a copy of this map
111         */
112        @SuppressWarnings("MethodDoesntCallSuperMethod")
113        @JsonIgnore
114        @Override
115        public SearchParameterMap clone() {
116                SearchParameterMap map = new SearchParameterMap();
117                map.setSummaryMode(getSummaryMode());
118                map.setSort(getSort());
119                map.setSearchTotalMode(getSearchTotalMode());
120                map.setRevIncludes(getRevIncludes());
121                map.setIncludes(getIncludes());
122                map.setEverythingMode(getEverythingMode());
123                map.setCount(getCount());
124                map.setDeleteExpunge(isDeleteExpunge());
125                map.setLastN(isLastN());
126                map.setLastNMax(getLastNMax());
127                map.setLastUpdated(getLastUpdated());
128                map.setLoadSynchronous(isLoadSynchronous());
129                map.setNearDistanceParam(getNearDistanceParam());
130                map.setLoadSynchronousUpTo(getLoadSynchronousUpTo());
131                map.setOffset(getOffset());
132                map.setSearchContainedMode(getSearchContainedMode());
133                map.setSearchIncludeDeletedMode(getSearchIncludeDeletedMode());
134
135                for (Map.Entry<String, List<List<IQueryParameterType>>> entry : mySearchParameterMap.entrySet()) {
136                        List<List<IQueryParameterType>> andParams = entry.getValue();
137                        List<List<IQueryParameterType>> newAndParams = new ArrayList<>();
138                        for (List<IQueryParameterType> orParams : andParams) {
139                                List<IQueryParameterType> newOrParams = new ArrayList<>(orParams);
140                                newAndParams.add(newOrParams);
141                        }
142                        map.put(entry.getKey(), newAndParams);
143                }
144
145                return map;
146        }
147
148        public SummaryEnum getSummaryMode() {
149                return mySummaryMode;
150        }
151
152        public void setSummaryMode(SummaryEnum theSummaryMode) {
153                mySummaryMode = theSummaryMode;
154        }
155
156        public SearchTotalModeEnum getSearchTotalMode() {
157                return mySearchTotalMode;
158        }
159
160        public void setSearchTotalMode(SearchTotalModeEnum theSearchTotalMode) {
161                mySearchTotalMode = theSearchTotalMode;
162        }
163
164        public SearchParameterMap add(String theName, DateParam theDateParam) {
165                add(theName, (IQueryParameterOr<?>) theDateParam);
166                return this;
167        }
168
169        @SuppressWarnings("unchecked")
170        public SearchParameterMap add(String theName, IQueryParameterAnd<?> theAnd) {
171                if (theAnd == null) {
172                        return this;
173                }
174                if (!containsKey(theName)) {
175                        put(theName, new ArrayList<>());
176                }
177
178                List<List<IQueryParameterType>> paramList = get(theName);
179                for (IQueryParameterOr<?> next : theAnd.getValuesAsQueryTokens()) {
180                        if (next == null) {
181                                continue;
182                        }
183                        paramList.add((List<IQueryParameterType>) next.getValuesAsQueryTokens());
184                }
185
186                return this;
187        }
188
189        @SuppressWarnings("unchecked")
190        public SearchParameterMap add(String theName, IQueryParameterOr<?> theOr) {
191                if (theOr == null) {
192                        return this;
193                }
194                if (!containsKey(theName)) {
195                        put(theName, new ArrayList<>());
196                }
197
198                get(theName).add((List<IQueryParameterType>) theOr.getValuesAsQueryTokens());
199                return this;
200        }
201
202        public Collection<List<List<IQueryParameterType>>> values() {
203                return mySearchParameterMap.values();
204        }
205
206        public SearchParameterMap add(String theName, IQueryParameterType theParam) {
207                assert !Constants.PARAM_LASTUPDATED.equals(theName); // this has it's own field in the map
208
209                if (theParam == null) {
210                        return this;
211                }
212                if (!containsKey(theName)) {
213                        put(theName, new ArrayList<>());
214                }
215                ArrayList<IQueryParameterType> list = new ArrayList<>();
216                list.add(theParam);
217                get(theName).add(list);
218
219                return this;
220        }
221
222        public SearchParameterMap addInclude(Include theInclude) {
223                getIncludes().add(theInclude);
224                return this;
225        }
226
227        private void addLastUpdateParam(StringBuilder theBuilder, ParamPrefixEnum thePrefix, DateParam theDateParam) {
228                if (theDateParam != null && isNotBlank(theDateParam.getValueAsString())) {
229                        addUrlParamSeparator(theBuilder);
230                        theBuilder.append(Constants.PARAM_LASTUPDATED);
231                        theBuilder.append('=');
232                        theBuilder.append(thePrefix.getValue());
233                        theBuilder.append(theDateParam.getValueAsString());
234                }
235        }
236
237        public SearchParameterMap addRevInclude(Include theInclude) {
238                getRevIncludes().add(theInclude);
239                return this;
240        }
241
242        private void addUrlIncludeParams(StringBuilder b, String paramName, Set<Include> theList) {
243                ArrayList<Include> list = new ArrayList<>(theList);
244
245                list.sort(new IncludeComparator());
246                for (Include nextInclude : list) {
247                        addUrlParamSeparator(b);
248                        b.append(paramName);
249                        if (nextInclude.isRecurse()) {
250                                b.append(Constants.PARAM_INCLUDE_QUALIFIER_RECURSE);
251                        }
252                        b.append('=');
253                        if (Constants.INCLUDE_STAR.equals(nextInclude.getValue())) {
254                                b.append(Constants.INCLUDE_STAR);
255                        } else {
256                                b.append(UrlUtil.escapeUrlParam(nextInclude.getParamType()));
257                                b.append(':');
258                                b.append(UrlUtil.escapeUrlParam(nextInclude.getParamName()));
259                                if (isNotBlank(nextInclude.getParamTargetType())) {
260                                        b.append(':');
261                                        b.append(nextInclude.getParamTargetType());
262                                }
263                        }
264                }
265        }
266
267        private void addUrlParamSeparator(StringBuilder theB) {
268                if (theB.isEmpty()) {
269                        theB.append('?');
270                } else {
271                        theB.append('&');
272                }
273        }
274
275        public Integer getCount() {
276                return myCount;
277        }
278
279        public SearchParameterMap setCount(Integer theCount) {
280                myCount = theCount;
281                return this;
282        }
283
284        public Integer getOffset() {
285                return myOffset;
286        }
287
288        public void setOffset(Integer theOffset) {
289                myOffset = theOffset;
290        }
291
292        public EverythingModeEnum getEverythingMode() {
293                return myEverythingMode;
294        }
295
296        public void setEverythingMode(EverythingModeEnum theConsolidateMatches) {
297                myEverythingMode = theConsolidateMatches;
298        }
299
300        public Set<Include> getIncludes() {
301                if (myIncludes == null) {
302                        myIncludes = new HashSet<>();
303                }
304                return myIncludes;
305        }
306
307        public void setIncludes(Set<Include> theIncludes) {
308                myIncludes = theIncludes;
309        }
310
311        /**
312         * Returns null if there is no last updated value
313         */
314        public DateRangeParam getLastUpdated() {
315                if (myLastUpdated != null) {
316                        if (myLastUpdated.isEmpty()) {
317                                myLastUpdated = null;
318                        }
319                }
320                return myLastUpdated;
321        }
322
323        public void setLastUpdated(DateRangeParam theLastUpdated) {
324                myLastUpdated = theLastUpdated;
325        }
326
327        /**
328         * If set, tells the server to load these results synchronously, and not to load
329         * more than X results
330         */
331        public Integer getLoadSynchronousUpTo() {
332                return myLoadSynchronousUpTo;
333        }
334
335        /**
336         * If set, tells the server to load these results synchronously, and not to load
337         * more than X results. Note that setting this to a value will also set
338         * {@link #setLoadSynchronous(boolean)} to true
339         */
340        public SearchParameterMap setLoadSynchronousUpTo(Integer theLoadSynchronousUpTo) {
341                myLoadSynchronousUpTo = theLoadSynchronousUpTo;
342                if (myLoadSynchronousUpTo != null) {
343                        setLoadSynchronous(true);
344                }
345                return this;
346        }
347
348        public Set<Include> getRevIncludes() {
349                if (myRevIncludes == null) {
350                        myRevIncludes = new HashSet<>();
351                }
352                return myRevIncludes;
353        }
354
355        public void setRevIncludes(Set<Include> theRevIncludes) {
356                myRevIncludes = theRevIncludes;
357        }
358
359        public SortSpec getSort() {
360                return mySort;
361        }
362
363        public SearchParameterMap setSort(SortSpec theSort) {
364                mySort = theSort;
365                return this;
366        }
367
368        /**
369         * If set, tells the server to load these results synchronously, and not to load
370         * more than X results
371         */
372        public boolean isLoadSynchronous() {
373                return myLoadSynchronous;
374        }
375
376        /**
377         * If set, tells the server to load these results synchronously, and not to load
378         * more than X results
379         */
380        public SearchParameterMap setLoadSynchronous(boolean theLoadSynchronous) {
381                myLoadSynchronous = theLoadSynchronous;
382                return this;
383        }
384
385        /**
386         * If set, tells the server to use an Elasticsearch query to generate a list of
387         * Resource IDs for the LastN operation
388         */
389        public boolean isLastN() {
390                return myLastN;
391        }
392
393        /**
394         * If set, tells the server to use an Elasticsearch query to generate a list of
395         * Resource IDs for the LastN operation
396         */
397        public SearchParameterMap setLastN(boolean theLastN) {
398                myLastN = theLastN;
399                return this;
400        }
401
402        /**
403         * If set, tells the server the maximum number of observations to return for each
404         * observation code in the result set of a lastn operation
405         */
406        public Integer getLastNMax() {
407                return myLastNMax;
408        }
409
410        /**
411         * If set, tells the server the maximum number of observations to return for each
412         * observation code in the result set of a lastn operation
413         */
414        public SearchParameterMap setLastNMax(Integer theLastNMax) {
415                myLastNMax = theLastNMax;
416                return this;
417        }
418
419        /**
420         * This method creates a URL query string representation of the parameters in this
421         * object, excluding the part before the parameters, e.g.
422         * <p>
423         * <code>?name=smith&amp;_sort=Patient:family</code>
424         * </p>
425         * <p>
426         * This method <b>excludes</b> the <code>_count</code> parameter,
427         * as it doesn't affect the substance of the results returned
428         * </p>
429         */
430        public String toNormalizedQueryString(FhirContext theCtx) {
431                StringBuilder b = new StringBuilder();
432
433                ArrayList<String> keys = new ArrayList<>(keySet());
434                Collections.sort(keys);
435                for (String nextKey : keys) {
436
437                        List<List<IQueryParameterType>> nextValuesAndsIn = get(nextKey);
438                        List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<>();
439
440                        for (List<? extends IQueryParameterType> nextValuesAndIn : nextValuesAndsIn) {
441
442                                List<IQueryParameterType> nextValuesOrsOut = new ArrayList<>(nextValuesAndIn);
443
444                                nextValuesOrsOut.sort(new QueryParameterTypeComparator(theCtx));
445
446                                if (!nextValuesOrsOut.isEmpty()) {
447                                        nextValuesAndsOut.add(nextValuesOrsOut);
448                                }
449                        } // for AND
450
451                        nextValuesAndsOut.sort(new QueryParameterOrComparator(theCtx));
452
453                        for (List<IQueryParameterType> nextValuesAnd : nextValuesAndsOut) {
454                                addUrlParamSeparator(b);
455                                IQueryParameterType firstValue = nextValuesAnd.get(0);
456                                b.append(UrlUtil.escapeUrlParam(nextKey));
457
458                                if (firstValue.getMissing() != null) {
459                                        b.append(Constants.PARAMQUALIFIER_MISSING);
460                                        b.append('=');
461                                        if (firstValue.getMissing()) {
462                                                b.append(Constants.PARAMQUALIFIER_MISSING_TRUE);
463                                        } else {
464                                                b.append(Constants.PARAMQUALIFIER_MISSING_FALSE);
465                                        }
466                                        continue;
467                                }
468
469                                if (isNotBlank(firstValue.getQueryParameterQualifier())) {
470                                        b.append(firstValue.getQueryParameterQualifier());
471                                }
472
473                                b.append('=');
474
475                                for (int i = 0; i < nextValuesAnd.size(); i++) {
476                                        IQueryParameterType nextValueOr = nextValuesAnd.get(i);
477                                        if (i > 0) {
478                                                b.append(',');
479                                        }
480                                        String valueAsQueryToken = nextValueOr.getValueAsQueryToken();
481                                        valueAsQueryToken = defaultString(valueAsQueryToken);
482                                        b.append(UrlUtil.escapeUrlParam(valueAsQueryToken, false));
483                                }
484                        }
485                } // for keys
486
487                SortSpec sort = getSort();
488                boolean first = true;
489                while (sort != null) {
490
491                        if (isNotBlank(sort.getParamName())) {
492                                if (first) {
493                                        addUrlParamSeparator(b);
494                                        b.append(Constants.PARAM_SORT);
495                                        b.append('=');
496                                        first = false;
497                                } else {
498                                        b.append(',');
499                                }
500                                if (sort.getOrder() == SortOrderEnum.DESC) {
501                                        b.append('-');
502                                }
503                                b.append(sort.getParamName());
504                        }
505
506                        Validate.isTrue(sort != sort.getChain()); // just in case, shouldn't happen
507                        sort = sort.getChain();
508                }
509
510                if (hasIncludes()) {
511                        addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes());
512                }
513                addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes());
514
515                if (getLastUpdated() != null) {
516                        DateParam lb = getLastUpdated().getLowerBound();
517                        DateParam ub = getLastUpdated().getUpperBound();
518
519                        if (isNotEqualsComparator(lb, ub)) {
520                                addLastUpdateParam(b, NOT_EQUAL, getLastUpdated().getLowerBound());
521                        } else {
522                                addLastUpdateParam(b, GREATERTHAN_OR_EQUALS, lb);
523                                addLastUpdateParam(b, LESSTHAN_OR_EQUALS, ub);
524                        }
525                }
526
527                if (getCount() != null) {
528                        addUrlParamSeparator(b);
529                        b.append(Constants.PARAM_COUNT);
530                        b.append('=');
531                        b.append(getCount());
532                }
533
534                if (getOffset() != null) {
535                        addUrlParamSeparator(b);
536                        b.append(Constants.PARAM_OFFSET);
537                        b.append('=');
538                        b.append(getOffset());
539                }
540
541                // Summary mode (_summary)
542                if (getSummaryMode() != null) {
543                        addUrlParamSeparator(b);
544                        b.append(Constants.PARAM_SUMMARY);
545                        b.append('=');
546                        b.append(getSummaryMode().getCode());
547                }
548
549                // Search count mode (_total)
550                if (getSearchTotalMode() != null) {
551                        addUrlParamSeparator(b);
552                        b.append(Constants.PARAM_SEARCH_TOTAL_MODE);
553                        b.append('=');
554                        b.append(getSearchTotalMode().getCode());
555                }
556
557                // Contained mode
558                // For some reason, instead of null here, we default to false. That said, ommitting it is identical to setting
559                // it to false.
560                if (getSearchContainedMode() != SearchContainedModeEnum.FALSE) {
561                        addUrlParamSeparator(b);
562                        b.append(Constants.PARAM_CONTAINED);
563                        b.append("=");
564                        b.append(getSearchContainedMode().getCode());
565                }
566
567                if (getSearchIncludeDeletedMode() != null) {
568                        addUrlParamSeparator(b);
569                        b.append(Constants.PARAM_INCLUDE_DELETED);
570                        b.append("=");
571                        b.append(getSearchIncludeDeletedMode().getCode());
572                }
573
574                if (b.isEmpty()) {
575                        b.append('?');
576                }
577
578                return b.toString();
579        }
580
581        private boolean isNotEqualsComparator(DateParam theLowerBound, DateParam theUpperBound) {
582                return theLowerBound != null
583                                && theUpperBound != null
584                                && NOT_EQUAL.equals(theLowerBound.getPrefix())
585                                && NOT_EQUAL.equals(theUpperBound.getPrefix());
586        }
587
588        /**
589         * @since 5.5.0
590         */
591        public boolean hasIncludes() {
592                return myIncludes != null && !myIncludes.isEmpty();
593        }
594
595        /**
596         * @since 6.2.0
597         */
598        public boolean hasRevIncludes() {
599                return myRevIncludes != null && !myRevIncludes.isEmpty();
600        }
601
602        @Override
603        public String toString() {
604                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
605                if (!isEmpty()) {
606                        b.append("params", mySearchParameterMap);
607                }
608                if (!getIncludes().isEmpty()) {
609                        b.append("includes", getIncludes());
610                }
611                return b.toString();
612        }
613
614        public void clean() {
615                for (Map.Entry<String, List<List<IQueryParameterType>>> nextParamEntry : this.entrySet()) {
616                        String nextParamName = nextParamEntry.getKey();
617                        List<List<IQueryParameterType>> andOrParams = nextParamEntry.getValue();
618                        cleanParameter(nextParamName, andOrParams);
619                }
620        }
621
622        /*
623         * Given a particular named parameter, e.g. `name`, iterate over AndOrParams and remove any which are empty.
624         */
625        private void cleanParameter(String theParamName, List<List<IQueryParameterType>> theAndOrParams) {
626                theAndOrParams.forEach(orList -> {
627                        List<IQueryParameterType> emptyParameters = orList.stream()
628                                        .filter(nextOr -> nextOr.getMissing() == null)
629                                        .filter(nextOr -> nextOr instanceof QuantityParam)
630                                        .filter(nextOr -> isBlank(((QuantityParam) nextOr).getValueAsString()))
631                                        .toList();
632
633                        ourLog.debug("Ignoring empty parameter: {}", theParamName);
634                        orList.removeAll(emptyParameters);
635                });
636                theAndOrParams.removeIf(List::isEmpty);
637        }
638
639        public QuantityParam getNearDistanceParam() {
640                return myNearDistanceParam;
641        }
642
643        public void setNearDistanceParam(QuantityParam theQuantityParam) {
644                myNearDistanceParam = theQuantityParam;
645        }
646
647        public boolean isWantOnlyCount() {
648                return SummaryEnum.COUNT.equals(getSummaryMode()) || INTEGER_0.equals(getCount());
649        }
650
651        public boolean isDeleteExpunge() {
652                return myDeleteExpunge;
653        }
654
655        public SearchParameterMap setDeleteExpunge(boolean theDeleteExpunge) {
656                myDeleteExpunge = theDeleteExpunge;
657                return this;
658        }
659
660        public List<List<IQueryParameterType>> get(String theName) {
661                return mySearchParameterMap.get(theName);
662        }
663
664        public void put(String theName, List<List<IQueryParameterType>> theParams) {
665                mySearchParameterMap.put(theName, theParams);
666        }
667
668        public boolean containsKey(String theName) {
669                return mySearchParameterMap.containsKey(theName);
670        }
671
672        public Set<String> keySet() {
673                return mySearchParameterMap.keySet();
674        }
675
676        public boolean isEmpty() {
677                return mySearchParameterMap.isEmpty();
678        }
679
680        // Wrapper methods
681
682        public Set<Map.Entry<String, List<List<IQueryParameterType>>>> entrySet() {
683                return mySearchParameterMap.entrySet();
684        }
685
686        public List<List<IQueryParameterType>> remove(String theName) {
687                return mySearchParameterMap.remove(theName);
688        }
689
690        /**
691         * Variant of removeByNameAndModifier for unmodified params.
692         *
693         * @param theName the query parameter key
694         * @return an And/Or List of Query Parameters matching the name with no modifier.
695         */
696        public List<List<IQueryParameterType>> removeByNameUnmodified(String theName) {
697                return this.removeByNameAndModifier(theName, "");
698        }
699
700        /**
701         * Given a search parameter name and modifier (e.g. :text),
702         * get and remove all Search Parameters matching this name and modifier
703         *
704         * @param theName     the query parameter key
705         * @param theModifier the qualifier you want to remove - nullable for unmodified params.
706         * @return an And/Or List of Query Parameters matching the qualifier.
707         */
708        public List<List<IQueryParameterType>> removeByNameAndModifier(String theName, String theModifier) {
709                theModifier = Objects.toString(theModifier, "");
710
711                List<List<IQueryParameterType>> remainderParameters = new ArrayList<>();
712                List<List<IQueryParameterType>> matchingParameters = new ArrayList<>();
713
714                // pull all of them out, partition by match against the qualifier
715                List<List<IQueryParameterType>> andList = mySearchParameterMap.remove(theName);
716                if (andList != null) {
717                        for (List<IQueryParameterType> orList : andList) {
718                                if (!orList.isEmpty()
719                                                && Objects.toString(orList.get(0).getQueryParameterQualifier(), "")
720                                                                .equals(theModifier)) {
721                                        matchingParameters.add(orList);
722                                } else {
723                                        remainderParameters.add(orList);
724                                }
725                        }
726                }
727
728                // put the unmatched back in.
729                if (!remainderParameters.isEmpty()) {
730                        mySearchParameterMap.put(theName, remainderParameters);
731                }
732                return matchingParameters;
733        }
734
735        public List<List<IQueryParameterType>> removeByNameAndModifier(
736                        String theName, @Nonnull TokenParamModifier theModifier) {
737                return removeByNameAndModifier(theName, theModifier.getValue());
738        }
739
740        /**
741         * For each search parameter in the map, extract any which have the given qualifier.
742         * e.g. Take the url: {@code Observation?code:text=abc&code=123&code:text=def&reason:text=somereason}
743         * <p>
744         * If we call this function with `:text`, it will return a map that looks like:
745         * <p>
746         * code -> [[code:text=abc], [code:text=def]]
747         * reason -> [[reason:text=somereason]]
748         * <p>
749         * and the remaining search parameters in the map will be:
750         * <p>
751         * code -> [[code=123]]
752         */
753        public Map<String, List<List<IQueryParameterType>>> removeByQualifier(String theQualifier) {
754
755                Map<String, List<List<IQueryParameterType>>> retVal = new HashMap<>();
756                Set<String> parameterNames = mySearchParameterMap.keySet();
757                for (String parameterName : parameterNames) {
758                        List<List<IQueryParameterType>> paramsWithQualifier = removeByNameAndModifier(parameterName, theQualifier);
759                        retVal.put(parameterName, paramsWithQualifier);
760                }
761
762                return retVal;
763        }
764
765        public Map<String, List<List<IQueryParameterType>>> removeByQualifier(@Nonnull TokenParamModifier theModifier) {
766                return removeByQualifier(theModifier.getValue());
767        }
768
769        public int size() {
770                return mySearchParameterMap.size();
771        }
772
773        public SearchContainedModeEnum getSearchContainedMode() {
774                return mySearchContainedMode;
775        }
776
777        public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) {
778                this.mySearchContainedMode = Objects.requireNonNullElse(theSearchContainedMode, SearchContainedModeEnum.FALSE);
779        }
780
781        public SearchIncludeDeletedEnum getSearchIncludeDeletedMode() {
782                return mySearchIncludeDeletedMode;
783        }
784
785        public void setSearchIncludeDeletedMode(SearchIncludeDeletedEnum theSearchIncludeDeletedMode) {
786                this.mySearchIncludeDeletedMode = theSearchIncludeDeletedMode;
787        }
788
789        /**
790         * Returns true if {@link #getOffset()} and {@link #getCount()} both return a non null response
791         *
792         * @since 5.5.0
793         */
794        public boolean isOffsetQuery() {
795                return getOffset() != null && getCount() != null;
796        }
797
798        public enum EverythingModeEnum {
799                /*
800                 * Don't reorder! We rely on the ordinals
801                 */
802                ENCOUNTER_INSTANCE(false, true, true),
803                ENCOUNTER_TYPE(false, true, false),
804                PATIENT_INSTANCE(true, false, true),
805                PATIENT_TYPE(true, false, false);
806
807                private final boolean myEncounter;
808
809                private final boolean myInstance;
810
811                private final boolean myPatient;
812
813                EverythingModeEnum(boolean thePatient, boolean theEncounter, boolean theInstance) {
814                        assert thePatient ^ theEncounter;
815                        myPatient = thePatient;
816                        myEncounter = theEncounter;
817                        myInstance = theInstance;
818                }
819
820                public boolean isEncounter() {
821                        return myEncounter;
822                }
823
824                public boolean isInstance() {
825                        return myInstance;
826                }
827
828                public boolean isPatient() {
829                        return myPatient;
830                }
831        }
832
833        static int compare(FhirContext theCtx, IQueryParameterType theO1, IQueryParameterType theO2) {
834                CompareToBuilder b = new CompareToBuilder();
835                b.append(theO1.getMissing(), theO2.getMissing());
836                b.append(theO1.getQueryParameterQualifier(), theO2.getQueryParameterQualifier());
837                if (b.toComparison() == 0) {
838                        b.append(theO1.getValueAsQueryToken(), theO2.getValueAsQueryToken());
839                }
840
841                return b.toComparison();
842        }
843
844        public static SearchParameterMap newSynchronous() {
845                SearchParameterMap retVal = new SearchParameterMap();
846                retVal.setLoadSynchronous(true);
847                return retVal;
848        }
849
850        public static SearchParameterMap newSynchronous(String theName, IQueryParameterType theParam) {
851                SearchParameterMap retVal = new SearchParameterMap();
852                retVal.setLoadSynchronous(true);
853                retVal.add(theName, theParam);
854                return retVal;
855        }
856
857        public static class IncludeComparator implements Comparator<Include> {
858
859                @Override
860                public int compare(Include theO1, Include theO2) {
861                        int retVal = Strings.CS.compare(theO1.getParamType(), theO2.getParamType());
862                        if (retVal == 0) {
863                                retVal = Strings.CS.compare(theO1.getParamName(), theO2.getParamName());
864                        }
865                        if (retVal == 0) {
866                                retVal = Strings.CS.compare(theO1.getParamTargetType(), theO2.getParamTargetType());
867                        }
868                        return retVal;
869                }
870        }
871
872        public static class QueryParameterOrComparator implements Comparator<List<IQueryParameterType>> {
873                private final FhirContext myCtx;
874
875                QueryParameterOrComparator(FhirContext theCtx) {
876                        myCtx = theCtx;
877                }
878
879                @Override
880                public int compare(List<IQueryParameterType> theO1, List<IQueryParameterType> theO2) {
881                        // These lists will never be empty
882                        return SearchParameterMap.compare(myCtx, theO1.get(0), theO2.get(0));
883                }
884        }
885
886        public static class QueryParameterTypeComparator implements Comparator<IQueryParameterType> {
887
888                private final FhirContext myCtx;
889
890                QueryParameterTypeComparator(FhirContext theCtx) {
891                        myCtx = theCtx;
892                }
893
894                @Override
895                public int compare(IQueryParameterType theO1, IQueryParameterType theO2) {
896                        return SearchParameterMap.compare(myCtx, theO1, theO2);
897                }
898        }
899
900        public List<SortSpec> getAllChainsInOrder() {
901                final List<SortSpec> allChainsInOrder = new ArrayList<>();
902                for (SortSpec sortSpec = getSort(); sortSpec != null; sortSpec = sortSpec.getChain()) {
903                        allChainsInOrder.add(sortSpec);
904                }
905
906                return Collections.unmodifiableList(allChainsInOrder);
907        }
908}