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.BaseRuntimeElementDefinition;
023import ca.uhn.fhir.context.ConfigurationException;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
027import ca.uhn.fhir.model.api.Include;
028import ca.uhn.fhir.model.api.TagList;
029import ca.uhn.fhir.model.api.annotation.Description;
030import ca.uhn.fhir.rest.annotation.At;
031import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
032import ca.uhn.fhir.rest.annotation.Count;
033import ca.uhn.fhir.rest.annotation.Elements;
034import ca.uhn.fhir.rest.annotation.GraphQLQueryBody;
035import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl;
036import ca.uhn.fhir.rest.annotation.IdParam;
037import ca.uhn.fhir.rest.annotation.IncludeParam;
038import ca.uhn.fhir.rest.annotation.Offset;
039import ca.uhn.fhir.rest.annotation.Operation;
040import ca.uhn.fhir.rest.annotation.OperationParam;
041import ca.uhn.fhir.rest.annotation.OptionalParam;
042import ca.uhn.fhir.rest.annotation.Patch;
043import ca.uhn.fhir.rest.annotation.RawParam;
044import ca.uhn.fhir.rest.annotation.RequiredParam;
045import ca.uhn.fhir.rest.annotation.ResourceParam;
046import ca.uhn.fhir.rest.annotation.ServerBase;
047import ca.uhn.fhir.rest.annotation.Since;
048import ca.uhn.fhir.rest.annotation.Sort;
049import ca.uhn.fhir.rest.annotation.TransactionParam;
050import ca.uhn.fhir.rest.annotation.Validate;
051import ca.uhn.fhir.rest.api.Constants;
052import ca.uhn.fhir.rest.api.EncodingEnum;
053import ca.uhn.fhir.rest.api.PatchTypeEnum;
054import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
055import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
056import ca.uhn.fhir.rest.api.SummaryEnum;
057import ca.uhn.fhir.rest.api.ValidationModeEnum;
058import ca.uhn.fhir.rest.api.server.RequestDetails;
059import ca.uhn.fhir.rest.param.binder.CollectionBinder;
060import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter;
061import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode;
062import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
063import ca.uhn.fhir.util.ParametersUtil;
064import ca.uhn.fhir.util.ReflectionUtil;
065import jakarta.servlet.ServletRequest;
066import jakarta.servlet.ServletResponse;
067import org.hl7.fhir.instance.model.api.IBaseResource;
068import org.hl7.fhir.instance.model.api.IPrimitiveType;
069
070import java.lang.annotation.Annotation;
071import java.lang.reflect.Method;
072import java.util.ArrayList;
073import java.util.Collection;
074import java.util.Date;
075import java.util.List;
076
077import static org.apache.commons.lang3.StringUtils.isNotBlank;
078
079public class MethodUtil {
080
081        /**
082         * Non instantiable
083         */
084        private MethodUtil() {
085                // nothing
086        }
087
088        public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) {
089                for (Annotation annotation : theAnnotations) {
090                        if (annotation instanceof Description) {
091                                Description desc = (Description) annotation;
092                                String description = ParametersUtil.extractDescription(desc);
093                                theParameter.setDescription(description);
094                        }
095                }
096        }
097
098        @SuppressWarnings("unchecked")
099        public static List<IParameter> getResourceParameters(
100                        final FhirContext theContext, Method theMethod, Object theProvider) {
101                List<IParameter> parameters = new ArrayList<>();
102
103                Class<?>[] parameterTypes = theMethod.getParameterTypes();
104                int paramIndex = 0;
105                for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) {
106
107                        IParameter param = null;
108                        Class<?> declaredParameterType = parameterTypes[paramIndex];
109                        Class<?> parameterType = declaredParameterType;
110                        Class<? extends java.util.Collection<?>> outerCollectionType = null;
111                        Class<? extends java.util.Collection<?>> innerCollectionType = null;
112                        if (TagList.class.isAssignableFrom(parameterType)) {
113                                // TagList is handled directly within the method bindings
114                                param = new NullParameter();
115                        } else {
116                                if (Collection.class.isAssignableFrom(parameterType)) {
117                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
118                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
119                                        if (parameterType == null && theMethod.getDeclaringClass().isSynthetic()) {
120                                                try {
121                                                        theMethod = theMethod
122                                                                        .getDeclaringClass()
123                                                                        .getSuperclass()
124                                                                        .getMethod(theMethod.getName(), parameterTypes);
125                                                        parameterType =
126                                                                        ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
127                                                } catch (NoSuchMethodException e) {
128                                                        throw new ConfigurationException(Msg.code(400) + "A method with name '"
129                                                                        + theMethod.getName() + "' does not exist for super class '"
130                                                                        + theMethod.getDeclaringClass().getSuperclass() + "'");
131                                                }
132                                        }
133                                        declaredParameterType = parameterType;
134                                }
135                                if (Collection.class.isAssignableFrom(parameterType)) {
136                                        outerCollectionType = innerCollectionType;
137                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
138                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
139                                        declaredParameterType = parameterType;
140                                }
141                                if (Collection.class.isAssignableFrom(parameterType)) {
142                                        throw new ConfigurationException(
143                                                        Msg.code(401) + "Argument #" + paramIndex + " of Method '" + theMethod.getName()
144                                                                        + "' in type '"
145                                                                        + theMethod.getDeclaringClass().getCanonicalName()
146                                                                        + "' is of an invalid generic type (can not be a collection of a collection of a collection)");
147                                }
148
149                                /*
150                                 * If the user is trying to bind IPrimitiveType they are probably
151                                 * trying to write code that is compatible across versions of FHIR.
152                                 * We'll try and come up with an appropriate subtype to give
153                                 * them.
154                                 *
155                                 * This gets tested in HistoryR4Test
156                                 */
157                                if (IPrimitiveType.class.equals(parameterType)) {
158                                        Class<?> genericType =
159                                                        ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
160                                        if (Date.class.equals(genericType)) {
161                                                BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime");
162                                                parameterType = dateTimeDef.getImplementingClass();
163                                        } else if (String.class.equals(genericType) || genericType == null) {
164                                                BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string");
165                                                parameterType = dateTimeDef.getImplementingClass();
166                                        }
167                                }
168                        }
169
170                        if (ServletRequest.class.isAssignableFrom(parameterType)) {
171                                param = new ServletRequestParameter();
172                        } else if (ServletResponse.class.isAssignableFrom(parameterType)) {
173                                param = new ServletResponseParameter();
174                        } else if (parameterType.equals(RequestDetails.class)
175                                        || parameterType.equals(ServletRequestDetails.class)) {
176                                param = new RequestDetailsParameter();
177                        } else if (parameterType.equals(IInterceptorBroadcaster.class)) {
178                                param = new InterceptorBroadcasterParameter();
179                        } else if (parameterType.equals(SummaryEnum.class)) {
180                                param = new SummaryEnumParameter();
181                        } else if (parameterType.equals(PatchTypeEnum.class)) {
182                                param = new PatchTypeParameter();
183                        } else if (parameterType.equals(SearchContainedModeEnum.class)) {
184                                param = new SearchContainedModeParameter();
185                        } else if (parameterType.equals(SearchTotalModeEnum.class)) {
186                                param = new SearchTotalModeParameter();
187                        } else {
188                                for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) {
189                                        Annotation nextAnnotation = nextParameterAnnotations[i];
190
191                                        if (nextAnnotation instanceof RequiredParam) {
192                                                SearchParameter parameter = new SearchParameter();
193                                                parameter.setName(((RequiredParam) nextAnnotation).name());
194                                                parameter.setRequired(true);
195                                                parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes());
196                                                parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes());
197                                                parameter.setChainLists(
198                                                                ((RequiredParam) nextAnnotation).chainWhitelist(),
199                                                                ((RequiredParam) nextAnnotation).chainBlacklist());
200                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
201                                                MethodUtil.extractDescription(parameter, nextParameterAnnotations);
202                                                param = parameter;
203                                        } else if (nextAnnotation instanceof OptionalParam) {
204                                                SearchParameter parameter = new SearchParameter();
205                                                parameter.setName(((OptionalParam) nextAnnotation).name());
206                                                parameter.setRequired(false);
207                                                parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes());
208                                                parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes());
209                                                parameter.setChainLists(
210                                                                ((OptionalParam) nextAnnotation).chainWhitelist(),
211                                                                ((OptionalParam) nextAnnotation).chainBlacklist());
212                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
213                                                MethodUtil.extractDescription(parameter, nextParameterAnnotations);
214                                                param = parameter;
215                                        } else if (nextAnnotation instanceof RawParam) {
216                                                param = new RawParamsParameter(parameters);
217                                        } else if (nextAnnotation instanceof IncludeParam) {
218                                                Class<? extends Collection<Include>> instantiableCollectionType;
219                                                Class<?> specType;
220
221                                                if (parameterType == String.class) {
222                                                        instantiableCollectionType = null;
223                                                        specType = String.class;
224                                                } else if ((parameterType != Include.class)
225                                                                || innerCollectionType == null
226                                                                || outerCollectionType != null) {
227                                                        throw new ConfigurationException(Msg.code(402) + "Method '" + theMethod.getName()
228                                                                        + "' is annotated with @" + IncludeParam.class.getSimpleName()
229                                                                        + " but has a type other than Collection<" + Include.class.getSimpleName() + ">");
230                                                } else {
231                                                        instantiableCollectionType = (Class<? extends Collection<Include>>)
232                                                                        CollectionBinder.getInstantiableCollectionType(
233                                                                                        innerCollectionType, "Method '" + theMethod.getName() + "'");
234                                                        specType = parameterType;
235                                                }
236
237                                                param = new IncludeParameter(
238                                                                (IncludeParam) nextAnnotation, instantiableCollectionType, specType);
239                                        } else if (nextAnnotation instanceof ResourceParam) {
240                                                Mode mode;
241                                                if (IBaseResource.class.isAssignableFrom(parameterType)) {
242                                                        mode = Mode.RESOURCE;
243                                                } else if (String.class.equals(parameterType)) {
244                                                        mode = ResourceParameter.Mode.BODY;
245                                                } else if (byte[].class.equals(parameterType)) {
246                                                        mode = ResourceParameter.Mode.BODY_BYTE_ARRAY;
247                                                } else if (EncodingEnum.class.equals(parameterType)) {
248                                                        mode = Mode.ENCODING;
249                                                } else {
250                                                        StringBuilder b = new StringBuilder();
251                                                        b.append("Method '");
252                                                        b.append(theMethod.getName());
253                                                        b.append("' is annotated with @");
254                                                        b.append(ResourceParam.class.getSimpleName());
255                                                        b.append(" but has a type that is not an implementation of ");
256                                                        b.append(IBaseResource.class.getCanonicalName());
257                                                        b.append(" or String or byte[]");
258                                                        throw new ConfigurationException(Msg.code(403) + b.toString());
259                                                }
260                                                boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null;
261                                                boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null;
262                                                param = new ResourceParameter(
263                                                                (Class<? extends IBaseResource>) parameterType,
264                                                                theProvider,
265                                                                mode,
266                                                                methodIsOperation,
267                                                                methodIsPatch);
268                                        } else if (nextAnnotation instanceof IdParam) {
269                                                param = new NullParameter();
270                                        } else if (nextAnnotation instanceof ServerBase) {
271                                                param = new ServerBaseParamBinder();
272                                        } else if (nextAnnotation instanceof Elements) {
273                                                param = new ElementsParameter();
274                                        } else if (nextAnnotation instanceof Since) {
275                                                param = new SinceParameter();
276                                                ((SinceParameter) param)
277                                                                .setType(theContext, parameterType, innerCollectionType, outerCollectionType);
278                                        } else if (nextAnnotation instanceof At) {
279                                                param = new AtParameter();
280                                                ((AtParameter) param)
281                                                                .setType(theContext, parameterType, innerCollectionType, outerCollectionType);
282                                        } else if (nextAnnotation instanceof Count) {
283                                                param = new CountParameter();
284                                        } else if (nextAnnotation instanceof Offset) {
285                                                param = new OffsetParameter();
286                                        } else if (nextAnnotation instanceof GraphQLQueryUrl) {
287                                                param = new GraphQLQueryUrlParameter();
288                                        } else if (nextAnnotation instanceof GraphQLQueryBody) {
289                                                param = new GraphQLQueryBodyParameter();
290                                        } else if (nextAnnotation instanceof Sort) {
291                                                param = new SortParameter(theContext);
292                                        } else if (nextAnnotation instanceof TransactionParam) {
293                                                param = new TransactionParameter(theContext);
294                                        } else if (nextAnnotation instanceof ConditionalUrlParam) {
295                                                param = new ConditionalParamBinder(((ConditionalUrlParam) nextAnnotation).supportsMultiple());
296                                        } else if (nextAnnotation instanceof OperationParam) {
297                                                Operation op = theMethod.getAnnotation(Operation.class);
298                                                if (op == null) {
299                                                        throw new ConfigurationException(Msg.code(404)
300                                                                        + "@OperationParam detected on method that is not annotated with @Operation: "
301                                                                        + theMethod.toGenericString());
302                                                }
303
304                                                OperationParam operationParam = (OperationParam) nextAnnotation;
305                                                String description = ParametersUtil.extractDescription(nextParameterAnnotations);
306                                                List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);
307                                                ;
308                                                param = new OperationParameter(
309                                                                theContext,
310                                                                op.name(),
311                                                                operationParam.name(),
312                                                                operationParam.min(),
313                                                                operationParam.max(),
314                                                                description,
315                                                                examples);
316                                                if (isNotBlank(operationParam.typeName())) {
317                                                        BaseRuntimeElementDefinition<?> elementDefinition =
318                                                                        theContext.getElementDefinition(operationParam.typeName());
319                                                        if (elementDefinition == null) {
320                                                                elementDefinition = theContext.getResourceDefinition(operationParam.typeName());
321                                                        }
322                                                        org.apache.commons.lang3.Validate.notNull(
323                                                                        elementDefinition,
324                                                                        "Unknown type name in @OperationParam: typeName=\"%s\"",
325                                                                        operationParam.typeName());
326
327                                                        Class<?> newParameterType = elementDefinition.getImplementingClass();
328                                                        if (!declaredParameterType.isAssignableFrom(newParameterType)) {
329                                                                throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\""
330                                                                                + operationParam.typeName() + "\" specified on method " + theMethod);
331                                                        }
332                                                        parameterType = newParameterType;
333                                                }
334                                        } else if (nextAnnotation instanceof Validate.Mode) {
335                                                if (parameterType.equals(ValidationModeEnum.class) == false) {
336                                                        throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @"
337                                                                        + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName()
338                                                                        + " must be of type " + ValidationModeEnum.class.getName());
339                                                }
340                                                String description = ParametersUtil.extractDescription(nextParameterAnnotations);
341                                                List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);
342                                                param = new OperationParameter(
343                                                                                theContext,
344                                                                                Constants.EXTOP_VALIDATE,
345                                                                                Constants.EXTOP_VALIDATE_MODE,
346                                                                                0,
347                                                                                1,
348                                                                                description,
349                                                                                examples)
350                                                                .setConverter(new IOperationParamConverter() {
351                                                                        @Override
352                                                                        public Object incomingServer(Object theObject) {
353                                                                                if (isNotBlank(theObject.toString())) {
354                                                                                        ValidationModeEnum retVal =
355                                                                                                        ValidationModeEnum.forCode(theObject.toString());
356                                                                                        if (retVal == null) {
357                                                                                                OperationParameter.throwInvalidMode(theObject.toString());
358                                                                                        }
359                                                                                        return retVal;
360                                                                                }
361                                                                                return null;
362                                                                        }
363
364                                                                        @Override
365                                                                        public Object outgoingClient(Object theObject) {
366                                                                                return ParametersUtil.createString(
367                                                                                                theContext, ((ValidationModeEnum) theObject).getCode());
368                                                                        }
369                                                                });
370                                        } else if (nextAnnotation instanceof Validate.Profile) {
371                                                if (parameterType.equals(String.class) == false) {
372                                                        throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @"
373                                                                        + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName()
374                                                                        + " must be of type " + String.class.getName());
375                                                }
376                                                String description = ParametersUtil.extractDescription(nextParameterAnnotations);
377                                                List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);
378                                                param = new OperationParameter(
379                                                                                theContext,
380                                                                                Constants.EXTOP_VALIDATE,
381                                                                                Constants.EXTOP_VALIDATE_PROFILE,
382                                                                                0,
383                                                                                1,
384                                                                                description,
385                                                                                examples)
386                                                                .setConverter(new IOperationParamConverter() {
387                                                                        @Override
388                                                                        public Object incomingServer(Object theObject) {
389                                                                                return theObject.toString();
390                                                                        }
391
392                                                                        @Override
393                                                                        public Object outgoingClient(Object theObject) {
394                                                                                return ParametersUtil.createString(theContext, theObject.toString());
395                                                                        }
396                                                                });
397                                        } else {
398                                                continue;
399                                        }
400                                }
401                        }
402
403                        if (param == null) {
404                                throw new ConfigurationException(
405                                                Msg.code(408) + "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length)
406                                                                + " of method '" + theMethod.getName() + "' on type '"
407                                                                + theMethod.getDeclaringClass().getCanonicalName()
408                                                                + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter");
409                        }
410
411                        param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);
412                        parameters.add(param);
413
414                        paramIndex++;
415                }
416                return parameters;
417        }
418}