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