001package ca.uhn.fhir.rest.server.method;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
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.context.BaseRuntimeElementDefinition;
024import ca.uhn.fhir.context.ConfigurationException;
025import ca.uhn.fhir.context.FhirContext;
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.*;
031import ca.uhn.fhir.rest.api.*;
032import ca.uhn.fhir.rest.api.server.RequestDetails;
033import ca.uhn.fhir.rest.param.binder.CollectionBinder;
034import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter;
035import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode;
036import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
037import ca.uhn.fhir.util.ParametersUtil;
038import ca.uhn.fhir.util.ReflectionUtil;
039import ca.uhn.fhir.util.ValidateUtil;
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        public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) {
057                for (Annotation annotation : theAnnotations) {
058                        if (annotation instanceof Description) {
059                                Description desc = (Description) annotation;
060                                if (isNotBlank(desc.formalDefinition())) {
061                                        theParameter.setDescription(desc.formalDefinition());
062                                } else {
063                                        theParameter.setDescription(desc.shortDefinition());
064                                }
065                        }
066                }
067        }
068
069
070        @SuppressWarnings("unchecked")
071        public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) {
072                List<IParameter> parameters = new ArrayList<>();
073
074                Class<?>[] parameterTypes = theMethod.getParameterTypes();
075                int paramIndex = 0;
076                for (Annotation[] annotations : theMethod.getParameterAnnotations()) {
077
078                        IParameter param = null;
079                        Class<?> declaredParameterType = parameterTypes[paramIndex];
080                        Class<?> parameterType = declaredParameterType;
081                        Class<? extends java.util.Collection<?>> outerCollectionType = null;
082                        Class<? extends java.util.Collection<?>> innerCollectionType = null;
083                        if (TagList.class.isAssignableFrom(parameterType)) {
084                                // TagList is handled directly within the method bindings
085                                param = new NullParameter();
086                        } else {
087                                if (Collection.class.isAssignableFrom(parameterType)) {
088                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
089                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
090                                        declaredParameterType = parameterType;
091                                }
092                                if (Collection.class.isAssignableFrom(parameterType)) {
093                                        outerCollectionType = innerCollectionType;
094                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
095                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
096                                        declaredParameterType = parameterType;
097                                }
098                                if (Collection.class.isAssignableFrom(parameterType)) {
099                                        throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName()
100                                                + "' is of an invalid generic type (can not be a collection of a collection of a collection)");
101                                }
102
103                                /*
104                                 * If the user is trying to bind IPrimitiveType they are probably
105                                 * trying to write code that is compatible across versions of FHIR.
106                                 * We'll try and come up with an appropriate subtype to give
107                                 * them.
108                                 *
109                                 * This gets tested in HistoryR4Test
110                                 */
111                                if (IPrimitiveType.class.equals(parameterType)) {
112                                        Class<?> genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
113                                        if (Date.class.equals(genericType)) {
114                                                BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime");
115                                                parameterType = dateTimeDef.getImplementingClass();
116                                        } else if (String.class.equals(genericType) || genericType == null) {
117                                                BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string");
118                                                parameterType = dateTimeDef.getImplementingClass();
119                                        }
120                                }
121                        }
122
123                        if (ServletRequest.class.isAssignableFrom(parameterType)) {
124                                param = new ServletRequestParameter();
125                        } else if (ServletResponse.class.isAssignableFrom(parameterType)) {
126                                param = new ServletResponseParameter();
127                        } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) {
128                                param = new RequestDetailsParameter();
129                        } else if (parameterType.equals(IInterceptorBroadcaster.class)) {
130                                param = new InterceptorBroadcasterParameter();
131                        } else if (parameterType.equals(SummaryEnum.class)) {
132                                param = new SummaryEnumParameter();
133                        } else if (parameterType.equals(PatchTypeEnum.class)) {
134                                param = new PatchTypeParameter();
135                        } else if (parameterType.equals(SearchTotalModeEnum.class)) {
136                                param = new SearchTotalModeParameter();
137                        } else {
138                                for (int i = 0; i < annotations.length && param == null; i++) {
139                                        Annotation nextAnnotation = annotations[i];
140
141                                        if (nextAnnotation instanceof RequiredParam) {
142                                                SearchParameter parameter = new SearchParameter();
143                                                parameter.setName(((RequiredParam) nextAnnotation).name());
144                                                parameter.setRequired(true);
145                                                parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes());
146                                                parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes());
147                                                parameter.setChainlists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist());
148                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
149                                                MethodUtil.extractDescription(parameter, annotations);
150                                                param = parameter;
151                                        } else if (nextAnnotation instanceof OptionalParam) {
152                                                SearchParameter parameter = new SearchParameter();
153                                                parameter.setName(((OptionalParam) nextAnnotation).name());
154                                                parameter.setRequired(false);
155                                                parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes());
156                                                parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes());
157                                                parameter.setChainlists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist());
158                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
159                                                MethodUtil.extractDescription(parameter, annotations);
160                                                param = parameter;
161                                        } else if (nextAnnotation instanceof RawParam) {
162                                                param = new RawParamsParmeter(parameters);
163                                        } else if (nextAnnotation instanceof IncludeParam) {
164                                                Class<? extends Collection<Include>> instantiableCollectionType;
165                                                Class<?> specType;
166
167                                                if (parameterType == String.class) {
168                                                        instantiableCollectionType = null;
169                                                        specType = String.class;
170                                                } else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) {
171                                                        throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<"
172                                                                + Include.class.getSimpleName() + ">");
173                                                } else {
174                                                        instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'");
175                                                        specType = parameterType;
176                                                }
177
178                                                param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, specType);
179                                        } else if (nextAnnotation instanceof ResourceParam) {
180                                                Mode mode;
181                                                if (IBaseResource.class.isAssignableFrom(parameterType)) {
182                                                        mode = Mode.RESOURCE;
183                                                } else if (String.class.equals(parameterType)) {
184                                                        mode = ResourceParameter.Mode.BODY;
185                                                } else if (byte[].class.equals(parameterType)) {
186                                                        mode = ResourceParameter.Mode.BODY_BYTE_ARRAY;
187                                                } else if (EncodingEnum.class.equals(parameterType)) {
188                                                        mode = Mode.ENCODING;
189                                                } else {
190                                                        StringBuilder b = new StringBuilder();
191                                                        b.append("Method '");
192                                                        b.append(theMethod.getName());
193                                                        b.append("' is annotated with @");
194                                                        b.append(ResourceParam.class.getSimpleName());
195                                                        b.append(" but has a type that is not an implemtation of ");
196                                                        b.append(IBaseResource.class.getCanonicalName());
197                                                        b.append(" or String or byte[]");
198                                                        throw new ConfigurationException(b.toString());
199                                                }
200                                                boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null;
201                                                param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation);
202                                        } else if (nextAnnotation instanceof IdParam) {
203                                                param = new NullParameter();
204                                        } else if (nextAnnotation instanceof ServerBase) {
205                                                param = new ServerBaseParamBinder();
206                                        } else if (nextAnnotation instanceof Elements) {
207                                                param = new ElementsParameter();
208                                        } else if (nextAnnotation instanceof Since) {
209                                                param = new SinceParameter();
210                                                ((SinceParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
211                                        } else if (nextAnnotation instanceof At) {
212                                                param = new AtParameter();
213                                                ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
214                                        } else if (nextAnnotation instanceof Count) {
215                                                param = new CountParameter();
216                                        } else if (nextAnnotation instanceof GraphQLQuery) {
217                                                param = new GraphQLQueryParameter();
218                                        } else if (nextAnnotation instanceof Sort) {
219                                                param = new SortParameter(theContext);
220                                        } else if (nextAnnotation instanceof TransactionParam) {
221                                                param = new TransactionParameter(theContext);
222                                        } else if (nextAnnotation instanceof ConditionalUrlParam) {
223                                                param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple());
224                                        } else if (nextAnnotation instanceof OperationParam) {
225                                                Operation op = theMethod.getAnnotation(Operation.class);
226                                                OperationParam operationParam = (OperationParam) nextAnnotation;
227                                                param = new OperationParameter(theContext, op.name(), operationParam);
228                                                if (isNotBlank(operationParam.typeName())) {
229                                                        BaseRuntimeElementDefinition<?> elementDefinition = theContext.getElementDefinition(operationParam.typeName());
230                                                        if (elementDefinition == null) {
231                                                                elementDefinition = theContext.getResourceDefinition(operationParam.typeName());
232                                                        }
233                                                        org.apache.commons.lang3.Validate.notNull(elementDefinition, "Unknown type name in @OperationParam: typeName=\"%s\"", operationParam.typeName());
234
235                                                        Class<?> newParameterType = elementDefinition.getImplementingClass();
236                                                        if (!declaredParameterType.isAssignableFrom(newParameterType)) {
237                                                                throw new ConfigurationException("Non assignable parameter typeName=\"" + operationParam.typeName() + "\" specified on method " + theMethod);
238                                                        }
239                                                        parameterType = newParameterType;
240                                                }
241                                        } else if (nextAnnotation instanceof Validate.Mode) {
242                                                if (parameterType.equals(ValidationModeEnum.class) == false) {
243                                                        throw new ConfigurationException(
244                                                                "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
245                                                }
246                                                param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() {
247                                                        @Override
248                                                        public Object incomingServer(Object theObject) {
249                                                                if (isNotBlank(theObject.toString())) {
250                                                                        ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString());
251                                                                        if (retVal == null) {
252                                                                                OperationParameter.throwInvalidMode(theObject.toString());
253                                                                        }
254                                                                        return retVal;
255                                                                }
256                                                                return null;
257                                                        }
258
259                                                        @Override
260                                                        public Object outgoingClient(Object theObject) {
261                                                                return ParametersUtil.createString(theContext, ((ValidationModeEnum) theObject).getCode());
262                                                        }
263                                                });
264                                        } else if (nextAnnotation instanceof Validate.Profile) {
265                                                if (parameterType.equals(String.class) == false) {
266                                                        throw new ConfigurationException(
267                                                                "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
268                                                }
269                                                param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() {
270                                                        @Override
271                                                        public Object incomingServer(Object theObject) {
272                                                                return theObject.toString();
273                                                        }
274
275                                                        @Override
276                                                        public Object outgoingClient(Object theObject) {
277                                                                return ParametersUtil.createString(theContext, theObject.toString());
278                                                        }
279                                                });
280                                        } else {
281                                                continue;
282                                        }
283
284                                }
285
286                        }
287
288                        if (param == null) {
289                                throw new ConfigurationException(
290                                        "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
291                                                + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
292                        }
293
294                        param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);
295                        parameters.add(param);
296
297                        paramIndex++;
298                }
299                return parameters;
300        }
301
302
303}