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.ConfigurationException; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.model.api.IResource; 029import ca.uhn.fhir.model.api.Include; 030import ca.uhn.fhir.parser.DataFormatException; 031import ca.uhn.fhir.rest.annotation.*; 032import ca.uhn.fhir.rest.api.MethodOutcome; 033import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 034import ca.uhn.fhir.rest.api.server.IBundleProvider; 035import ca.uhn.fhir.rest.api.server.IRestfulServer; 036import ca.uhn.fhir.rest.api.server.RequestDetails; 037import ca.uhn.fhir.rest.server.BundleProviders; 038import ca.uhn.fhir.rest.server.IResourceProvider; 039import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 040import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 041import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; 042import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 043import ca.uhn.fhir.util.ReflectionUtil; 044import org.hl7.fhir.instance.model.api.IAnyResource; 045import org.hl7.fhir.instance.model.api.IBaseBundle; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047 048import javax.annotation.Nonnull; 049import java.io.IOException; 050import java.lang.reflect.InvocationTargetException; 051import java.lang.reflect.Method; 052import java.util.ArrayList; 053import java.util.Collection; 054import java.util.HashSet; 055import java.util.List; 056import java.util.Set; 057import java.util.TreeSet; 058import java.util.stream.Collectors; 059 060import static org.apache.commons.lang3.StringUtils.isNotBlank; 061 062public abstract class BaseMethodBinding<T> { 063 064 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class); 065 private final List<BaseQueryParameter> myQueryParameters; 066 private FhirContext myContext; 067 private Method myMethod; 068 private List<IParameter> myParameters; 069 private Object myProvider; 070 private boolean mySupportsConditional; 071 private boolean mySupportsConditionalMultiple; 072 public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { 073 assert theMethod != null; 074 assert theContext != null; 075 076 myMethod = theMethod; 077 myContext = theContext; 078 myProvider = theProvider; 079 myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType()); 080 myQueryParameters = myParameters 081 .stream() 082 .filter(t -> t instanceof BaseQueryParameter) 083 .map(t -> (BaseQueryParameter) t) 084 .collect(Collectors.toList()); 085 086 for (IParameter next : myParameters) { 087 if (next instanceof ConditionalParamBinder) { 088 mySupportsConditional = true; 089 if (((ConditionalParamBinder) next).isSupportsMultiple()) { 090 mySupportsConditionalMultiple = true; 091 } 092 break; 093 } 094 } 095 096 // This allows us to invoke methods on private classes 097 myMethod.setAccessible(true); 098 } 099 100 protected List<BaseQueryParameter> getQueryParameters() { 101 return myQueryParameters; 102 } 103 104 protected Object[] createMethodParams(RequestDetails theRequest) { 105 Object[] params = new Object[getParameters().size()]; 106 for (int i = 0; i < getParameters().size(); i++) { 107 IParameter param = getParameters().get(i); 108 if (param != null) { 109 params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); 110 } 111 } 112 return params; 113 } 114 115 protected Object[] createParametersForServerRequest(RequestDetails theRequest) { 116 Object[] params = new Object[getParameters().size()]; 117 for (int i = 0; i < getParameters().size(); i++) { 118 IParameter param = getParameters().get(i); 119 if (param == null) { 120 continue; 121 } 122 params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); 123 } 124 return params; 125 } 126 127 /** 128 * Subclasses may override to declare that they apply to all resource types 129 */ 130 public boolean isGlobalMethod() { 131 return false; 132 } 133 134 public List<Class<?>> getAllowableParamAnnotations() { 135 return null; 136 } 137 138 public FhirContext getContext() { 139 return myContext; 140 } 141 142 public Set<String> getIncludes() { 143 return doGetIncludesOrRevIncludes(false); 144 } 145 146 public Set<String> getRevIncludes() { 147 return doGetIncludesOrRevIncludes(true); 148 } 149 150 private Set<String> doGetIncludesOrRevIncludes(boolean reverse) { 151 Set<String> retVal = new TreeSet<>(); 152 for (IParameter next : myParameters) { 153 if (next instanceof IncludeParameter) { 154 IncludeParameter includeParameter = (IncludeParameter) next; 155 if (includeParameter.isReverse() == reverse) { 156 retVal.addAll(includeParameter.getAllow()); 157 } 158 } 159 } 160 return retVal; 161 } 162 163 public Method getMethod() { 164 return myMethod; 165 } 166 167 public List<IParameter> getParameters() { 168 return myParameters; 169 } 170 171 /** 172 * For unit tests only 173 */ 174 public void setParameters(List<IParameter> theParameters) { 175 myParameters = theParameters; 176 } 177 178 public Object getProvider() { 179 return myProvider; 180 } 181 182 @SuppressWarnings({"unchecked", "rawtypes"}) 183 public Set<Include> getRequestIncludesFromParams(Object[] params) { 184 if (params == null || params.length == 0) { 185 return null; 186 } 187 int index = 0; 188 boolean match = false; 189 for (IParameter parameter : myParameters) { 190 if (parameter instanceof IncludeParameter) { 191 match = true; 192 break; 193 } 194 index++; 195 } 196 if (!match) { 197 return null; 198 } 199 if (index >= params.length) { 200 ourLog.warn("index out of parameter range (should never happen"); 201 return null; 202 } 203 if (params[index] instanceof Set) { 204 return (Set<Include>) params[index]; 205 } 206 if (params[index] instanceof Iterable) { 207 Set includes = new HashSet<Include>(); 208 for (Object o : (Iterable) params[index]) { 209 if (o instanceof Include) { 210 includes.add(o); 211 } 212 } 213 return includes; 214 } 215 ourLog.warn("include params wasn't Set or Iterable, it was {}", params[index].getClass()); 216 return null; 217 } 218 219 /** 220 * Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific 221 */ 222 public abstract String getResourceName(); 223 224 @Nonnull 225 public abstract RestOperationTypeEnum getRestOperationType(); 226 227 /** 228 * Determine which operation is being fired for a specific request 229 * 230 * @param theRequestDetails The request 231 */ 232 public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { 233 return getRestOperationType(); 234 } 235 236 public abstract MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest); 237 238 public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException; 239 240 protected final Object invokeServerMethod(RequestDetails theRequest, Object[] theMethodParams) { 241 // Handle server action interceptors 242 RestOperationTypeEnum operationType = getRestOperationType(theRequest); 243 if (operationType != null) { 244 245 ActionRequestDetails details = new ActionRequestDetails(theRequest); 246 populateActionRequestDetailsForInterceptor(theRequest, details, theMethodParams); 247 248 // Interceptor invoke: SERVER_INCOMING_REQUEST_PRE_HANDLED 249 HookParams preHandledParams = new HookParams(); 250 preHandledParams.add(RestOperationTypeEnum.class, theRequest.getRestOperationType()); 251 preHandledParams.add(RequestDetails.class, theRequest); 252 preHandledParams.addIfMatchesType(ServletRequestDetails.class, theRequest); 253 preHandledParams.add(ActionRequestDetails.class, details); 254 if (theRequest.getInterceptorBroadcaster() != null) { 255 theRequest 256 .getInterceptorBroadcaster() 257 .callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, preHandledParams); 258 } 259 260 } 261 262 // Actually invoke the method 263 try { 264 Method method = getMethod(); 265 return method.invoke(getProvider(), theMethodParams); 266 } catch (InvocationTargetException e) { 267 if (e.getCause() instanceof BaseServerResponseException) { 268 throw (BaseServerResponseException) e.getCause(); 269 } 270 if (e.getTargetException() instanceof DataFormatException) { 271 throw (DataFormatException)e.getTargetException(); 272 } 273 throw new InternalErrorException(Msg.code(389) + "Failed to call access method: " + e.getCause(), e); 274 } catch (Exception e) { 275 throw new InternalErrorException(Msg.code(390) + "Failed to call access method: " + e.getCause(), e); 276 } 277 } 278 279 /** 280 * Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't actually support this paramter, so this will only return true occasionally. 281 */ 282 public boolean isSupportsConditional() { 283 return mySupportsConditional; 284 } 285 286 /** 287 * Does this method support conditional operations over multiple objects (basically for conditional delete) 288 */ 289 public boolean isSupportsConditionalMultiple() { 290 return mySupportsConditionalMultiple; 291 } 292 293 /** 294 * Subclasses may override this method (but should also call super) to provide method specifics to the 295 * interceptors. 296 * 297 * @param theRequestDetails The server request details 298 * @param theDetails The details object to populate 299 * @param theMethodParams The method params as generated by the specific method binding 300 */ 301 protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) { 302 // nothing by default 303 } 304 305 protected IBundleProvider toResourceList(Object response) throws InternalErrorException { 306 if (response == null) { 307 return BundleProviders.newEmptyList(); 308 } else if (response instanceof IBundleProvider) { 309 return (IBundleProvider) response; 310 } else if (response instanceof IBaseResource) { 311 return BundleProviders.newList((IBaseResource) response); 312 } else if (response instanceof Collection) { 313 List<IBaseResource> retVal = new ArrayList<IBaseResource>(); 314 for (Object next : ((Collection<?>) response)) { 315 retVal.add((IBaseResource) next); 316 } 317 return BundleProviders.newList(retVal); 318 } else if (response instanceof MethodOutcome) { 319 IBaseResource retVal = ((MethodOutcome) response).getOperationOutcome(); 320 if (retVal == null) { 321 retVal = getContext().getResourceDefinition("OperationOutcome").newInstance(); 322 } 323 return BundleProviders.newList(retVal); 324 } else { 325 throw new InternalErrorException(Msg.code(391) + "Unexpected return type: " + response.getClass().getCanonicalName()); 326 } 327 } 328 329 public void close() { 330 // subclasses may override 331 } 332 333 @SuppressWarnings("unchecked") 334 public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) { 335 Read read = theMethod.getAnnotation(Read.class); 336 Search search = theMethod.getAnnotation(Search.class); 337 Metadata conformance = theMethod.getAnnotation(Metadata.class); 338 Create create = theMethod.getAnnotation(Create.class); 339 Update update = theMethod.getAnnotation(Update.class); 340 Delete delete = theMethod.getAnnotation(Delete.class); 341 History history = theMethod.getAnnotation(History.class); 342 Validate validate = theMethod.getAnnotation(Validate.class); 343 AddTags addTags = theMethod.getAnnotation(AddTags.class); 344 DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class); 345 Transaction transaction = theMethod.getAnnotation(Transaction.class); 346 Operation operation = theMethod.getAnnotation(Operation.class); 347 GetPage getPage = theMethod.getAnnotation(GetPage.class); 348 Patch patch = theMethod.getAnnotation(Patch.class); 349 GraphQL graphQL = theMethod.getAnnotation(GraphQL.class); 350 351 // ** if you add another annotation above, also add it to the next line: 352 if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, addTags, deleteTags, transaction, operation, getPage, patch, graphQL)) { 353 return null; 354 } 355 356 if (getPage != null) { 357 return new PageMethodBinding(theContext, theMethod); 358 } 359 360 if (graphQL != null) { 361 return new GraphQLMethodBinding(theMethod, graphQL.type(), theContext, theProvider); 362 } 363 364 Class<? extends IBaseResource> returnType; 365 366 Class<? extends IBaseResource> returnTypeFromRp = null; 367 if (theProvider instanceof IResourceProvider) { 368 returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType(); 369 if (!verifyIsValidResourceReturnType(returnTypeFromRp)) { 370 throw new ConfigurationException(Msg.code(392) + "getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " 371 + toLogString(returnTypeFromRp) + " - Must return a resource type"); 372 } 373 } 374 375 Class<?> returnTypeFromMethod = theMethod.getReturnType(); 376 if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) { 377 // returns a method outcome 378 } else if (IBundleProvider.class.equals(returnTypeFromMethod)) { 379 // returns a bundle provider 380 } else if (void.class.equals(returnTypeFromMethod)) { 381 // returns a bundle 382 } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) { 383 returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); 384 if (returnTypeFromMethod == null) { 385 ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod); 386 } else if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !isResourceInterface(returnTypeFromMethod)) { 387 throw new ConfigurationException(Msg.code(393) + "Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() 388 + " returns a collection with generic type " + toLogString(returnTypeFromMethod) 389 + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )"); 390 } 391 } else if (IBaseBundle.class.isAssignableFrom(returnTypeFromMethod) && returnTypeFromRp == null) { 392 // If a plain provider method returns a Bundle, we'll assume it to be a system 393 // level operation and not a type/instance level operation on the Bundle type. 394 returnTypeFromMethod = null; 395 } else { 396 if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) { 397 throw new ConfigurationException(Msg.code(394) + "Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() 398 + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, Bundle, " + IBundleProvider.class.getSimpleName() 399 + ", etc., see the documentation for more details)"); 400 } 401 } 402 403 Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class; 404 String returnTypeNameFromAnnotation = null; 405 if (read != null) { 406 returnTypeFromAnnotation = read.type(); 407 returnTypeNameFromAnnotation = read.typeName(); 408 } else if (search != null) { 409 returnTypeFromAnnotation = search.type(); 410 returnTypeNameFromAnnotation = search.typeName(); 411 } else if (history != null) { 412 returnTypeFromAnnotation = history.type(); 413 returnTypeNameFromAnnotation = history.typeName(); 414 } else if (delete != null) { 415 returnTypeFromAnnotation = delete.type(); 416 returnTypeNameFromAnnotation = delete.typeName(); 417 } else if (patch != null) { 418 returnTypeFromAnnotation = patch.type(); 419 returnTypeNameFromAnnotation = patch.typeName(); 420 } else if (create != null) { 421 returnTypeFromAnnotation = create.type(); 422 returnTypeNameFromAnnotation = create.typeName(); 423 } else if (update != null) { 424 returnTypeFromAnnotation = update.type(); 425 returnTypeNameFromAnnotation = update.typeName(); 426 } else if (validate != null) { 427 returnTypeFromAnnotation = validate.type(); 428 returnTypeNameFromAnnotation = validate.typeName(); 429 } else if (addTags != null) { 430 returnTypeFromAnnotation = addTags.type(); 431 returnTypeNameFromAnnotation = addTags.typeName(); 432 } else if (deleteTags != null) { 433 returnTypeFromAnnotation = deleteTags.type(); 434 returnTypeNameFromAnnotation = deleteTags.typeName(); 435 } 436 437 if (isNotBlank(returnTypeNameFromAnnotation)) { 438 returnTypeFromAnnotation = theContext.getResourceDefinition(returnTypeNameFromAnnotation).getImplementingClass(); 439 } 440 441 if (returnTypeFromRp != null) { 442 if (returnTypeFromAnnotation != null && !isResourceInterface(returnTypeFromAnnotation)) { 443 if (returnTypeFromMethod != null && !returnTypeFromRp.isAssignableFrom(returnTypeFromMethod)) { 444 throw new ConfigurationException(Msg.code(395) + "Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " 445 + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract"); 446 } 447 if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) { 448 throw new ConfigurationException(Msg.code(396) + "Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName() 449 + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract"); 450 } 451 returnType = returnTypeFromAnnotation; 452 } else { 453 returnType = returnTypeFromRp; 454 } 455 } else { 456 if (!isResourceInterface(returnTypeFromAnnotation)) { 457 if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) { 458 throw new ConfigurationException(Msg.code(397) + "Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() 459 + " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type"); 460 } 461 returnType = returnTypeFromAnnotation; 462 } else { 463 returnType = (Class<? extends IBaseResource>) returnTypeFromMethod; 464 } 465 } 466 467 if (read != null) { 468 return new ReadMethodBinding(returnType, theMethod, theContext, theProvider); 469 } else if (search != null) { 470 return new SearchMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider); 471 } else if (conformance != null) { 472 return new ConformanceMethodBinding(theMethod, theContext, theProvider); 473 } else if (create != null) { 474 return new CreateMethodBinding(theMethod, theContext, theProvider); 475 } else if (update != null) { 476 return new UpdateMethodBinding(theMethod, theContext, theProvider); 477 } else if (delete != null) { 478 return new DeleteMethodBinding(theMethod, theContext, theProvider); 479 } else if (patch != null) { 480 return new PatchMethodBinding(theMethod, theContext, theProvider); 481 } else if (history != null) { 482 return new HistoryMethodBinding(theMethod, theContext, theProvider); 483 } else if (validate != null) { 484 return new ValidateMethodBindingDstu2Plus(returnType, returnTypeFromRp, theMethod, theContext, theProvider, validate); 485 } else if (transaction != null) { 486 return new TransactionMethodBinding(theMethod, theContext, theProvider); 487 } else if (operation != null) { 488 return new OperationMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation); 489 } else { 490 throw new ConfigurationException(Msg.code(398) + "Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName()); 491 } 492 493 } 494 495 private static boolean isResourceInterface(Class<?> theReturnTypeFromMethod) { 496 return theReturnTypeFromMethod != null && (theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class)); 497 } 498 499 private static String toLogString(Class<?> theType) { 500 if (theType == null) { 501 return null; 502 } 503 return theType.getCanonicalName(); 504 } 505 506 private static boolean verifyIsValidResourceReturnType(Class<?> theReturnType) { 507 if (theReturnType == null) { 508 return false; 509 } 510 if (!IBaseResource.class.isAssignableFrom(theReturnType)) { 511 return false; 512 } 513 return true; 514 } 515 516 public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) { 517 Object obj1 = null; 518 for (Object object : theAnnotations) { 519 if (object != null) { 520 if (obj1 == null) { 521 obj1 = object; 522 } else { 523 throw new ConfigurationException(Msg.code(399) + "Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" 524 + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both."); 525 } 526 527 } 528 } 529 if (obj1 == null) { 530 return false; 531 } 532 return true; 533 } 534 535}