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