001/*
002 * #%L
003 * HAPI FHIR - Client 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.client.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.StringAndListParam;
056import ca.uhn.fhir.rest.param.StringOrListParam;
057import ca.uhn.fhir.rest.param.StringParam;
058import ca.uhn.fhir.rest.param.TokenAndListParam;
059import ca.uhn.fhir.rest.param.TokenOrListParam;
060import ca.uhn.fhir.rest.param.TokenParam;
061import ca.uhn.fhir.rest.param.UriAndListParam;
062import ca.uhn.fhir.rest.param.UriOrListParam;
063import ca.uhn.fhir.rest.param.UriParam;
064import ca.uhn.fhir.rest.param.binder.CalendarBinder;
065import ca.uhn.fhir.rest.param.binder.DateBinder;
066import ca.uhn.fhir.rest.param.binder.FhirPrimitiveBinder;
067import ca.uhn.fhir.rest.param.binder.IParamBinder;
068import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
069import ca.uhn.fhir.rest.param.binder.QueryParameterOrBinder;
070import ca.uhn.fhir.rest.param.binder.QueryParameterTypeBinder;
071import ca.uhn.fhir.rest.param.binder.StringBinder;
072import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
073import ca.uhn.fhir.util.CollectionUtil;
074import ca.uhn.fhir.util.ReflectionUtil;
075import org.apache.commons.lang3.builder.ToStringBuilder;
076import org.hl7.fhir.instance.model.api.IBaseResource;
077import org.hl7.fhir.instance.model.api.IPrimitiveType;
078
079import java.util.ArrayList;
080import java.util.Arrays;
081import java.util.Calendar;
082import java.util.Collection;
083import java.util.Collections;
084import java.util.Date;
085import java.util.HashMap;
086import java.util.HashSet;
087import java.util.List;
088import java.util.Set;
089
090public class SearchParameter extends BaseQueryParameter {
091
092        private static final String EMPTY_STRING = "";
093        private static HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers;
094        private static HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes;
095        static final String QUALIFIER_ANY_TYPE = ":*";
096
097        static {
098                ourParamTypes = new HashMap<>();
099                ourParamQualifiers = new HashMap<>();
100
101                ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING);
102                ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING);
103                ourParamTypes.put(StringAndListParam.class, RestSearchParameterTypeEnum.STRING);
104                ourParamQualifiers.put(
105                                RestSearchParameterTypeEnum.STRING,
106                                CollectionUtil.newSet(
107                                                Constants.PARAMQUALIFIER_STRING_EXACT,
108                                                Constants.PARAMQUALIFIER_STRING_CONTAINS,
109                                                Constants.PARAMQUALIFIER_MISSING,
110                                                EMPTY_STRING));
111
112                ourParamTypes.put(UriParam.class, RestSearchParameterTypeEnum.URI);
113                ourParamTypes.put(UriOrListParam.class, RestSearchParameterTypeEnum.URI);
114                ourParamTypes.put(UriAndListParam.class, RestSearchParameterTypeEnum.URI);
115                // TODO: are these right for URI?
116                ourParamQualifiers.put(
117                                RestSearchParameterTypeEnum.URI,
118                                CollectionUtil.newSet(
119                                                Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
120
121                ourParamTypes.put(TokenParam.class, RestSearchParameterTypeEnum.TOKEN);
122                ourParamTypes.put(TokenOrListParam.class, RestSearchParameterTypeEnum.TOKEN);
123                ourParamTypes.put(TokenAndListParam.class, RestSearchParameterTypeEnum.TOKEN);
124                ourParamQualifiers.put(
125                                RestSearchParameterTypeEnum.TOKEN,
126                                CollectionUtil.newSet(
127                                                Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING));
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                                CollectionUtil.newSet(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                                CollectionUtil.newSet(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                                CollectionUtil.newSet(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, CollectionUtil.newSet(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                                CollectionUtil.newSet(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
170        private List<Class<? extends IQueryParameterType>> myCompositeTypes = Collections.emptyList();
171        private List<Class<? extends IBaseResource>> myDeclaredTypes;
172        private String myName;
173        private IParamBinder<?> myParamBinder;
174        private RestSearchParameterTypeEnum myParamType;
175        private Set<String> myQualifierWhitelist;
176        private boolean myRequired;
177        private Class<?> myType;
178
179        public SearchParameter() {}
180
181        public SearchParameter(String theName, boolean theRequired) {
182                this.myName = theName;
183                this.myRequired = theRequired;
184        }
185
186        /*
187         * (non-Javadoc)
188         *
189         * @see ca.uhn.fhir.rest.param.IParameter#encode(java.lang.Object)
190         */
191        @Override
192        public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
193                ArrayList<QualifiedParamList> retVal = new ArrayList<>();
194
195                // TODO: declaring method should probably have a generic type..
196                @SuppressWarnings("rawtypes")
197                IParamBinder paramBinder = myParamBinder;
198
199                @SuppressWarnings("unchecked")
200                List<IQueryParameterOr<?>> val = paramBinder.encode(theContext, theObject);
201                for (IQueryParameterOr<?> nextOr : val) {
202                        retVal.add(new QualifiedParamList(nextOr, theContext));
203                }
204
205                return retVal;
206        }
207
208        /*
209         * (non-Javadoc)
210         *
211         * @see ca.uhn.fhir.rest.param.IParameter#getName()
212         */
213        @Override
214        public String getName() {
215                return myName;
216        }
217
218        @Override
219        public RestSearchParameterTypeEnum getParamType() {
220                return myParamType;
221        }
222
223        public Class<?> getType() {
224                return myType;
225        }
226
227        @Override
228        public boolean isRequired() {
229                return myRequired;
230        }
231
232        public void setChainlists(String[] theChainWhitelist) {
233                myQualifierWhitelist = new HashSet<>(theChainWhitelist.length);
234                myQualifierWhitelist.add(QUALIFIER_ANY_TYPE);
235
236                for (String chain : theChainWhitelist) {
237                        if (chain.equals(OptionalParam.ALLOW_CHAIN_ANY)) {
238                                myQualifierWhitelist.add('.' + OptionalParam.ALLOW_CHAIN_ANY);
239                        } else if (chain.equals(EMPTY_STRING)) {
240                                myQualifierWhitelist.add(".");
241                        } else {
242                                myQualifierWhitelist.add('.' + chain);
243                        }
244                }
245        }
246
247        public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) {
248                myCompositeTypes = Arrays.asList(theCompositeTypes);
249        }
250
251        public void setDeclaredTypes(Class<? extends IBaseResource>[] theTypes) {
252                myDeclaredTypes = Arrays.asList(theTypes);
253        }
254
255        public void setName(String name) {
256                this.myName = name;
257        }
258
259        public void setRequired(boolean required) {
260                this.myRequired = required;
261        }
262
263        @SuppressWarnings("unchecked")
264        public void setType(
265                        FhirContext theContext,
266                        final Class<?> type,
267                        Class<? extends Collection<?>> theInnerCollectionType,
268                        Class<? extends Collection<?>> theOuterCollectionType) {
269
270                this.myType = type;
271                if (IQueryParameterType.class.isAssignableFrom(type)) {
272                        myParamBinder = new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) type, myCompositeTypes);
273                } else if (IQueryParameterOr.class.isAssignableFrom(type)) {
274                        myParamBinder = new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) type, myCompositeTypes);
275                } else if (IQueryParameterAnd.class.isAssignableFrom(type)) {
276                        myParamBinder =
277                                        new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) type, myCompositeTypes);
278                } else if (String.class.equals(type)) {
279                        myParamBinder = new StringBinder();
280                        myParamType = RestSearchParameterTypeEnum.STRING;
281                } else if (Date.class.equals(type)) {
282                        myParamBinder = new DateBinder();
283                        myParamType = RestSearchParameterTypeEnum.DATE;
284                } else if (Calendar.class.equals(type)) {
285                        myParamBinder = new CalendarBinder();
286                        myParamType = RestSearchParameterTypeEnum.DATE;
287                } else if (IPrimitiveType.class.isAssignableFrom(type) && ReflectionUtil.isInstantiable(type)) {
288                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition)
289                                        theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) type);
290                        if (def.getNativeType() != null) {
291                                if (def.getNativeType().equals(Date.class)) {
292                                        myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) type);
293                                        myParamType = RestSearchParameterTypeEnum.DATE;
294                                } else if (def.getNativeType().equals(String.class)) {
295                                        myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) type);
296                                        myParamType = RestSearchParameterTypeEnum.STRING;
297                                }
298                        }
299                } else {
300                        throw new ConfigurationException(
301                                        Msg.code(1406) + "Unsupported data type for parameter: " + type.getCanonicalName());
302                }
303
304                RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(type);
305                if (typeEnum != null) {
306                        Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
307                        if (builtInQualifiers != null) {
308                                if (myQualifierWhitelist != null) {
309                                        HashSet<String> qualifierWhitelist = new HashSet<>();
310                                        qualifierWhitelist.addAll(myQualifierWhitelist);
311                                        qualifierWhitelist.addAll(builtInQualifiers);
312                                        myQualifierWhitelist = qualifierWhitelist;
313                                } else {
314                                        myQualifierWhitelist = Collections.unmodifiableSet(builtInQualifiers);
315                                }
316                        }
317                }
318
319                if (myParamType == null) {
320                        myParamType = typeEnum;
321                }
322
323                if (myParamType != null) {
324                        // ok
325                } else if (StringDt.class.isAssignableFrom(type)) {
326                        myParamType = RestSearchParameterTypeEnum.STRING;
327                } else if (BaseIdentifierDt.class.isAssignableFrom(type)) {
328                        myParamType = RestSearchParameterTypeEnum.TOKEN;
329                } else if (BaseQuantityDt.class.isAssignableFrom(type)) {
330                        myParamType = RestSearchParameterTypeEnum.QUANTITY;
331                } else if (ReferenceParam.class.isAssignableFrom(type)) {
332                        myParamType = RestSearchParameterTypeEnum.REFERENCE;
333                } else if (HasParam.class.isAssignableFrom(type)) {
334                        myParamType = RestSearchParameterTypeEnum.STRING;
335                } else {
336                        throw new ConfigurationException(Msg.code(1407) + "Unknown search parameter type: " + type);
337                }
338
339                // NB: Once this is enabled, we should return true from handlesMissing if
340                // it's a collection type
341                // if (theInnerCollectionType != null) {
342                // this.parser = new CollectionBinder(this.parser, theInnerCollectionType);
343                // }
344                //
345                // if (theOuterCollectionType != null) {
346                // this.parser = new CollectionBinder(this.parser, theOuterCollectionType);
347                // }
348
349        }
350
351        @Override
352        public String toString() {
353                ToStringBuilder retVal = new ToStringBuilder(this);
354                retVal.append("name", myName);
355                retVal.append("required", myRequired);
356                return retVal.toString();
357        }
358}