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