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