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