001package ca.uhn.fhir.rest.server.method;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.ConfigurationException;
026import ca.uhn.fhir.context.FhirContext;
027import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
028import ca.uhn.fhir.model.api.Include;
029import ca.uhn.fhir.model.api.TagList;
030import ca.uhn.fhir.model.api.annotation.Description;
031import ca.uhn.fhir.rest.annotation.*;
032import ca.uhn.fhir.rest.api.*;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.param.binder.CollectionBinder;
035import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter;
036import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode;
037import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
038import ca.uhn.fhir.util.ParametersUtil;
039import ca.uhn.fhir.util.ReflectionUtil;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041import org.hl7.fhir.instance.model.api.IPrimitiveType;
042
043import javax.servlet.ServletRequest;
044import javax.servlet.ServletResponse;
045import java.lang.annotation.Annotation;
046import java.lang.reflect.Method;
047import java.util.ArrayList;
048import java.util.Collection;
049import java.util.Date;
050import java.util.List;
051
052import static org.apache.commons.lang3.StringUtils.isNotBlank;
053
054public class MethodUtil {
055
056        /**
057         * Non instantiable
058         */
059        private MethodUtil() {
060                // nothing
061        }
062
063        public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) {
064                for (Annotation annotation : theAnnotations) {
065                        if (annotation instanceof Description) {
066                                Description desc = (Description) annotation;
067                                String description = ParametersUtil.extractDescription(desc);
068                                theParameter.setDescription(description);
069                        }
070                }
071        }
072
073
074        @SuppressWarnings("unchecked")
075        public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) {
076                List<IParameter> parameters = new ArrayList<>();
077
078                Class<?>[] parameterTypes = theMethod.getParameterTypes();
079                int paramIndex = 0;
080                for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) {
081
082                        IParameter param = null;
083                        Class<?> declaredParameterType = parameterTypes[paramIndex];
084                        Class<?> parameterType = declaredParameterType;
085                        Class<? extends java.util.Collection<?>> outerCollectionType = null;
086                        Class<? extends java.util.Collection<?>> innerCollectionType = null;
087                        if (TagList.class.isAssignableFrom(parameterType)) {
088                                // TagList is handled directly within the method bindings
089                                param = new NullParameter();
090                        } else {
091                                if (Collection.class.isAssignableFrom(parameterType)) {
092                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
093                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
094                                        if(parameterType == null && theMethod.getDeclaringClass().isSynthetic()) {
095                                                try {
096                                                        theMethod = theMethod.getDeclaringClass().getSuperclass().getMethod(theMethod.getName(), parameterTypes);
097                                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
098                                                } catch (NoSuchMethodException e) {
099                                                        throw new ConfigurationException(Msg.code(400) + "A method with name '" + theMethod.getName() + "' does not exist for super class '"
100                                                                + theMethod.getDeclaringClass().getSuperclass() + "'");
101                                                }
102                                        }
103                                        declaredParameterType = parameterType;
104                                }
105                                if (Collection.class.isAssignableFrom(parameterType)) {
106                                        outerCollectionType = innerCollectionType;
107                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
108                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
109                                        declaredParameterType = parameterType;
110                                }
111                                if (Collection.class.isAssignableFrom(parameterType)) {
112                                        throw new ConfigurationException(Msg.code(401) + "Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName()
113                                                + "' is of an invalid generic type (can not be a collection of a collection of a collection)");
114                                }
115
116                                /*
117                                 * If the user is trying to bind IPrimitiveType they are probably
118                                 * trying to write code that is compatible across versions of FHIR.
119                                 * We'll try and come up with an appropriate subtype to give
120                                 * them.
121                                 *
122                                 * This gets tested in HistoryR4Test
123                                 */
124                                if (IPrimitiveType.class.equals(parameterType)) {
125                                        Class<?> genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
126                                        if (Date.class.equals(genericType)) {
127                                                BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime");
128                                                parameterType = dateTimeDef.getImplementingClass();
129                                        } else if (String.class.equals(genericType) || genericType == null) {
130                                                BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string");
131                                                parameterType = dateTimeDef.getImplementingClass();
132                                        }
133                                }
134                        }
135
136                        if (ServletRequest.class.isAssignableFrom(parameterType)) {
137                                param = new ServletRequestParameter();
138                        } else if (ServletResponse.class.isAssignableFrom(parameterType)) {
139                                param = new ServletResponseParameter();
140                        } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) {
141                                param = new RequestDetailsParameter();
142                        } else if (parameterType.equals(IInterceptorBroadcaster.class)) {
143                                param = new InterceptorBroadcasterParameter();
144                        } else if (parameterType.equals(SummaryEnum.class)) {
145                                param = new SummaryEnumParameter();
146                        } else if (parameterType.equals(PatchTypeEnum.class)) {
147                                param = new PatchTypeParameter();
148                        } else if (parameterType.equals(SearchContainedModeEnum.class)) {
149                                param = new SearchContainedModeParameter();
150                        } else if (parameterType.equals(SearchTotalModeEnum.class)) {
151                                param = new SearchTotalModeParameter();
152                        } else {
153                                for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) {
154                                        Annotation nextAnnotation = nextParameterAnnotations[i];
155
156                                        if (nextAnnotation instanceof RequiredParam) {
157                                                SearchParameter parameter = new SearchParameter();
158                                                parameter.setName(((RequiredParam) nextAnnotation).name());
159                                                parameter.setRequired(true);
160                                                parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes());
161                                                parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes());
162                                                parameter.setChainLists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist());
163                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
164                                                MethodUtil.extractDescription(parameter, nextParameterAnnotations);
165                                                param = parameter;
166                                        } else if (nextAnnotation instanceof OptionalParam) {
167                                                SearchParameter parameter = new SearchParameter();
168                                                parameter.setName(((OptionalParam) nextAnnotation).name());
169                                                parameter.setRequired(false);
170                                                parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes());
171                                                parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes());
172                                                parameter.setChainLists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist());
173                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
174                                                MethodUtil.extractDescription(parameter, nextParameterAnnotations);
175                                                param = parameter;
176                                        } else if (nextAnnotation instanceof RawParam) {
177                                                param = new RawParamsParameter(parameters);
178                                        } else if (nextAnnotation instanceof IncludeParam) {
179                                                Class<? extends Collection<Include>> instantiableCollectionType;
180                                                Class<?> specType;
181
182                                                if (parameterType == String.class) {
183                                                        instantiableCollectionType = null;
184                                                        specType = String.class;
185                                                } else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) {
186                                                        throw new ConfigurationException(Msg.code(402) + "Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<"
187                                                                + Include.class.getSimpleName() + ">");
188                                                } else {
189                                                        instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'");
190                                                        specType = parameterType;
191                                                }
192
193                                                param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, specType);
194                                        } else if (nextAnnotation instanceof ResourceParam) {
195                                                Mode mode;
196                                                if (IBaseResource.class.isAssignableFrom(parameterType)) {
197                                                        mode = Mode.RESOURCE;
198                                                } else if (String.class.equals(parameterType)) {
199                                                        mode = ResourceParameter.Mode.BODY;
200                                                } else if (byte[].class.equals(parameterType)) {
201                                                        mode = ResourceParameter.Mode.BODY_BYTE_ARRAY;
202                                                } else if (EncodingEnum.class.equals(parameterType)) {
203                                                        mode = Mode.ENCODING;
204                                                } else {
205                                                        StringBuilder b = new StringBuilder();
206                                                        b.append("Method '");
207                                                        b.append(theMethod.getName());
208                                                        b.append("' is annotated with @");
209                                                        b.append(ResourceParam.class.getSimpleName());
210                                                        b.append(" but has a type that is not an implementation of ");
211                                                        b.append(IBaseResource.class.getCanonicalName());
212                                                        b.append(" or String or byte[]");
213                                                        throw new ConfigurationException(Msg.code(403) + b.toString());
214                                                }
215                                                boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null;
216                                                boolean methodIsPatch = theMethod.getAnnotation(Patch.class) != null;
217                                                param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation, methodIsPatch);
218                                        } else if (nextAnnotation instanceof IdParam) {
219                                                param = new NullParameter();
220                                        } else if (nextAnnotation instanceof ServerBase) {
221                                                param = new ServerBaseParamBinder();
222                                        } else if (nextAnnotation instanceof Elements) {
223                                                param = new ElementsParameter();
224                                        } else if (nextAnnotation instanceof Since) {
225                                                param = new SinceParameter();
226                                                ((SinceParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
227                                        } else if (nextAnnotation instanceof At) {
228                                                param = new AtParameter();
229                                                ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
230                                        } else if (nextAnnotation instanceof Count) {
231                                                param = new CountParameter();
232                                        } else if (nextAnnotation instanceof Offset) {
233                                                param = new OffsetParameter();
234                                        } else if (nextAnnotation instanceof GraphQLQueryUrl) {
235                                                param = new GraphQLQueryUrlParameter();
236                                        } else if (nextAnnotation instanceof GraphQLQueryBody) {
237                                                param = new GraphQLQueryBodyParameter();
238                                        } else if (nextAnnotation instanceof Sort) {
239                                                param = new SortParameter(theContext);
240                                        } else if (nextAnnotation instanceof TransactionParam) {
241                                                param = new TransactionParameter(theContext);
242                                        } else if (nextAnnotation instanceof ConditionalUrlParam) {
243                                                param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple());
244                                        } else if (nextAnnotation instanceof OperationParam) {
245                                                Operation op = theMethod.getAnnotation(Operation.class);
246                                                if (op == null) {
247                                                        throw new ConfigurationException(Msg.code(404) + "@OperationParam detected on method that is not annotated with @Operation: " + theMethod.toGenericString());
248                                                }
249
250                                                OperationParam operationParam = (OperationParam) nextAnnotation;
251                                                String description = ParametersUtil.extractDescription(nextParameterAnnotations);
252                                                List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);;
253                                                param = new OperationParameter(theContext, op.name(), operationParam.name(), operationParam.min(), operationParam.max(), description, examples);
254                                                if (isNotBlank(operationParam.typeName())) {
255                                                        BaseRuntimeElementDefinition<?> elementDefinition = theContext.getElementDefinition(operationParam.typeName());
256                                                        if (elementDefinition == null) {
257                                                                elementDefinition = theContext.getResourceDefinition(operationParam.typeName());
258                                                        }
259                                                        org.apache.commons.lang3.Validate.notNull(elementDefinition, "Unknown type name in @OperationParam: typeName=\"%s\"", operationParam.typeName());
260
261                                                        Class<?> newParameterType = elementDefinition.getImplementingClass();
262                                                        if (!declaredParameterType.isAssignableFrom(newParameterType)) {
263                                                                throw new ConfigurationException(Msg.code(405) + "Non assignable parameter typeName=\"" + operationParam.typeName() + "\" specified on method " + theMethod);
264                                                        }
265                                                        parameterType = newParameterType;
266                                                }
267                                        } else if (nextAnnotation instanceof Validate.Mode) {
268                                                if (parameterType.equals(ValidationModeEnum.class) == false) {
269                                                        throw new ConfigurationException(Msg.code(406) + "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
270                                                }
271                                                String description = ParametersUtil.extractDescription(nextParameterAnnotations);
272                                                List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);
273                                                param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1, description, examples).setConverter(new IOperationParamConverter() {
274                                                        @Override
275                                                        public Object incomingServer(Object theObject) {
276                                                                if (isNotBlank(theObject.toString())) {
277                                                                        ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString());
278                                                                        if (retVal == null) {
279                                                                                OperationParameter.throwInvalidMode(theObject.toString());
280                                                                        }
281                                                                        return retVal;
282                                                                }
283                                                                return null;
284                                                        }
285
286                                                        @Override
287                                                        public Object outgoingClient(Object theObject) {
288                                                                return ParametersUtil.createString(theContext, ((ValidationModeEnum) theObject).getCode());
289                                                        }
290                                                });
291                                        } else if (nextAnnotation instanceof Validate.Profile) {
292                                                if (parameterType.equals(String.class) == false) {
293                                                        throw new ConfigurationException(Msg.code(407) + "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
294                                                }
295                                                String description = ParametersUtil.extractDescription(nextParameterAnnotations);
296                                                List<String> examples = ParametersUtil.extractExamples(nextParameterAnnotations);
297                                                param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1, description, examples).setConverter(new IOperationParamConverter() {
298                                                        @Override
299                                                        public Object incomingServer(Object theObject) {
300                                                                return theObject.toString();
301                                                        }
302
303                                                        @Override
304                                                        public Object outgoingClient(Object theObject) {
305                                                                return ParametersUtil.createString(theContext, theObject.toString());
306                                                        }
307                                                });
308                                        } else {
309                                                continue;
310                                        }
311
312                                }
313
314                        }
315
316                        if (param == null) {
317                                throw new ConfigurationException(Msg.code(408) + "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
318                                                + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter");
319                        }
320
321                        param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);
322                        parameters.add(param);
323
324                        paramIndex++;
325                }
326                return parameters;
327        }
328
329
330}