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