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