001package ca.uhn.fhir.rest.server.method;
002
003import ca.uhn.fhir.context.*;
004import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor;
005import ca.uhn.fhir.i18n.HapiLocalizer;
006import ca.uhn.fhir.model.api.IDatatype;
007import ca.uhn.fhir.model.api.IQueryParameterAnd;
008import ca.uhn.fhir.model.api.IQueryParameterOr;
009import ca.uhn.fhir.model.api.IQueryParameterType;
010import ca.uhn.fhir.rest.annotation.OperationParam;
011import ca.uhn.fhir.rest.api.QualifiedParamList;
012import ca.uhn.fhir.rest.api.RequestTypeEnum;
013import ca.uhn.fhir.rest.api.ValidationModeEnum;
014import ca.uhn.fhir.rest.api.server.RequestDetails;
015import ca.uhn.fhir.rest.param.BaseAndListParam;
016import ca.uhn.fhir.rest.param.DateRangeParam;
017import ca.uhn.fhir.rest.param.binder.CollectionBinder;
018import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
019import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
020import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
021import ca.uhn.fhir.util.FhirTerser;
022import ca.uhn.fhir.util.ReflectionUtil;
023import org.apache.commons.lang3.Validate;
024import org.hl7.fhir.instance.model.api.IBase;
025import org.hl7.fhir.instance.model.api.IBaseDatatype;
026import org.hl7.fhir.instance.model.api.IBaseResource;
027import org.hl7.fhir.instance.model.api.IPrimitiveType;
028
029import java.lang.reflect.Method;
030import java.lang.reflect.Modifier;
031import java.util.*;
032
033import static org.apache.commons.lang3.StringUtils.isNotBlank;
034
035/*
036 * #%L
037 * HAPI FHIR - Server Framework
038 * %%
039 * Copyright (C) 2014 - 2019 University Health Network
040 * %%
041 * Licensed under the Apache License, Version 2.0 (the "License");
042 * you may not use this file except in compliance with the License.
043 * You may obtain a copy of the License at
044 * 
045 *      http://www.apache.org/licenses/LICENSE-2.0
046 * 
047 * Unless required by applicable law or agreed to in writing, software
048 * distributed under the License is distributed on an "AS IS" BASIS,
049 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
050 * See the License for the specific language governing permissions and
051 * limitations under the License.
052 * #L%
053 */
054
055public class OperationParameter implements IParameter {
056
057        static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE";
058        @SuppressWarnings("unchecked")
059        private static final Class<? extends IQueryParameterType>[] COMPOSITE_TYPES = new Class[0];
060        private final FhirContext myContext;
061        private final String myName;
062        private final String myOperationName;
063        private boolean myAllowGet;
064        private IOperationParamConverter myConverter;
065        @SuppressWarnings("rawtypes")
066        private Class<? extends Collection> myInnerCollectionType;
067        private int myMax;
068        private int myMin;
069        private Class<?> myParameterType;
070        private String myParamType;
071        private SearchParameter mySearchParameterBinding;
072
073        public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) {
074                this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max());
075        }
076
077        OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) {
078                myOperationName = theOperationName;
079                myName = theParameterName;
080                myMin = theMin;
081                myMax = theMax;
082                myContext = theCtx;
083        }
084
085        @SuppressWarnings({"rawtypes", "unchecked"})
086        private void addValueToList(List<Object> matchingParamValues, Object values) {
087                if (values != null) {
088                        if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) {
089                                BaseAndListParam existing = (BaseAndListParam<?>) matchingParamValues.get(0);
090                                BaseAndListParam<?> newAndList = (BaseAndListParam<?>) values;
091                                for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) {
092                                        existing.addAnd(nextAnd);
093                                }
094                        } else {
095                                matchingParamValues.add(values);
096                        }
097                }
098        }
099
100        protected FhirContext getContext() {
101                return myContext;
102        }
103
104        public int getMax() {
105                return myMax;
106        }
107
108        public int getMin() {
109                return myMin;
110        }
111
112        public String getName() {
113                return myName;
114        }
115
116        public String getParamType() {
117                return myParamType;
118        }
119
120        public String getSearchParamType() {
121                if (mySearchParameterBinding != null) {
122                        return mySearchParameterBinding.getParamType().getCode();
123                }
124                return null;
125        }
126
127        @SuppressWarnings("unchecked")
128        @Override
129        public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
130                if (getContext().getVersion().getVersion().isRi()) {
131                        if (IDatatype.class.isAssignableFrom(theParameterType)) {
132                                throw new ConfigurationException("Incorrect use of type " + theParameterType.getSimpleName() + " as parameter type for method when context is for version " + getContext().getVersion().getVersion().name() + " in method: " + theMethod.toString());
133                        }
134                }
135
136                myParameterType = theParameterType;
137                if (theInnerCollectionType != null) {
138                        myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName);
139                        if (myMax == OperationParam.MAX_DEFAULT) {
140                                myMax = OperationParam.MAX_UNLIMITED;
141                        }
142                } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) {
143                        if (myMax == OperationParam.MAX_DEFAULT) {
144                                myMax = OperationParam.MAX_UNLIMITED;
145                        }
146                } else {
147                        if (myMax == OperationParam.MAX_DEFAULT) {
148                                myMax = 1;
149                        }
150                }
151
152                boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers());
153
154                //@formatter:off
155                boolean isSearchParam =
156                        IQueryParameterType.class.isAssignableFrom(myParameterType) ||
157                                IQueryParameterOr.class.isAssignableFrom(myParameterType) ||
158                                IQueryParameterAnd.class.isAssignableFrom(myParameterType);
159                //@formatter:off
160
161                /*
162                 * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also
163                 * extend this interface. I'm not sure if they should in the end.. but they do, so we
164                 * exclude them.
165                 */
166                isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType);
167
168                myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) || isSearchParam || ValidationModeEnum.class.equals(myParameterType);
169
170                /*
171                 * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We
172                 * should probably clean this up..
173                 */
174                if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) {
175                        if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) {
176                                myParamType = "Resource";
177                        } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
178                                myParamType = "date";
179                                myMax = 2;
180                                myAllowGet = true;
181                        } else if (myParameterType.equals(ValidationModeEnum.class)) {
182                                myParamType = "code";
183                        } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) {
184                                myParamType = myContext.getElementDefinition((Class<? extends IBase>) myParameterType).getName();
185                        } else if (isSearchParam) {
186                                myParamType = "string";
187                                mySearchParameterBinding = new SearchParameter(myName, myMin > 0);
188                                mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES);
189                                mySearchParameterBinding.setType(myContext, theParameterType, theInnerCollectionType, theOuterCollectionType);
190                                myConverter = new OperationParamConverter();
191                        } else {
192                                throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName());
193                        }
194
195                }
196
197        }
198
199        public OperationParameter setConverter(IOperationParamConverter theConverter) {
200                myConverter = theConverter;
201                return this;
202        }
203
204        private void throwWrongParamType(Object nextValue) {
205                throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName());
206        }
207
208        @SuppressWarnings("unchecked")
209        @Override
210        public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
211                List<Object> matchingParamValues = new ArrayList<Object>();
212
213                if (theRequest.getRequestType() == RequestTypeEnum.GET) {
214                        translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues);
215                } else {
216                        translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues);
217                }
218
219                if (matchingParamValues.isEmpty()) {
220                        return null;
221                }
222
223                if (myInnerCollectionType == null) {
224                        return matchingParamValues.get(0);
225                }
226
227                Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType);
228                retVal.addAll(matchingParamValues);
229                return retVal;
230        }
231
232        private void translateQueryParametersIntoServerArgumentForGet(RequestDetails theRequest, List<Object> matchingParamValues) {
233                if (mySearchParameterBinding != null) {
234
235                        List<QualifiedParamList> params = new ArrayList<QualifiedParamList>();
236                        String nameWithQualifierColon = myName + ":";
237
238                        for (String nextParamName : theRequest.getParameters().keySet()) {
239                                String qualifier;
240                                if (nextParamName.equals(myName)) {
241                                        qualifier = null;
242                                } else if (nextParamName.startsWith(nameWithQualifierColon)) {
243                                        qualifier = nextParamName.substring(nextParamName.indexOf(':'));
244                                } else {
245                                        // This is some other parameter, not the one bound by this instance
246                                        continue;
247                                }
248                                String[] values = theRequest.getParameters().get(nextParamName);
249                                if (values != null) {
250                                        for (String nextValue : values) {
251                                                params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue));
252                                        }
253                                }
254                        }
255                        if (!params.isEmpty()) {
256                                for (QualifiedParamList next : params) {
257                                        Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next));
258                                        addValueToList(matchingParamValues, values);
259                                }
260
261                        }
262
263                } else {
264                        String[] paramValues = theRequest.getParameters().get(myName);
265                        if (paramValues != null && paramValues.length > 0) {
266                                if (myAllowGet) {
267
268                                        if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
269                                                List<QualifiedParamList> parameters = new ArrayList<QualifiedParamList>();
270                                                parameters.add(QualifiedParamList.singleton(paramValues[0]));
271                                                if (paramValues.length > 1) {
272                                                        parameters.add(QualifiedParamList.singleton(paramValues[1]));
273                                                }
274                                                DateRangeParam dateRangeParam = new DateRangeParam();
275                                                FhirContext ctx = theRequest.getServer().getFhirContext();
276                                                dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters);
277                                                matchingParamValues.add(dateRangeParam);
278                                        } else if (String.class.isAssignableFrom(myParameterType)) {
279
280                                                for (String next : paramValues) {
281                                                        matchingParamValues.add(next);
282                                                }
283                                        } else if (ValidationModeEnum.class.equals(myParameterType)) {
284
285                                                if (isNotBlank(paramValues[0])) {
286                                                        ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]);
287                                                        if (validationMode != null) {
288                                                                matchingParamValues.add(validationMode);
289                                                        } else {
290                                                                throwInvalidMode(paramValues[0]);
291                                                        }
292                                                }
293
294                                        } else {
295                                                for (String nextValue : paramValues) {
296                                                        FhirContext ctx = theRequest.getServer().getFhirContext();
297                                                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx.getElementDefinition(myParameterType.asSubclass(IBase.class));
298                                                        IPrimitiveType<?> instance = def.newInstance();
299                                                        instance.setValueAsString(nextValue);
300                                                        matchingParamValues.add(instance);
301                                                }
302                                        }
303                                } else {
304                                        HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer();
305                                        String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName);
306                                        throw new MethodNotAllowedException(msg, RequestTypeEnum.POST);
307                                }
308                        }
309                }
310        }
311
312        private void translateQueryParametersIntoServerArgumentForPost(RequestDetails theRequest, List<Object> matchingParamValues) {
313                IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY);
314                RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents);
315                if (def.getName().equals("Parameters")) {
316
317                        BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
318                        BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
319
320                        RuntimeChildPrimitiveDatatypeDefinition nameChild = (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name");
321                        BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]");
322                        BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource");
323
324                        IAccessor paramChildAccessor = paramChild.getAccessor();
325                        List<IBase> values = paramChildAccessor.getValues(requestContents);
326                        for (IBase nextParameter : values) {
327                                List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter);
328                                if (nextNames != null && nextNames.size() > 0) {
329                                        IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0);
330                                        if (myName.equals(nextName.getValueAsString())) {
331
332                                                if (myParameterType.isAssignableFrom(nextParameter.getClass())) {
333                                                        matchingParamValues.add(nextParameter);
334                                                } else {
335                                                        List<IBase> paramValues = valueChild.getAccessor().getValues(nextParameter);
336                                                        List<IBase> paramResources = resourceChild.getAccessor().getValues(nextParameter);
337                                                        if (paramValues != null && paramValues.size() > 0) {
338                                                                tryToAddValues(paramValues, matchingParamValues);
339                                                        } else if (paramResources != null && paramResources.size() > 0) {
340                                                                tryToAddValues(paramResources, matchingParamValues);
341                                                        }
342                                                }
343
344                                        }
345                                }
346                        }
347
348                } else {
349
350                        if (myParameterType.isAssignableFrom(requestContents.getClass())) {
351                                tryToAddValues(Arrays.asList((IBase) requestContents), matchingParamValues);
352                        }
353
354                }
355        }
356
357        @SuppressWarnings("unchecked")
358        private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) {
359                for (Object nextValue : theParamValues) {
360                        if (nextValue == null) {
361                                continue;
362                        }
363                        if (myConverter != null) {
364                                nextValue = myConverter.incomingServer(nextValue);
365                        }
366                        if (myParameterType.equals(String.class)) {
367                                if (nextValue instanceof IPrimitiveType<?>) {
368                                        IPrimitiveType<?> source = (IPrimitiveType<?>) nextValue;
369                                        theMatchingParamValues.add(source.getValueAsString());
370                                        continue;
371                                }
372                        }
373                        if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
374                                Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass();
375                                Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType;
376                                BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType);
377                                BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType);
378                                if (targetTypeDef instanceof IRuntimeDatatypeDefinition && sourceTypeDef instanceof IRuntimeDatatypeDefinition) {
379                                        IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef;
380                                        if (targetTypeDtDef.isProfileOf(sourceType)) {
381                                                FhirTerser terser = myContext.newTerser();
382                                                IBase newTarget = targetTypeDef.newInstance();
383                                                terser.cloneInto((IBase) nextValue, newTarget, true);
384                                                theMatchingParamValues.add(newTarget);
385                                                continue;
386                                        }
387                                }
388                                throwWrongParamType(nextValue);
389                        }
390
391                        addValueToList(theMatchingParamValues, nextValue);
392                }
393        }
394
395        public static void throwInvalidMode(String paramValues) {
396                throw new InvalidRequestException("Invalid mode value: \"" + paramValues + "\"");
397        }
398
399        interface IOperationParamConverter {
400
401                Object incomingServer(Object theObject);
402
403                Object outgoingClient(Object theObject);
404
405        }
406
407        class OperationParamConverter implements IOperationParamConverter {
408
409                public OperationParamConverter() {
410                        Validate.isTrue(mySearchParameterBinding != null);
411                }
412
413                @Override
414                public Object incomingServer(Object theObject) {
415                        IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject;
416                        List<QualifiedParamList> paramList = Collections.singletonList(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString()));
417                        return mySearchParameterBinding.parse(myContext, paramList);
418                }
419
420                @Override
421                public Object outgoingClient(Object theObject) {
422                        IQueryParameterType obj = (IQueryParameterType) theObject;
423                        IPrimitiveType<?> retVal = (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance();
424                        retVal.setValueAsString(obj.getValueAsQueryToken(myContext));
425                        return retVal;
426                }
427
428        }
429
430
431}