001/*
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.method;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.model.api.IQueryParameterAnd;
027import ca.uhn.fhir.model.api.IQueryParameterOr;
028import ca.uhn.fhir.model.api.IQueryParameterType;
029import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
030import ca.uhn.fhir.model.base.composite.BaseQuantityDt;
031import ca.uhn.fhir.model.primitive.StringDt;
032import ca.uhn.fhir.rest.annotation.OptionalParam;
033import ca.uhn.fhir.rest.api.Constants;
034import ca.uhn.fhir.rest.api.QualifiedParamList;
035import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
036import ca.uhn.fhir.rest.param.CompositeAndListParam;
037import ca.uhn.fhir.rest.param.CompositeOrListParam;
038import ca.uhn.fhir.rest.param.CompositeParam;
039import ca.uhn.fhir.rest.param.DateAndListParam;
040import ca.uhn.fhir.rest.param.DateOrListParam;
041import ca.uhn.fhir.rest.param.DateParam;
042import ca.uhn.fhir.rest.param.DateRangeParam;
043import ca.uhn.fhir.rest.param.HasAndListParam;
044import ca.uhn.fhir.rest.param.HasOrListParam;
045import ca.uhn.fhir.rest.param.HasParam;
046import ca.uhn.fhir.rest.param.NumberAndListParam;
047import ca.uhn.fhir.rest.param.NumberOrListParam;
048import ca.uhn.fhir.rest.param.NumberParam;
049import ca.uhn.fhir.rest.param.QuantityAndListParam;
050import ca.uhn.fhir.rest.param.QuantityOrListParam;
051import ca.uhn.fhir.rest.param.QuantityParam;
052import ca.uhn.fhir.rest.param.ReferenceAndListParam;
053import ca.uhn.fhir.rest.param.ReferenceOrListParam;
054import ca.uhn.fhir.rest.param.ReferenceParam;
055import ca.uhn.fhir.rest.param.SpecialAndListParam;
056import ca.uhn.fhir.rest.param.SpecialOrListParam;
057import ca.uhn.fhir.rest.param.SpecialParam;
058import ca.uhn.fhir.rest.param.StringAndListParam;
059import ca.uhn.fhir.rest.param.StringOrListParam;
060import ca.uhn.fhir.rest.param.StringParam;
061import ca.uhn.fhir.rest.param.TokenAndListParam;
062import ca.uhn.fhir.rest.param.TokenOrListParam;
063import ca.uhn.fhir.rest.param.TokenParam;
064import ca.uhn.fhir.rest.param.UriAndListParam;
065import ca.uhn.fhir.rest.param.UriOrListParam;
066import ca.uhn.fhir.rest.param.UriParam;
067import ca.uhn.fhir.rest.param.binder.CalendarBinder;
068import ca.uhn.fhir.rest.param.binder.DateBinder;
069import ca.uhn.fhir.rest.param.binder.FhirPrimitiveBinder;
070import ca.uhn.fhir.rest.param.binder.IParamBinder;
071import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
072import ca.uhn.fhir.rest.param.binder.QueryParameterOrBinder;
073import ca.uhn.fhir.rest.param.binder.QueryParameterTypeBinder;
074import ca.uhn.fhir.rest.param.binder.StringBinder;
075import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
076import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
077import ca.uhn.fhir.util.ReflectionUtil;
078import org.apache.commons.lang3.builder.ToStringBuilder;
079import org.hl7.fhir.instance.model.api.IBaseResource;
080import org.hl7.fhir.instance.model.api.IPrimitiveType;
081
082import java.util.ArrayList;
083import java.util.Arrays;
084import java.util.Calendar;
085import java.util.Collection;
086import java.util.Collections;
087import java.util.Date;
088import java.util.HashMap;
089import java.util.HashSet;
090import java.util.List;
091import java.util.Set;
092
093public class SearchParameter extends BaseQueryParameter {
094
095        private static final String EMPTY_STRING = "";
096        private static final HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
097        private static final HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
098        static final String QUALIFIER_ANY_TYPE = ":*";
099
100        static {
101                ourParamTypes = new HashMap<>();
102                ourParamQualifiers = new HashMap<>();
103
104                ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING);
105                ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING);
106                ourParamTypes.put(StringAndListParam.class, RestSearchParameterTypeEnum.STRING);
107                ourParamQualifiers.put(RestSearchParameterTypeEnum.STRING, Set.of(new String[] {
108                        Constants.PARAMQUALIFIER_STRING_EXACT,
109                        Constants.PARAMQUALIFIER_STRING_CONTAINS,
110                        Constants.PARAMQUALIFIER_MISSING,
111                        EMPTY_STRING
112                }));
113
114                ourParamTypes.put(UriParam.class, RestSearchParameterTypeEnum.URI);
115                ourParamTypes.put(UriOrListParam.class, RestSearchParameterTypeEnum.URI);
116                ourParamTypes.put(UriAndListParam.class, RestSearchParameterTypeEnum.URI);
117                // TODO: are these right for URI?
118                ourParamQualifiers.put(RestSearchParameterTypeEnum.URI, Set.of(new String[] {
119                        Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING
120                }));
121
122                ourParamTypes.put(TokenParam.class, RestSearchParameterTypeEnum.TOKEN);
123                ourParamTypes.put(TokenOrListParam.class, RestSearchParameterTypeEnum.TOKEN);
124                ourParamTypes.put(TokenAndListParam.class, RestSearchParameterTypeEnum.TOKEN);
125                ourParamQualifiers.put(RestSearchParameterTypeEnum.TOKEN, Set.of(new String[] {
126                        Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING
127                }));
128
129                ourParamTypes.put(DateParam.class, RestSearchParameterTypeEnum.DATE);
130                ourParamTypes.put(DateOrListParam.class, RestSearchParameterTypeEnum.DATE);
131                ourParamTypes.put(DateAndListParam.class, RestSearchParameterTypeEnum.DATE);
132                ourParamTypes.put(DateRangeParam.class, RestSearchParameterTypeEnum.DATE);
133                ourParamQualifiers.put(
134                                RestSearchParameterTypeEnum.DATE,
135                                Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING}));
136
137                ourParamTypes.put(QuantityParam.class, RestSearchParameterTypeEnum.QUANTITY);
138                ourParamTypes.put(QuantityOrListParam.class, RestSearchParameterTypeEnum.QUANTITY);
139                ourParamTypes.put(QuantityAndListParam.class, RestSearchParameterTypeEnum.QUANTITY);
140                ourParamQualifiers.put(
141                                RestSearchParameterTypeEnum.QUANTITY,
142                                Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING}));
143
144                ourParamTypes.put(NumberParam.class, RestSearchParameterTypeEnum.NUMBER);
145                ourParamTypes.put(NumberOrListParam.class, RestSearchParameterTypeEnum.NUMBER);
146                ourParamTypes.put(NumberAndListParam.class, RestSearchParameterTypeEnum.NUMBER);
147                ourParamQualifiers.put(
148                                RestSearchParameterTypeEnum.NUMBER,
149                                Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING}));
150
151                ourParamTypes.put(ReferenceParam.class, RestSearchParameterTypeEnum.REFERENCE);
152                ourParamTypes.put(ReferenceOrListParam.class, RestSearchParameterTypeEnum.REFERENCE);
153                ourParamTypes.put(ReferenceAndListParam.class, RestSearchParameterTypeEnum.REFERENCE);
154                // --vvvv-- no empty because that gets added from OptionalParam#chainWhitelist
155                ourParamQualifiers.put(
156                                RestSearchParameterTypeEnum.REFERENCE, Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING}));
157
158                ourParamTypes.put(CompositeParam.class, RestSearchParameterTypeEnum.COMPOSITE);
159                ourParamTypes.put(CompositeOrListParam.class, RestSearchParameterTypeEnum.COMPOSITE);
160                ourParamTypes.put(CompositeAndListParam.class, RestSearchParameterTypeEnum.COMPOSITE);
161                ourParamQualifiers.put(
162                                RestSearchParameterTypeEnum.COMPOSITE,
163                                Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING}));
164
165                ourParamTypes.put(HasParam.class, RestSearchParameterTypeEnum.HAS);
166                ourParamTypes.put(HasOrListParam.class, RestSearchParameterTypeEnum.HAS);
167                ourParamTypes.put(HasAndListParam.class, RestSearchParameterTypeEnum.HAS);
168
169                ourParamTypes.put(SpecialParam.class, RestSearchParameterTypeEnum.SPECIAL);
170                ourParamTypes.put(SpecialOrListParam.class, RestSearchParameterTypeEnum.SPECIAL);
171                ourParamTypes.put(SpecialAndListParam.class, RestSearchParameterTypeEnum.SPECIAL);
172                ourParamQualifiers.put(
173                                RestSearchParameterTypeEnum.SPECIAL, Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING}));
174        }
175
176        private List<Class<? extends IQueryParameterType>> myCompositeTypes = Collections.emptyList();
177        private List<Class<? extends IBaseResource>> myDeclaredTypes;
178        private String myDescription;
179        private String myName;
180        private IParamBinder<?> myParamBinder;
181        private RestSearchParameterTypeEnum myParamType;
182        private Set<String> myQualifierBlacklist;
183        private Set<String> myQualifierWhitelist;
184        private boolean myRequired;
185        private Class<?> myType;
186        private boolean mySupportsRepetition = false;
187
188        public SearchParameter() {}
189
190        public SearchParameter(String theName, boolean theRequired) {
191                this.myName = theName;
192                this.myRequired = theRequired;
193        }
194
195        /*
196         * (non-Javadoc)
197         *
198         * @see ca.uhn.fhir.rest.param.IParameter#encode(java.lang.Object)
199         */
200        @Override
201        public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
202                ArrayList<QualifiedParamList> retVal = new ArrayList<>();
203
204                // TODO: declaring method should probably have a generic type..
205                @SuppressWarnings("rawtypes")
206                IParamBinder paramBinder = myParamBinder;
207
208                @SuppressWarnings("unchecked")
209                List<IQueryParameterOr<?>> val = paramBinder.encode(theContext, theObject);
210                for (IQueryParameterOr<?> nextOr : val) {
211                        retVal.add(new QualifiedParamList(nextOr, theContext));
212                }
213
214                return retVal;
215        }
216
217        public List<Class<? extends IBaseResource>> getDeclaredTypes() {
218                return Collections.unmodifiableList(myDeclaredTypes);
219        }
220
221        public String getDescription() {
222                return myDescription;
223        }
224
225        /*
226         * (non-Javadoc)
227         *
228         * @see ca.uhn.fhir.rest.param.IParameter#getName()
229         */
230        @Override
231        public String getName() {
232                return myName;
233        }
234
235        @Override
236        public RestSearchParameterTypeEnum getParamType() {
237                return myParamType;
238        }
239
240        @Override
241        public Set<String> getQualifierBlacklist() {
242                return myQualifierBlacklist;
243        }
244
245        @Override
246        public Set<String> getQualifierWhitelist() {
247                return myQualifierWhitelist;
248        }
249
250        public Class<?> getType() {
251                return myType;
252        }
253
254        @Override
255        public boolean handlesMissing() {
256                return false;
257        }
258
259        @Override
260        public boolean isRequired() {
261                return myRequired;
262        }
263
264        /*
265         * (non-Javadoc)
266         *
267         * @see ca.uhn.fhir.rest.param.IParameter#parse(java.util.List)
268         */
269        @Override
270        public Object parse(FhirContext theContext, List<QualifiedParamList> theString)
271                        throws InternalErrorException, InvalidRequestException {
272                return myParamBinder.parse(theContext, getName(), theString);
273        }
274
275        public void setChainLists(String[] theChainWhitelist, String[] theChainBlacklist) {
276                myQualifierWhitelist = new HashSet<>(theChainWhitelist.length);
277                myQualifierWhitelist.add(QUALIFIER_ANY_TYPE);
278
279                for (String nextChain : theChainWhitelist) {
280                        if (nextChain.equals(OptionalParam.ALLOW_CHAIN_ANY)) {
281                                myQualifierWhitelist.add('.' + OptionalParam.ALLOW_CHAIN_ANY);
282                        } else if (nextChain.equals(EMPTY_STRING)) {
283                                myQualifierWhitelist.add(".");
284                        } else {
285                                myQualifierWhitelist.add('.' + nextChain);
286                        }
287                }
288
289                if (theChainBlacklist.length > 0) {
290                        myQualifierBlacklist = new HashSet<>(theChainBlacklist.length);
291                        for (String next : theChainBlacklist) {
292                                if (next.equals(EMPTY_STRING)) {
293                                        myQualifierBlacklist.add(EMPTY_STRING);
294                                } else {
295                                        myQualifierBlacklist.add('.' + next);
296                                }
297                        }
298                }
299        }
300
301        public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) {
302                myCompositeTypes = Arrays.asList(theCompositeTypes);
303        }
304
305        public void setDeclaredTypes(Class<? extends IBaseResource>[] theTypes) {
306                myDeclaredTypes = Arrays.asList(theTypes);
307        }
308
309        public void setDescription(String theDescription) {
310                myDescription = theDescription;
311        }
312
313        public void setName(String name) {
314                this.myName = name;
315        }
316
317        public void setRequired(boolean required) {
318                this.myRequired = required;
319        }
320
321        @Override
322        protected boolean supportsRepetition() {
323                return mySupportsRepetition;
324        }
325
326        @SuppressWarnings("unchecked")
327        public void setType(
328                        FhirContext theContext,
329                        final Class<?> theType,
330                        Class<? extends Collection<?>> theInnerCollectionType,
331                        Class<? extends Collection<?>> theOuterCollectionType) {
332
333                this.myType = theType;
334                if (IQueryParameterType.class.isAssignableFrom(theType)) {
335                        myParamBinder =
336                                        new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) theType, myCompositeTypes);
337                } else if (IQueryParameterOr.class.isAssignableFrom(theType)) {
338                        myParamBinder =
339                                        new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) theType, myCompositeTypes);
340                } else if (IQueryParameterAnd.class.isAssignableFrom(theType)) {
341                        myParamBinder =
342                                        new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) theType, myCompositeTypes);
343                        mySupportsRepetition = true;
344                } else if (String.class.equals(theType)) {
345                        myParamBinder = new StringBinder();
346                        myParamType = RestSearchParameterTypeEnum.STRING;
347                } else if (Date.class.equals(theType)) {
348                        myParamBinder = new DateBinder();
349                        myParamType = RestSearchParameterTypeEnum.DATE;
350                } else if (Calendar.class.equals(theType)) {
351                        myParamBinder = new CalendarBinder();
352                        myParamType = RestSearchParameterTypeEnum.DATE;
353                } else if (IPrimitiveType.class.isAssignableFrom(theType) && ReflectionUtil.isInstantiable(theType)) {
354                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition)
355                                        theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) theType);
356                        if (def.getNativeType() != null) {
357                                if (def.getNativeType().equals(Date.class)) {
358                                        myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType);
359                                        myParamType = RestSearchParameterTypeEnum.DATE;
360                                } else if (def.getNativeType().equals(String.class)) {
361                                        myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType);
362                                        myParamType = RestSearchParameterTypeEnum.STRING;
363                                }
364                        }
365                } else {
366                        throw new ConfigurationException(
367                                        Msg.code(354) + "Unsupported data type for parameter: " + theType.getCanonicalName());
368                }
369
370                RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(theType);
371                if (typeEnum != null) {
372                        Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
373                        if (builtInQualifiers != null) {
374                                if (myQualifierWhitelist != null) {
375                                        HashSet<String> qualifierWhitelist = new HashSet<>();
376                                        qualifierWhitelist.addAll(myQualifierWhitelist);
377                                        qualifierWhitelist.addAll(builtInQualifiers);
378                                        myQualifierWhitelist = qualifierWhitelist;
379                                } else {
380                                        myQualifierWhitelist = Collections.unmodifiableSet(builtInQualifiers);
381                                }
382                        }
383                }
384
385                if (myParamType == null) {
386                        myParamType = typeEnum;
387                }
388
389                if (myParamType != null) {
390                        // ok
391                } else if (StringDt.class.isAssignableFrom(theType)) {
392                        myParamType = RestSearchParameterTypeEnum.STRING;
393                } else if (BaseIdentifierDt.class.isAssignableFrom(theType)) {
394                        myParamType = RestSearchParameterTypeEnum.TOKEN;
395                } else if (BaseQuantityDt.class.isAssignableFrom(theType)) {
396                        myParamType = RestSearchParameterTypeEnum.QUANTITY;
397                } else if (ReferenceParam.class.isAssignableFrom(theType)) {
398                        myParamType = RestSearchParameterTypeEnum.REFERENCE;
399                } else if (HasParam.class.isAssignableFrom(theType)) {
400                        myParamType = RestSearchParameterTypeEnum.STRING;
401                } else {
402                        throw new ConfigurationException(Msg.code(355) + "Unknown search parameter type: " + theType);
403                }
404
405                // NB: Once this is enabled, we should return true from handlesMissing if
406                // it's a collection theType
407                // if (theInnerCollectionType != null) {
408                // this.parser = new CollectionBinder(this.parser, theInnerCollectionType);
409                // }
410                //
411                // if (theOuterCollectionType != null) {
412                // this.parser = new CollectionBinder(this.parser, theOuterCollectionType);
413                // }
414
415        }
416
417        @Override
418        public String toString() {
419                ToStringBuilder retVal = new ToStringBuilder(this);
420                retVal.append("name", myName);
421                retVal.append("required", myRequired);
422                return retVal.toString();
423        }
424}