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