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