001/*
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2025 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 the request body is a Parameters resource, check if it has any
281                // values for us
282                if (theRequest.getRequestType() != RequestTypeEnum.GET
283                                && !method.isManualRequestMode()
284                                && !method.isDeleteEnabled()) {
285                        translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues);
286                }
287
288                // We always look at the URL to see if any matching parameters were provided there
289                translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues);
290
291                if (matchingParamValues.isEmpty()) {
292                        return null;
293                }
294
295                if (myInnerCollectionType == null) {
296                        return matchingParamValues.get(0);
297                }
298
299                Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType);
300                retVal.addAll(matchingParamValues);
301                return retVal;
302        }
303
304        private void translateQueryParametersIntoServerArgumentForGet(
305                        RequestDetails theRequest, List<Object> matchingParamValues) {
306                if (mySearchParameterBinding != null) {
307
308                        List<QualifiedParamList> params = new ArrayList<QualifiedParamList>();
309                        String nameWithQualifierColon = myName + ":";
310
311                        for (String nextParamName : theRequest.getParameters().keySet()) {
312                                String qualifier;
313                                if (nextParamName.equals(myName)) {
314                                        qualifier = null;
315                                } else if (nextParamName.startsWith(nameWithQualifierColon)) {
316                                        qualifier = nextParamName.substring(nextParamName.indexOf(':'));
317                                } else {
318                                        // This is some other parameter, not the one bound by this instance
319                                        continue;
320                                }
321                                String[] values = theRequest.getParameters().get(nextParamName);
322                                if (values != null) {
323                                        for (String nextValue : values) {
324                                                params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue));
325                                        }
326                                }
327                        }
328                        if (!params.isEmpty()) {
329                                for (QualifiedParamList next : params) {
330                                        Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next));
331                                        addValueToList(matchingParamValues, values);
332                                }
333                        }
334
335                } else {
336                        String[] paramValues = theRequest.getParameters().get(myName);
337                        if (paramValues != null && paramValues.length > 0) {
338                                if (myAllowGet) {
339
340                                        if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
341                                                List<QualifiedParamList> parameters = new ArrayList<>();
342                                                parameters.add(QualifiedParamList.singleton(paramValues[0]));
343                                                if (paramValues.length > 1) {
344                                                        parameters.add(QualifiedParamList.singleton(paramValues[1]));
345                                                }
346                                                DateRangeParam dateRangeParam = new DateRangeParam();
347                                                FhirContext ctx = theRequest.getServer().getFhirContext();
348                                                dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters);
349                                                matchingParamValues.add(dateRangeParam);
350
351                                        } else if (IBaseReference.class.isAssignableFrom(myParameterType)) {
352
353                                                processAllCommaSeparatedValues(paramValues, t -> {
354                                                        IBaseReference param = (IBaseReference) ReflectionUtil.newInstance(myParameterType);
355                                                        param.setReference(t);
356                                                        matchingParamValues.add(param);
357                                                });
358
359                                        } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) {
360
361                                                processAllCommaSeparatedValues(paramValues, t -> {
362                                                        TokenParam tokenParam = new TokenParam();
363                                                        tokenParam.setValueAsQueryToken(myContext, myName, null, t);
364
365                                                        IBaseCoding param = (IBaseCoding) ReflectionUtil.newInstance(myParameterType);
366                                                        param.setSystem(tokenParam.getSystem());
367                                                        param.setCode(tokenParam.getValue());
368                                                        matchingParamValues.add(param);
369                                                });
370
371                                        } else if (String.class.isAssignableFrom(myParameterType)) {
372
373                                                matchingParamValues.addAll(Arrays.asList(paramValues));
374
375                                        } else if (ValidationModeEnum.class.equals(myParameterType)) {
376
377                                                if (isNotBlank(paramValues[0])) {
378                                                        ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]);
379                                                        if (validationMode != null) {
380                                                                matchingParamValues.add(validationMode);
381                                                        } else {
382                                                                throwInvalidMode(paramValues[0]);
383                                                        }
384                                                }
385
386                                        } else {
387                                                for (String nextValue : paramValues) {
388                                                        FhirContext ctx = theRequest.getServer().getFhirContext();
389                                                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition)
390                                                                        ctx.getElementDefinition(myParameterType.asSubclass(IBase.class));
391                                                        IPrimitiveType<?> instance = def.newInstance();
392                                                        instance.setValueAsString(nextValue);
393                                                        matchingParamValues.add(instance);
394                                                }
395                                        }
396                                } else {
397                                        HapiLocalizer localizer =
398                                                        theRequest.getServer().getFhirContext().getLocalizer();
399                                        String msg = localizer.getMessage(
400                                                        OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName);
401                                        throw new MethodNotAllowedException(Msg.code(363) + msg, RequestTypeEnum.POST);
402                                }
403                        }
404                }
405        }
406
407        /**
408         * This method is here to mediate between the POST form of operation parameters (i.e. elements within a <code>Parameters</code>
409         * resource) and the GET form (i.e. URL parameters).
410         * <p>
411         * Essentially we want to allow comma-separated values as is done with searches on URLs.
412         * </p>
413         */
414        private void processAllCommaSeparatedValues(String[] theParamValues, Consumer<String> theHandler) {
415                for (String nextValue : theParamValues) {
416                        QualifiedParamList qualifiedParamList =
417                                        QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextValue);
418                        for (String nextSplitValue : qualifiedParamList) {
419                                theHandler.accept(nextSplitValue);
420                        }
421                }
422        }
423
424        private void translateQueryParametersIntoServerArgumentForPost(
425                        RequestDetails theRequest, List<Object> matchingParamValues) {
426                IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY);
427                if (requestContents != null) {
428                        RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents);
429                        if (def.getName().equals("Parameters")) {
430
431                                BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
432                                BaseRuntimeElementCompositeDefinition<?> paramChildElem =
433                                                (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
434
435                                RuntimeChildPrimitiveDatatypeDefinition nameChild =
436                                                (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name");
437                                BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]");
438                                BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource");
439
440                                IAccessor paramChildAccessor = paramChild.getAccessor();
441                                List<IBase> values = paramChildAccessor.getValues(requestContents);
442                                for (IBase nextParameter : values) {
443                                        List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter);
444                                        if (nextNames != null && nextNames.size() > 0) {
445                                                IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0);
446                                                if (myName.equals(nextName.getValueAsString())) {
447
448                                                        if (myParameterType.isAssignableFrom(nextParameter.getClass())) {
449                                                                matchingParamValues.add(nextParameter);
450                                                        } else {
451                                                                List<IBase> paramValues =
452                                                                                valueChild.getAccessor().getValues(nextParameter);
453                                                                List<IBase> paramResources =
454                                                                                resourceChild.getAccessor().getValues(nextParameter);
455                                                                if (paramValues != null && paramValues.size() > 0) {
456                                                                        tryToAddValues(paramValues, matchingParamValues);
457                                                                } else if (paramResources != null && paramResources.size() > 0) {
458                                                                        tryToAddValues(paramResources, matchingParamValues);
459                                                                }
460                                                        }
461                                                }
462                                        }
463                                }
464
465                        } else {
466
467                                if (myParameterType.isAssignableFrom(requestContents.getClass())) {
468                                        tryToAddValues(Arrays.asList(requestContents), matchingParamValues);
469                                }
470                        }
471                }
472        }
473
474        @SuppressWarnings("unchecked")
475        private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) {
476                for (Object nextValue : theParamValues) {
477                        if (nextValue == null) {
478                                continue;
479                        }
480                        if (myConverter != null) {
481                                nextValue = myConverter.incomingServer(nextValue);
482                        }
483                        if (myParameterType.equals(String.class)) {
484                                if (nextValue instanceof IPrimitiveType<?>) {
485                                        IPrimitiveType<?> source = (IPrimitiveType<?>) nextValue;
486                                        theMatchingParamValues.add(source.getValueAsString());
487                                        continue;
488                                }
489                        }
490                        if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
491                                Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass();
492                                Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType;
493                                BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType);
494                                BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType);
495                                if (targetTypeDef instanceof IRuntimeDatatypeDefinition
496                                                && sourceTypeDef instanceof IRuntimeDatatypeDefinition) {
497                                        IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef;
498                                        if (targetTypeDtDef.isProfileOf(sourceType)) {
499                                                FhirTerser terser = myContext.newTerser();
500                                                IBase newTarget = targetTypeDef.newInstance();
501                                                terser.cloneInto((IBase) nextValue, newTarget, true);
502                                                theMatchingParamValues.add(newTarget);
503                                                continue;
504                                        }
505                                }
506                                throwWrongParamType(nextValue);
507                        }
508
509                        addValueToList(theMatchingParamValues, nextValue);
510                }
511        }
512
513        public String getDescription() {
514                return myDescription;
515        }
516
517        public List<String> getExampleValues() {
518                return myExampleValues;
519        }
520
521        interface IOperationParamConverter {
522
523                Object incomingServer(Object theObject);
524
525                Object outgoingClient(Object theObject);
526        }
527
528        class OperationParamConverter implements IOperationParamConverter {
529
530                public OperationParamConverter() {
531                        Validate.isTrue(mySearchParameterBinding != null);
532                }
533
534                @Override
535                public Object incomingServer(Object theObject) {
536                        IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject;
537                        List<QualifiedParamList> paramList = Collections.singletonList(
538                                        QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString()));
539                        return mySearchParameterBinding.parse(myContext, paramList);
540                }
541
542                @Override
543                public Object outgoingClient(Object theObject) {
544                        IQueryParameterType obj = (IQueryParameterType) theObject;
545                        IPrimitiveType<?> retVal =
546                                        (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance();
547                        retVal.setValueAsString(obj.getValueAsQueryToken(myContext));
548                        return retVal;
549                }
550        }
551
552        public static void throwInvalidMode(String paramValues) {
553                throw new InvalidRequestException(Msg.code(364) + "Invalid mode value: \"" + paramValues + "\"");
554        }
555}