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         * If set, tells the server to load these results synchronously, and not to load
358         * more than X results
359         */
360        public boolean isLoadSynchronous() {
361                return myLoadSynchronous;
362        }
363
364        /**
365         * If set, tells the server to load these results synchronously, and not to load
366         * more than X results
367         */
368        public SearchParameterMap setLoadSynchronous(boolean theLoadSynchronous) {
369                myLoadSynchronous = theLoadSynchronous;
370                return this;
371        }
372
373        /**
374         * If set, tells the server to use an Elasticsearch query to generate a list of
375         * Resource IDs for the LastN operation
376         */
377        public boolean isLastN() {
378                return myLastN;
379        }
380
381        /**
382         * If set, tells the server to use an Elasticsearch query to generate a list of
383         * Resource IDs for the LastN operation
384         */
385        public SearchParameterMap setLastN(boolean theLastN) {
386                myLastN = theLastN;
387                return this;
388        }
389
390        /**
391         * If set, tells the server the maximum number of observations to return for each
392         * observation code in the result set of a lastn operation
393         */
394        public Integer getLastNMax() {
395                return myLastNMax;
396        }
397
398        /**
399         * If set, tells the server the maximum number of observations to return for each
400         * observation code in the result set of a lastn operation
401         */
402        public SearchParameterMap setLastNMax(Integer theLastNMax) {
403                myLastNMax = theLastNMax;
404                return this;
405        }
406
407        /**
408         * This method creates a URL query string representation of the parameters in this
409         * object, excluding the part before the parameters, e.g.
410         * <p>
411         * <code>?name=smith&amp;_sort=Patient:family</code>
412         * </p>
413         * <p>
414         * This method <b>excludes</b> the <code>_count</code> parameter,
415         * as it doesn't affect the substance of the results returned
416         * </p>
417         */
418        public String toNormalizedQueryString(FhirContext theCtx) {
419                StringBuilder b = new StringBuilder();
420
421                ArrayList<String> keys = new ArrayList<>(keySet());
422                Collections.sort(keys);
423                for (String nextKey : keys) {
424
425                        List<List<IQueryParameterType>> nextValuesAndsIn = get(nextKey);
426                        List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<>();
427
428                        for (List<? extends IQueryParameterType> nextValuesAndIn : nextValuesAndsIn) {
429
430                                List<IQueryParameterType> nextValuesOrsOut = new ArrayList<>();
431
432                                nextValuesOrsOut.addAll(nextValuesAndIn);
433
434                                nextValuesOrsOut.sort(new QueryParameterTypeComparator(theCtx));
435
436                                if (nextValuesOrsOut.size() > 0) {
437                                        nextValuesAndsOut.add(nextValuesOrsOut);
438                                }
439                        } // for AND
440
441                        nextValuesAndsOut.sort(new QueryParameterOrComparator(theCtx));
442
443                        for (List<IQueryParameterType> nextValuesAnd : nextValuesAndsOut) {
444                                addUrlParamSeparator(b);
445                                IQueryParameterType firstValue = nextValuesAnd.get(0);
446                                b.append(UrlUtil.escapeUrlParam(nextKey));
447
448                                if (firstValue.getMissing() != null) {
449                                        b.append(Constants.PARAMQUALIFIER_MISSING);
450                                        b.append('=');
451                                        if (firstValue.getMissing()) {
452                                                b.append(Constants.PARAMQUALIFIER_MISSING_TRUE);
453                                        } else {
454                                                b.append(Constants.PARAMQUALIFIER_MISSING_FALSE);
455                                        }
456                                        continue;
457                                }
458
459                                if (isNotBlank(firstValue.getQueryParameterQualifier())) {
460                                        b.append(firstValue.getQueryParameterQualifier());
461                                }
462
463                                b.append('=');
464
465                                for (int i = 0; i < nextValuesAnd.size(); i++) {
466                                        IQueryParameterType nextValueOr = nextValuesAnd.get(i);
467                                        if (i > 0) {
468                                                b.append(',');
469                                        }
470                                        String valueAsQueryToken = nextValueOr.getValueAsQueryToken(theCtx);
471                                        valueAsQueryToken = defaultString(valueAsQueryToken);
472                                        b.append(UrlUtil.escapeUrlParam(valueAsQueryToken));
473                                }
474                        }
475                } // for keys
476
477                SortSpec sort = getSort();
478                boolean first = true;
479                while (sort != null) {
480
481                        if (isNotBlank(sort.getParamName())) {
482                                if (first) {
483                                        addUrlParamSeparator(b);
484                                        b.append(Constants.PARAM_SORT);
485                                        b.append('=');
486                                        first = false;
487                                } else {
488                                        b.append(',');
489                                }
490                                if (sort.getOrder() == SortOrderEnum.DESC) {
491                                        b.append('-');
492                                }
493                                b.append(sort.getParamName());
494                        }
495
496                        Validate.isTrue(sort != sort.getChain()); // just in case, shouldn't happen
497                        sort = sort.getChain();
498                }
499
500                if (hasIncludes()) {
501                        addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes());
502                }
503                addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes());
504
505                if (getLastUpdated() != null) {
506                        DateParam lb = getLastUpdated().getLowerBound();
507                        DateParam ub = getLastUpdated().getUpperBound();
508
509                        if (isNotEqualsComparator(lb, ub)) {
510                                addLastUpdateParam(b, NOT_EQUAL, getLastUpdated().getLowerBound());
511                        } else {
512                                addLastUpdateParam(b, GREATERTHAN_OR_EQUALS, lb);
513                                addLastUpdateParam(b, LESSTHAN_OR_EQUALS, ub);
514                        }
515                }
516
517                if (getCount() != null) {
518                        addUrlParamSeparator(b);
519                        b.append(Constants.PARAM_COUNT);
520                        b.append('=');
521                        b.append(getCount());
522                }
523
524                if (getOffset() != null) {
525                        addUrlParamSeparator(b);
526                        b.append(Constants.PARAM_OFFSET);
527                        b.append('=');
528                        b.append(getOffset());
529                }
530
531                // Summary mode (_summary)
532                if (getSummaryMode() != null) {
533                        addUrlParamSeparator(b);
534                        b.append(Constants.PARAM_SUMMARY);
535                        b.append('=');
536                        b.append(getSummaryMode().getCode());
537                }
538
539                // Search count mode (_total)
540                if (getSearchTotalMode() != null) {
541                        addUrlParamSeparator(b);
542                        b.append(Constants.PARAM_SEARCH_TOTAL_MODE);
543                        b.append('=');
544                        b.append(getSearchTotalMode().getCode());
545                }
546
547                // Contained mode
548                // For some reason, instead of null here, we default to false. That said, ommitting it is identical to setting
549                // it to false.
550                if (getSearchContainedMode() != SearchContainedModeEnum.FALSE) {
551                        addUrlParamSeparator(b);
552                        b.append(Constants.PARAM_CONTAINED);
553                        b.append("=");
554                        b.append(getSearchContainedMode().getCode());
555                }
556
557                if (b.length() == 0) {
558                        b.append('?');
559                }
560
561                return b.toString();
562        }
563
564        private boolean isNotEqualsComparator(DateParam theLowerBound, DateParam theUpperBound) {
565                return theLowerBound != null
566                                && theUpperBound != null
567                                && theLowerBound.getPrefix().equals(NOT_EQUAL)
568                                && theUpperBound.getPrefix().equals(NOT_EQUAL);
569        }
570
571        /**
572         * @since 5.5.0
573         */
574        public boolean hasIncludes() {
575                return myIncludes != null && !myIncludes.isEmpty();
576        }
577
578        /**
579         * @since 6.2.0
580         */
581        public boolean hasRevIncludes() {
582                return myRevIncludes != null && !myRevIncludes.isEmpty();
583        }
584
585        @Override
586        public String toString() {
587                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
588                if (!isEmpty()) {
589                        b.append("params", mySearchParameterMap);
590                }
591                if (!getIncludes().isEmpty()) {
592                        b.append("includes", getIncludes());
593                }
594                return b.toString();
595        }
596
597        public void clean() {
598                for (Map.Entry<String, List<List<IQueryParameterType>>> nextParamEntry : this.entrySet()) {
599                        String nextParamName = nextParamEntry.getKey();
600                        List<List<IQueryParameterType>> andOrParams = nextParamEntry.getValue();
601                        cleanParameter(nextParamName, andOrParams);
602                }
603        }
604
605        /*
606         * Given a particular named parameter, e.g. `name`, iterate over AndOrParams and remove any which are empty.
607         */
608        private void cleanParameter(String theParamName, List<List<IQueryParameterType>> theAndOrParams) {
609                theAndOrParams.forEach(orList -> {
610                        List<IQueryParameterType> emptyParameters = orList.stream()
611                                        .filter(nextOr -> nextOr.getMissing() == null)
612                                        .filter(nextOr -> nextOr instanceof QuantityParam)
613                                        .filter(nextOr -> isBlank(((QuantityParam) nextOr).getValueAsString()))
614                                        .collect(Collectors.toList());
615
616                        ourLog.debug("Ignoring empty parameter: {}", theParamName);
617                        orList.removeAll(emptyParameters);
618                });
619                theAndOrParams.removeIf(List::isEmpty);
620        }
621
622        public QuantityParam getNearDistanceParam() {
623                return myNearDistanceParam;
624        }
625
626        public void setNearDistanceParam(QuantityParam theQuantityParam) {
627                myNearDistanceParam = theQuantityParam;
628        }
629
630        public boolean isWantOnlyCount() {
631                return SummaryEnum.COUNT.equals(getSummaryMode()) || INTEGER_0.equals(getCount());
632        }
633
634        public boolean isDeleteExpunge() {
635                return myDeleteExpunge;
636        }
637
638        public SearchParameterMap setDeleteExpunge(boolean theDeleteExpunge) {
639                myDeleteExpunge = theDeleteExpunge;
640                return this;
641        }
642
643        public List<List<IQueryParameterType>> get(String theName) {
644                return mySearchParameterMap.get(theName);
645        }
646
647        public void put(String theName, List<List<IQueryParameterType>> theParams) {
648                mySearchParameterMap.put(theName, theParams);
649        }
650
651        public boolean containsKey(String theName) {
652                return mySearchParameterMap.containsKey(theName);
653        }
654
655        public Set<String> keySet() {
656                return mySearchParameterMap.keySet();
657        }
658
659        public boolean isEmpty() {
660                return mySearchParameterMap.isEmpty();
661        }
662
663        // Wrapper methods
664
665        public Set<Map.Entry<String, List<List<IQueryParameterType>>>> entrySet() {
666                return mySearchParameterMap.entrySet();
667        }
668
669        public List<List<IQueryParameterType>> remove(String theName) {
670                return mySearchParameterMap.remove(theName);
671        }
672
673        /**
674         * Variant of removeByNameAndModifier for unmodified params.
675         *
676         * @param theName the query parameter key
677         * @return an And/Or List of Query Parameters matching the name with no modifier.
678         */
679        public List<List<IQueryParameterType>> removeByNameUnmodified(String theName) {
680                return this.removeByNameAndModifier(theName, "");
681        }
682
683        /**
684         * Given a search parameter name and modifier (e.g. :text),
685         * get and remove all Search Parameters matching this name and modifier
686         *
687         * @param theName     the query parameter key
688         * @param theModifier the qualifier you want to remove - nullable for unmodified params.
689         * @return an And/Or List of Query Parameters matching the qualifier.
690         */
691        public List<List<IQueryParameterType>> removeByNameAndModifier(String theName, String theModifier) {
692                theModifier = StringUtils.defaultString(theModifier, "");
693
694                List<List<IQueryParameterType>> remainderParameters = new ArrayList<>();
695                List<List<IQueryParameterType>> matchingParameters = new ArrayList<>();
696
697                // pull all of them out, partition by match against the qualifier
698                List<List<IQueryParameterType>> andList = mySearchParameterMap.remove(theName);
699                if (andList != null) {
700                        for (List<IQueryParameterType> orList : andList) {
701                                if (!orList.isEmpty()
702                                                && StringUtils.defaultString(orList.get(0).getQueryParameterQualifier(), "")
703                                                                .equals(theModifier)) {
704                                        matchingParameters.add(orList);
705                                } else {
706                                        remainderParameters.add(orList);
707                                }
708                        }
709                }
710
711                // put the unmatched back in.
712                if (!remainderParameters.isEmpty()) {
713                        mySearchParameterMap.put(theName, remainderParameters);
714                }
715                return matchingParameters;
716        }
717
718        public List<List<IQueryParameterType>> removeByNameAndModifier(
719                        String theName, @Nonnull TokenParamModifier theModifier) {
720                return removeByNameAndModifier(theName, theModifier.getValue());
721        }
722
723        /**
724         * For each search parameter in the map, extract any which have the given qualifier.
725         * e.g. Take the url: Observation?code:text=abc&code=123&code:text=def&reason:text=somereason
726         * <p>
727         * If we call this function with `:text`, it will return a map that looks like:
728         * <p>
729         * code -> [[code:text=abc], [code:text=def]]
730         * reason -> [[reason:text=somereason]]
731         * <p>
732         * and the remaining search parameters in the map will be:
733         * <p>
734         * code -> [[code=123]]
735         *
736         * @param theQualifier
737         * @return
738         */
739        public Map<String, List<List<IQueryParameterType>>> removeByQualifier(String theQualifier) {
740
741                Map<String, List<List<IQueryParameterType>>> retVal = new HashMap<>();
742                Set<String> parameterNames = mySearchParameterMap.keySet();
743                for (String parameterName : parameterNames) {
744                        List<List<IQueryParameterType>> paramsWithQualifier = removeByNameAndModifier(parameterName, theQualifier);
745                        retVal.put(parameterName, paramsWithQualifier);
746                }
747
748                return retVal;
749        }
750
751        public Map<String, List<List<IQueryParameterType>>> removeByQualifier(@Nonnull TokenParamModifier theModifier) {
752                return removeByQualifier(theModifier.getValue());
753        }
754
755        public int size() {
756                return mySearchParameterMap.size();
757        }
758
759        public SearchContainedModeEnum getSearchContainedMode() {
760                return mySearchContainedMode;
761        }
762
763        public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) {
764                if (theSearchContainedMode == null) {
765                        mySearchContainedMode = SearchContainedModeEnum.FALSE;
766                } else {
767                        this.mySearchContainedMode = theSearchContainedMode;
768                }
769        }
770
771        /**
772         * Returns true if {@link #getOffset()} and {@link #getCount()} both return a non null response
773         *
774         * @since 5.5.0
775         */
776        public boolean isOffsetQuery() {
777                return getOffset() != null && getCount() != null;
778        }
779
780        public enum EverythingModeEnum {
781                /*
782                 * Don't reorder! We rely on the ordinals
783                 */
784                ENCOUNTER_INSTANCE(false, true, true),
785                ENCOUNTER_TYPE(false, true, false),
786                PATIENT_INSTANCE(true, false, true),
787                PATIENT_TYPE(true, false, false);
788
789                private final boolean myEncounter;
790
791                private final boolean myInstance;
792
793                private final boolean myPatient;
794
795                EverythingModeEnum(boolean thePatient, boolean theEncounter, boolean theInstance) {
796                        assert thePatient ^ theEncounter;
797                        myPatient = thePatient;
798                        myEncounter = theEncounter;
799                        myInstance = theInstance;
800                }
801
802                public boolean isEncounter() {
803                        return myEncounter;
804                }
805
806                public boolean isInstance() {
807                        return myInstance;
808                }
809
810                public boolean isPatient() {
811                        return myPatient;
812                }
813        }
814
815        static int compare(FhirContext theCtx, IQueryParameterType theO1, IQueryParameterType theO2) {
816                CompareToBuilder b = new CompareToBuilder();
817                b.append(theO1.getMissing(), theO2.getMissing());
818                b.append(theO1.getQueryParameterQualifier(), theO2.getQueryParameterQualifier());
819                if (b.toComparison() == 0) {
820                        b.append(theO1.getValueAsQueryToken(theCtx), theO2.getValueAsQueryToken(theCtx));
821                }
822
823                return b.toComparison();
824        }
825
826        public static SearchParameterMap newSynchronous() {
827                SearchParameterMap retVal = new SearchParameterMap();
828                retVal.setLoadSynchronous(true);
829                return retVal;
830        }
831
832        public static SearchParameterMap newSynchronous(String theName, IQueryParameterType theParam) {
833                SearchParameterMap retVal = new SearchParameterMap();
834                retVal.setLoadSynchronous(true);
835                retVal.add(theName, theParam);
836                return retVal;
837        }
838
839        public static class IncludeComparator implements Comparator<Include> {
840
841                @Override
842                public int compare(Include theO1, Include theO2) {
843                        int retVal = StringUtils.compare(theO1.getParamType(), theO2.getParamType());
844                        if (retVal == 0) {
845                                retVal = StringUtils.compare(theO1.getParamName(), theO2.getParamName());
846                        }
847                        if (retVal == 0) {
848                                retVal = StringUtils.compare(theO1.getParamTargetType(), theO2.getParamTargetType());
849                        }
850                        return retVal;
851                }
852        }
853
854        public static class QueryParameterOrComparator implements Comparator<List<IQueryParameterType>> {
855                private final FhirContext myCtx;
856
857                QueryParameterOrComparator(FhirContext theCtx) {
858                        myCtx = theCtx;
859                }
860
861                @Override
862                public int compare(List<IQueryParameterType> theO1, List<IQueryParameterType> theO2) {
863                        // These lists will never be empty
864                        return SearchParameterMap.compare(myCtx, theO1.get(0), theO2.get(0));
865                }
866        }
867
868        public static class QueryParameterTypeComparator implements Comparator<IQueryParameterType> {
869
870                private final FhirContext myCtx;
871
872                QueryParameterTypeComparator(FhirContext theCtx) {
873                        myCtx = theCtx;
874                }
875
876                @Override
877                public int compare(IQueryParameterType theO1, IQueryParameterType theO2) {
878                        return SearchParameterMap.compare(myCtx, theO1, theO2);
879                }
880        }
881
882        public List<SortSpec> getAllChainsInOrder() {
883                final List<SortSpec> allChainsInOrder = new ArrayList<>();
884                for (SortSpec sortSpec = getSort(); sortSpec != null; sortSpec = sortSpec.getChain()) {
885                        allChainsInOrder.add(sortSpec);
886                }
887
888                return Collections.unmodifiableList(allChainsInOrder);
889        }
890}