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