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