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.CollectionUtil;
078import ca.uhn.fhir.util.ReflectionUtil;
079import org.apache.commons.lang3.builder.ToStringBuilder;
080import org.hl7.fhir.instance.model.api.IBaseResource;
081import org.hl7.fhir.instance.model.api.IPrimitiveType;
082
083import java.util.ArrayList;
084import java.util.Arrays;
085import java.util.Calendar;
086import java.util.Collection;
087import java.util.Collections;
088import java.util.Date;
089import java.util.HashMap;
090import java.util.HashSet;
091import java.util.List;
092import java.util.Set;
093
094public class SearchParameter extends BaseQueryParameter {
095
096        private static final String EMPTY_STRING = "";
097        private static final HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
098        private static final HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
099        static final String QUALIFIER_ANY_TYPE = ":*";
100
101        static {
102                ourParamTypes = new HashMap<>();
103                ourParamQualifiers = new HashMap<>();
104
105                ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING);
106                ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING);
107                ourParamTypes.put(StringAndListParam.class, RestSearchParameterTypeEnum.STRING);
108                ourParamQualifiers.put(
109                                RestSearchParameterTypeEnum.STRING,
110                                CollectionUtil.newSet(
111                                                Constants.PARAMQUALIFIER_STRING_EXACT,
112                                                Constants.PARAMQUALIFIER_STRING_CONTAINS,
113                                                Constants.PARAMQUALIFIER_MISSING,
114                                                EMPTY_STRING));
115
116                ourParamTypes.put(UriParam.class, RestSearchParameterTypeEnum.URI);
117                ourParamTypes.put(UriOrListParam.class, RestSearchParameterTypeEnum.URI);
118                ourParamTypes.put(UriAndListParam.class, RestSearchParameterTypeEnum.URI);
119                // TODO: are these right for URI?
120                ourParamQualifiers.put(
121                                RestSearchParameterTypeEnum.URI,
122                                CollectionUtil.newSet(
123                                                Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
124
125                ourParamTypes.put(TokenParam.class, RestSearchParameterTypeEnum.TOKEN);
126                ourParamTypes.put(TokenOrListParam.class, RestSearchParameterTypeEnum.TOKEN);
127                ourParamTypes.put(TokenAndListParam.class, RestSearchParameterTypeEnum.TOKEN);
128                ourParamQualifiers.put(
129                                RestSearchParameterTypeEnum.TOKEN,
130                                CollectionUtil.newSet(
131                                                Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
132
133                ourParamTypes.put(DateParam.class, RestSearchParameterTypeEnum.DATE);
134                ourParamTypes.put(DateOrListParam.class, RestSearchParameterTypeEnum.DATE);
135                ourParamTypes.put(DateAndListParam.class, RestSearchParameterTypeEnum.DATE);
136                ourParamTypes.put(DateRangeParam.class, RestSearchParameterTypeEnum.DATE);
137                ourParamQualifiers.put(
138                                RestSearchParameterTypeEnum.DATE,
139                                CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
140
141                ourParamTypes.put(QuantityParam.class, RestSearchParameterTypeEnum.QUANTITY);
142                ourParamTypes.put(QuantityOrListParam.class, RestSearchParameterTypeEnum.QUANTITY);
143                ourParamTypes.put(QuantityAndListParam.class, RestSearchParameterTypeEnum.QUANTITY);
144                ourParamQualifiers.put(
145                                RestSearchParameterTypeEnum.QUANTITY,
146                                CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
147
148                ourParamTypes.put(NumberParam.class, RestSearchParameterTypeEnum.NUMBER);
149                ourParamTypes.put(NumberOrListParam.class, RestSearchParameterTypeEnum.NUMBER);
150                ourParamTypes.put(NumberAndListParam.class, RestSearchParameterTypeEnum.NUMBER);
151                ourParamQualifiers.put(
152                                RestSearchParameterTypeEnum.NUMBER,
153                                CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
154
155                ourParamTypes.put(ReferenceParam.class, RestSearchParameterTypeEnum.REFERENCE);
156                ourParamTypes.put(ReferenceOrListParam.class, RestSearchParameterTypeEnum.REFERENCE);
157                ourParamTypes.put(ReferenceAndListParam.class, RestSearchParameterTypeEnum.REFERENCE);
158                // --vvvv-- no empty because that gets added from OptionalParam#chainWhitelist
159                ourParamQualifiers.put(
160                                RestSearchParameterTypeEnum.REFERENCE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING));
161
162                ourParamTypes.put(CompositeParam.class, RestSearchParameterTypeEnum.COMPOSITE);
163                ourParamTypes.put(CompositeOrListParam.class, RestSearchParameterTypeEnum.COMPOSITE);
164                ourParamTypes.put(CompositeAndListParam.class, RestSearchParameterTypeEnum.COMPOSITE);
165                ourParamQualifiers.put(
166                                RestSearchParameterTypeEnum.COMPOSITE,
167                                CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
168
169                ourParamTypes.put(HasParam.class, RestSearchParameterTypeEnum.HAS);
170                ourParamTypes.put(HasOrListParam.class, RestSearchParameterTypeEnum.HAS);
171                ourParamTypes.put(HasAndListParam.class, RestSearchParameterTypeEnum.HAS);
172
173                ourParamTypes.put(SpecialParam.class, RestSearchParameterTypeEnum.SPECIAL);
174                ourParamTypes.put(SpecialOrListParam.class, RestSearchParameterTypeEnum.SPECIAL);
175                ourParamTypes.put(SpecialAndListParam.class, RestSearchParameterTypeEnum.SPECIAL);
176                ourParamQualifiers.put(
177                                RestSearchParameterTypeEnum.SPECIAL, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING));
178        }
179
180        private List<Class<? extends IQueryParameterType>> myCompositeTypes = Collections.emptyList();
181        private List<Class<? extends IBaseResource>> myDeclaredTypes;
182        private String myDescription;
183        private String myName;
184        private IParamBinder<?> myParamBinder;
185        private RestSearchParameterTypeEnum myParamType;
186        private Set<String> myQualifierBlacklist;
187        private Set<String> myQualifierWhitelist;
188        private boolean myRequired;
189        private Class<?> myType;
190        private boolean mySupportsRepetition = false;
191
192        public SearchParameter() {}
193
194        public SearchParameter(String theName, boolean theRequired) {
195                this.myName = theName;
196                this.myRequired = theRequired;
197        }
198
199        /*
200         * (non-Javadoc)
201         *
202         * @see ca.uhn.fhir.rest.param.IParameter#encode(java.lang.Object)
203         */
204        @Override
205        public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
206                ArrayList<QualifiedParamList> retVal = new ArrayList<>();
207
208                // TODO: declaring method should probably have a generic type..
209                @SuppressWarnings("rawtypes")
210                IParamBinder paramBinder = myParamBinder;
211
212                @SuppressWarnings("unchecked")
213                List<IQueryParameterOr<?>> val = paramBinder.encode(theContext, theObject);
214                for (IQueryParameterOr<?> nextOr : val) {
215                        retVal.add(new QualifiedParamList(nextOr, theContext));
216                }
217
218                return retVal;
219        }
220
221        public List<Class<? extends IBaseResource>> getDeclaredTypes() {
222                return Collections.unmodifiableList(myDeclaredTypes);
223        }
224
225        public String getDescription() {
226                return myDescription;
227        }
228
229        /*
230         * (non-Javadoc)
231         *
232         * @see ca.uhn.fhir.rest.param.IParameter#getName()
233         */
234        @Override
235        public String getName() {
236                return myName;
237        }
238
239        @Override
240        public RestSearchParameterTypeEnum getParamType() {
241                return myParamType;
242        }
243
244        @Override
245        public Set<String> getQualifierBlacklist() {
246                return myQualifierBlacklist;
247        }
248
249        @Override
250        public Set<String> getQualifierWhitelist() {
251                return myQualifierWhitelist;
252        }
253
254        public Class<?> getType() {
255                return myType;
256        }
257
258        @Override
259        public boolean handlesMissing() {
260                return false;
261        }
262
263        @Override
264        public boolean isRequired() {
265                return myRequired;
266        }
267
268        /*
269         * (non-Javadoc)
270         *
271         * @see ca.uhn.fhir.rest.param.IParameter#parse(java.util.List)
272         */
273        @Override
274        public Object parse(FhirContext theContext, List<QualifiedParamList> theString)
275                        throws InternalErrorException, InvalidRequestException {
276                return myParamBinder.parse(theContext, getName(), theString);
277        }
278
279        public void setChainLists(String[] theChainWhitelist, String[] theChainBlacklist) {
280                myQualifierWhitelist = new HashSet<>(theChainWhitelist.length);
281                myQualifierWhitelist.add(QUALIFIER_ANY_TYPE);
282
283                for (String nextChain : theChainWhitelist) {
284                        if (nextChain.equals(OptionalParam.ALLOW_CHAIN_ANY)) {
285                                myQualifierWhitelist.add('.' + OptionalParam.ALLOW_CHAIN_ANY);
286                        } else if (nextChain.equals(EMPTY_STRING)) {
287                                myQualifierWhitelist.add(".");
288                        } else {
289                                myQualifierWhitelist.add('.' + nextChain);
290                        }
291                }
292
293                if (theChainBlacklist.length > 0) {
294                        myQualifierBlacklist = new HashSet<>(theChainBlacklist.length);
295                        for (String next : theChainBlacklist) {
296                                if (next.equals(EMPTY_STRING)) {
297                                        myQualifierBlacklist.add(EMPTY_STRING);
298                                } else {
299                                        myQualifierBlacklist.add('.' + next);
300                                }
301                        }
302                }
303        }
304
305        public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) {
306                myCompositeTypes = Arrays.asList(theCompositeTypes);
307        }
308
309        public void setDeclaredTypes(Class<? extends IBaseResource>[] theTypes) {
310                myDeclaredTypes = Arrays.asList(theTypes);
311        }
312
313        public void setDescription(String theDescription) {
314                myDescription = theDescription;
315        }
316
317        public void setName(String name) {
318                this.myName = name;
319        }
320
321        public void setRequired(boolean required) {
322                this.myRequired = required;
323        }
324
325        @Override
326        protected boolean supportsRepetition() {
327                return mySupportsRepetition;
328        }
329
330        @SuppressWarnings("unchecked")
331        public void setType(
332                        FhirContext theContext,
333                        final Class<?> theType,
334                        Class<? extends Collection<?>> theInnerCollectionType,
335                        Class<? extends Collection<?>> theOuterCollectionType) {
336
337                this.myType = theType;
338                if (IQueryParameterType.class.isAssignableFrom(theType)) {
339                        myParamBinder =
340                                        new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) theType, myCompositeTypes);
341                } else if (IQueryParameterOr.class.isAssignableFrom(theType)) {
342                        myParamBinder =
343                                        new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) theType, myCompositeTypes);
344                } else if (IQueryParameterAnd.class.isAssignableFrom(theType)) {
345                        myParamBinder =
346                                        new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) theType, myCompositeTypes);
347                        mySupportsRepetition = true;
348                } else if (String.class.equals(theType)) {
349                        myParamBinder = new StringBinder();
350                        myParamType = RestSearchParameterTypeEnum.STRING;
351                } else if (Date.class.equals(theType)) {
352                        myParamBinder = new DateBinder();
353                        myParamType = RestSearchParameterTypeEnum.DATE;
354                } else if (Calendar.class.equals(theType)) {
355                        myParamBinder = new CalendarBinder();
356                        myParamType = RestSearchParameterTypeEnum.DATE;
357                } else if (IPrimitiveType.class.isAssignableFrom(theType) && ReflectionUtil.isInstantiable(theType)) {
358                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition)
359                                        theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) theType);
360                        if (def.getNativeType() != null) {
361                                if (def.getNativeType().equals(Date.class)) {
362                                        myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType);
363                                        myParamType = RestSearchParameterTypeEnum.DATE;
364                                } else if (def.getNativeType().equals(String.class)) {
365                                        myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType);
366                                        myParamType = RestSearchParameterTypeEnum.STRING;
367                                }
368                        }
369                } else {
370                        throw new ConfigurationException(
371                                        Msg.code(354) + "Unsupported data type for parameter: " + theType.getCanonicalName());
372                }
373
374                RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(theType);
375                if (typeEnum != null) {
376                        Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
377                        if (builtInQualifiers != null) {
378                                if (myQualifierWhitelist != null) {
379                                        HashSet<String> qualifierWhitelist = new HashSet<>();
380                                        qualifierWhitelist.addAll(myQualifierWhitelist);
381                                        qualifierWhitelist.addAll(builtInQualifiers);
382                                        myQualifierWhitelist = qualifierWhitelist;
383                                } else {
384                                        myQualifierWhitelist = Collections.unmodifiableSet(builtInQualifiers);
385                                }
386                        }
387                }
388
389                if (myParamType == null) {
390                        myParamType = typeEnum;
391                }
392
393                if (myParamType != null) {
394                        // ok
395                } else if (StringDt.class.isAssignableFrom(theType)) {
396                        myParamType = RestSearchParameterTypeEnum.STRING;
397                } else if (BaseIdentifierDt.class.isAssignableFrom(theType)) {
398                        myParamType = RestSearchParameterTypeEnum.TOKEN;
399                } else if (BaseQuantityDt.class.isAssignableFrom(theType)) {
400                        myParamType = RestSearchParameterTypeEnum.QUANTITY;
401                } else if (ReferenceParam.class.isAssignableFrom(theType)) {
402                        myParamType = RestSearchParameterTypeEnum.REFERENCE;
403                } else if (HasParam.class.isAssignableFrom(theType)) {
404                        myParamType = RestSearchParameterTypeEnum.STRING;
405                } else {
406                        throw new ConfigurationException(Msg.code(355) + "Unknown search parameter type: " + theType);
407                }
408
409                // NB: Once this is enabled, we should return true from handlesMissing if
410                // it's a collection theType
411                // if (theInnerCollectionType != null) {
412                // this.parser = new CollectionBinder(this.parser, theInnerCollectionType);
413                // }
414                //
415                // if (theOuterCollectionType != null) {
416                // this.parser = new CollectionBinder(this.parser, theOuterCollectionType);
417                // }
418
419        }
420
421        @Override
422        public String toString() {
423                ToStringBuilder retVal = new ToStringBuilder(this);
424                retVal.append("name", myName);
425                retVal.append("required", myRequired);
426                return retVal.toString();
427        }
428}