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