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