001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2026 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.api.fhirgw.model;
011
012import ca.uhn.fhir.model.api.Include;
013import ca.uhn.fhir.rest.api.SearchStyleEnum;
014import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
015import ca.uhn.fhir.rest.api.SortOrderEnum;
016import ca.uhn.fhir.rest.api.SortSpec;
017import ca.uhn.fhir.rest.param.DateRangeParam;
018import ca.uhn.fhir.rest.param.UriAndListParam;
019import ca.uhn.fhir.rest.param.UriOrListParam;
020import ca.uhn.fhir.rest.param.UriParam;
021import ca.uhn.fhir.util.UrlUtil;
022import com.google.common.collect.ArrayListMultimap;
023import com.google.common.collect.ListMultimap;
024import com.google.common.collect.Multimap;
025import com.google.common.collect.Multimaps;
026import jakarta.annotation.Nonnull;
027import jakarta.annotation.Nullable;
028import org.apache.commons.lang3.Validate;
029import org.springframework.web.util.UriComponentsBuilder;
030
031import java.util.Collections;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036
037import static org.apache.commons.lang3.ObjectUtils.getIfNull;
038
039/**
040 * This class represents a FHIR Gateway Search request
041 */
042public class SearchRequest extends BaseRequest<SearchRequest> {
043
044        private ListMultimap<String, String> myParameters = EMPTY_STRING_MULTIMAP;
045        private Set<Include> myIncludes = Collections.emptySet();
046        private Set<Include> myRevIncludes = Collections.emptySet();
047        private SortSpec mySort;
048        private Integer myCount;
049        private SearchTotalModeEnum myTotalMode;
050        private Integer myOffset;
051        private DateRangeParam myLastUpdated;
052        private UriAndListParam myProfile;
053
054        @Nullable
055        private SearchStyleEnum mySearchStyle;
056
057        /**
058         * Constructor
059         */
060        public SearchRequest() {
061                // nothing
062        }
063
064        /**
065         * Copy constructor
066         */
067        public SearchRequest(SearchRequest theSearchRequest) {
068                super(theSearchRequest);
069
070                if (!theSearchRequest.getParameters().isEmpty()) {
071                        myParameters = ArrayListMultimap.create();
072                        myParameters.putAll(theSearchRequest.getParameters());
073                }
074
075                if (!theSearchRequest.getIncludes().isEmpty()) {
076                        myIncludes = new HashSet<>();
077                        for (Include next : theSearchRequest.getIncludes()) {
078                                myIncludes.add(next.toLocked());
079                        }
080                }
081
082                if (!theSearchRequest.getRevIncludes().isEmpty()) {
083                        myRevIncludes = new HashSet<>();
084                        for (Include next : theSearchRequest.getRevIncludes()) {
085                                myRevIncludes.add(next.toLocked());
086                        }
087                }
088
089                if (theSearchRequest.getSort() != null) {
090                        mySort = clone(theSearchRequest.getSort());
091                }
092
093                if (theSearchRequest.getLastUpdated() != null) {
094                        myLastUpdated = new DateRangeParam(
095                                        theSearchRequest.getLastUpdated().getLowerBound() != null
096                                                        ? theSearchRequest.getLastUpdated().getLowerBound().getValueAsString()
097                                                        : null,
098                                        theSearchRequest.getLastUpdated().getUpperBound() != null
099                                                        ? theSearchRequest.getLastUpdated().getUpperBound().getValueAsString()
100                                                        : null);
101                }
102
103                if (theSearchRequest.getProfile() != null) {
104                        myProfile = new UriAndListParam();
105                        for (UriOrListParam uriOrListParam : theSearchRequest.getProfile().getValuesAsQueryTokens()) {
106                                myProfile.addAnd(uriOrListParam);
107                        }
108                }
109
110                myCount = theSearchRequest.getCount();
111                myOffset = theSearchRequest.getOffset();
112                myTotalMode = theSearchRequest.getTotalMode();
113                mySearchStyle = theSearchRequest.getSearchStyle();
114        }
115
116        private static SortSpec clone(SortSpec theSort) {
117                if (theSort == null) {
118                        return null;
119                }
120                return new SortSpec(theSort.getParamName(), theSort.getOrder(), clone(theSort.getChain()));
121        }
122
123        /**
124         * Specifies the search form (GET vs POST) to use for this search. If set to
125         * <code>null</code>, the default will be used.
126         */
127        @Nullable
128        public SearchStyleEnum getSearchStyle() {
129                return mySearchStyle;
130        }
131
132        /**
133         * Specifies the search form (GET vs POST) to use for this search. If set to
134         * <code>null</code>, the default will be used.
135         */
136        public void setSearchStyle(@Nullable SearchStyleEnum theSearchStyle) {
137                mySearchStyle = theSearchStyle;
138        }
139
140        public Multimap<String, String> getParameters() {
141                return Multimaps.unmodifiableMultimap(myParameters);
142        }
143
144        public SearchRequest setParameters(Map<String, List<String>> theParameters) {
145                Validate.notNull(theParameters, "theParameters must not be null");
146                myParameters = ArrayListMultimap.create();
147                for (Map.Entry<String, List<String>> next : theParameters.entrySet()) {
148                        myParameters.putAll(next.getKey(), next.getValue());
149                }
150                return this;
151        }
152
153        /**
154         * Add a new search parameter
155         */
156        public void addParameter(String theName, String theValue) {
157                Validate.notBlank(theName, "theName must not be empty");
158                Validate.notBlank(theValue, "theValue must not be empty");
159                if (myParameters.isEmpty()) {
160                        myParameters = ArrayListMultimap.create();
161                }
162                myParameters.put(theName, theValue);
163        }
164
165        /**
166         * Adds OR parameters to the list of search parameters as a key value
167         * pair where the value is a comma-delimited list i.e. _id=123,345
168         *
169         * @param theName   The parameter name
170         * @param theValues the values for the parameter
171         */
172        public void addOrListParameter(String theName, List<String> theValues) {
173                Validate.notBlank(theName, "theName must not be empty");
174                Validate.notEmpty(theValues, "theValues must not be empty");
175                String values = String.join(",", theValues);
176                if (myParameters.isEmpty()) {
177                        myParameters = ArrayListMultimap.create();
178                }
179                myParameters.put(theName, values);
180        }
181
182        /**
183         * Remove all parameters with the given name
184         *
185         * @return Returns any parameter values that were removed
186         */
187        public List<String> removeParameters(String theName) {
188                if (!myParameters.isEmpty()) {
189                        return Collections.unmodifiableList(myParameters.removeAll(theName));
190                } else {
191                        return Collections.emptyList();
192                }
193        }
194
195        /**
196         * Remove the parameter with the given name and value, if found. Any other parameters with the same name are kept.
197         *
198         * @param theName  The parameter name
199         * @param theValue The parameter value
200         * @return Returns <code>true</code> if a parameter value was removed
201         */
202        public boolean removeParameter(String theName, String theValue) {
203                return myParameters.remove(theName, theValue);
204        }
205
206        /**
207         * Returns the first parameter value for a given parameter name (returns <code>null</code> if no parameters match the given name)
208         *
209         * @param theName The parameter name
210         * @return The value, or <code>null</code>
211         */
212        @Nullable
213        public String getParameter(String theName) {
214                List<String> values = getParameters(theName);
215                if (!values.isEmpty()) {
216                        return values.get(0);
217                }
218                return null;
219        }
220
221        /**
222         * Returns a List of parameter values for the given name. Will not return <code>null</code>.
223         *
224         * @param theName The parameter name
225         * @return A List of values, or an empty List
226         */
227        @Nonnull
228        public List<String> getParameters(String theName) {
229                List<String> values = myParameters.get(theName);
230                values = getIfNull(values, Collections.emptyList());
231                return Collections.unmodifiableList(values);
232        }
233
234        public Set<Include> getIncludes() {
235                return myIncludes;
236        }
237
238        public SearchRequest setIncludes(Set<Include> theIncludes) {
239                Validate.notNull(theIncludes, "theIncludes must not be null");
240                myIncludes = theIncludes;
241                return this;
242        }
243
244        public Set<Include> getRevIncludes() {
245                return myRevIncludes;
246        }
247
248        public SearchRequest setRevIncludes(Set<Include> theRevIncludes) {
249                Validate.notNull(theRevIncludes, "theRevIncludes must not be null");
250                myRevIncludes = theRevIncludes;
251                return this;
252        }
253
254        public SortSpec getSort() {
255                return mySort;
256        }
257
258        public SearchRequest setSort(SortSpec theSort) {
259                mySort = theSort;
260                return this;
261        }
262
263        public Integer getCount() {
264                return myCount;
265        }
266
267        public SearchRequest setCount(Integer theCount) {
268                myCount = theCount;
269                return this;
270        }
271
272        public Integer getOffset() {
273                return myOffset;
274        }
275
276        public SearchRequest setOffset(Integer theOffset) {
277                myOffset = theOffset;
278                return this;
279        }
280
281        public DateRangeParam getLastUpdated() {
282                return myLastUpdated;
283        }
284
285        public SearchRequest setLastUpdated(DateRangeParam theLastUpdated) {
286                myLastUpdated = theLastUpdated;
287                return this;
288        }
289
290        public UriAndListParam getProfile() {
291                return myProfile;
292        }
293
294        public SearchRequest setProfile(UriAndListParam theProfile) {
295                myProfile = theProfile;
296                return this;
297        }
298
299        public SearchTotalModeEnum getTotalMode() {
300                return myTotalMode;
301        }
302
303        public SearchRequest setTotalMode(SearchTotalModeEnum theTotalMode) {
304                myTotalMode = theTotalMode;
305                return this;
306        }
307
308        /**
309         * Serializes the structured search parameters into a URL query string.
310         * Returns an empty string if there are no parameters, or a string starting with "?"
311         * containing all raw and structured parameters.
312         */
313        public String toQueryString() {
314                UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
315
316                // Raw parameters
317                for (Map.Entry<String, String> entry : myParameters.entries()) {
318                        builder.queryParam(UrlUtil.escapeUrlParam(entry.getKey()), UrlUtil.escapeUrlParam(entry.getValue()));
319                }
320
321                for (Include include : myIncludes) {
322                        String paramName = include.isRecurse() ? "_include:iterate" : "_include";
323                        builder.queryParam(UrlUtil.escapeUrlParam(paramName), UrlUtil.escapeUrlParam(include.getValue()));
324                }
325
326                for (Include revInclude : myRevIncludes) {
327                        String paramName = revInclude.isRecurse() ? "_revinclude:iterate" : "_revinclude";
328                        builder.queryParam(UrlUtil.escapeUrlParam(paramName), UrlUtil.escapeUrlParam(revInclude.getValue()));
329                }
330
331                if (myCount != null) {
332                        builder.queryParam("_count", myCount);
333                }
334
335                if (myOffset != null) {
336                        builder.queryParam("_offset", myOffset);
337                }
338
339                if (mySort != null) {
340                        builder.queryParam(UrlUtil.escapeUrlParam("_sort"), UrlUtil.escapeUrlParam(serializeSort(mySort)));
341                }
342
343                if (myLastUpdated != null) {
344                        if (myLastUpdated.getLowerBound() != null) {
345                                builder.queryParam(
346                                                UrlUtil.escapeUrlParam("_lastUpdated"),
347                                                UrlUtil.escapeUrlParam(
348                                                                myLastUpdated.getLowerBound().getPrefix().getValue()
349                                                                                + myLastUpdated.getLowerBound().getValueAsString()));
350                        }
351                        if (myLastUpdated.getUpperBound() != null) {
352                                builder.queryParam(
353                                                UrlUtil.escapeUrlParam("_lastUpdated"),
354                                                UrlUtil.escapeUrlParam(
355                                                                myLastUpdated.getUpperBound().getPrefix().getValue()
356                                                                                + myLastUpdated.getUpperBound().getValueAsString()));
357                        }
358                }
359
360                if (myTotalMode != null) {
361                        builder.queryParam("_total", myTotalMode.getCode());
362                }
363
364                if (myProfile != null) {
365                        for (UriOrListParam orList : myProfile.getValuesAsQueryTokens()) {
366                                StringBuilder profileValue = new StringBuilder();
367                                boolean firstOr = true;
368                                for (UriParam uriParam : orList.getValuesAsQueryTokens()) {
369                                        if (!firstOr) {
370                                                profileValue.append(UrlUtil.escapeUrlParam(","));
371                                        }
372                                        profileValue.append(UrlUtil.escapeUrlParam(uriParam.getValue()));
373                                        firstOr = false;
374                                }
375                                builder.queryParam(UrlUtil.escapeUrlParam("_profile"), profileValue.toString());
376                        }
377                }
378
379                return builder.build().toUriString();
380        }
381
382        private static String serializeSort(SortSpec theSort) {
383                StringBuilder sb = new StringBuilder();
384                SortSpec current = theSort;
385                boolean first = true;
386                while (current != null) {
387                        if (!first) {
388                                sb.append(",");
389                        }
390                        if (current.getOrder() == SortOrderEnum.DESC) {
391                                sb.append("-");
392                        }
393                        sb.append(current.getParamName());
394                        current = current.getChain();
395                        first = false;
396                }
397                return sb.toString();
398        }
399}