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}