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