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}