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