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