001/*- 002 * #%L 003 * HAPI FHIR - Client 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.client.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.base.resource.BaseOperationOutcome; 027import ca.uhn.fhir.parser.IParser; 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.History; 034import ca.uhn.fhir.rest.annotation.Metadata; 035import ca.uhn.fhir.rest.annotation.Operation; 036import ca.uhn.fhir.rest.annotation.Patch; 037import ca.uhn.fhir.rest.annotation.Read; 038import ca.uhn.fhir.rest.annotation.Search; 039import ca.uhn.fhir.rest.annotation.Transaction; 040import ca.uhn.fhir.rest.annotation.Update; 041import ca.uhn.fhir.rest.annotation.Validate; 042import ca.uhn.fhir.rest.api.Constants; 043import ca.uhn.fhir.rest.api.EncodingEnum; 044import ca.uhn.fhir.rest.api.MethodOutcome; 045import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 046import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; 047import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; 048import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 049import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 050import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 051import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 052import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 053import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 054import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 055import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; 056import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 057import ca.uhn.fhir.util.ReflectionUtil; 058import org.apache.commons.io.IOUtils; 059import org.hl7.fhir.instance.model.api.IAnyResource; 060import org.hl7.fhir.instance.model.api.IBaseResource; 061 062import java.io.IOException; 063import java.io.InputStream; 064import java.lang.reflect.Method; 065import java.util.Collection; 066import java.util.List; 067import java.util.Set; 068import java.util.TreeSet; 069 070public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T> { 071 072 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class); 073 private FhirContext myContext; 074 private Method myMethod; 075 private List<IParameter> myParameters; 076 private Object myProvider; 077 private boolean mySupportsConditional; 078 private boolean mySupportsConditionalMultiple; 079 080 public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { 081 assert theMethod != null; 082 assert theContext != null; 083 084 myMethod = theMethod; 085 myContext = theContext; 086 myProvider = theProvider; 087 myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType()); 088 089 for (IParameter next : myParameters) { 090 if (next instanceof ConditionalParamBinder) { 091 mySupportsConditional = true; 092 if (((ConditionalParamBinder) next).isSupportsMultiple()) { 093 mySupportsConditionalMultiple = true; 094 } 095 break; 096 } 097 } 098 } 099 100 protected IParser createAppropriateParserForParsingResponse( 101 String theResponseMimeType, 102 InputStream theResponseInputStream, 103 int theResponseStatusCode, 104 List<Class<? extends IBaseResource>> thePreferTypes) { 105 EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType); 106 if (encoding == null) { 107 NonFhirResponseException ex = NonFhirResponseException.newInstance( 108 theResponseStatusCode, theResponseMimeType, theResponseInputStream); 109 populateException(ex, theResponseInputStream); 110 throw ex; 111 } 112 113 IParser parser = encoding.newParser(getContext()); 114 115 parser.setPreferTypes(thePreferTypes); 116 117 return parser; 118 } 119 120 public List<Class<?>> getAllowableParamAnnotations() { 121 return null; 122 } 123 124 public FhirContext getContext() { 125 return myContext; 126 } 127 128 public Set<String> getIncludes() { 129 Set<String> retVal = new TreeSet<String>(); 130 for (IParameter next : myParameters) { 131 if (next instanceof IncludeParameter) { 132 retVal.addAll(((IncludeParameter) next).getAllow()); 133 } 134 } 135 return retVal; 136 } 137 138 public Method getMethod() { 139 return myMethod; 140 } 141 142 public List<IParameter> getParameters() { 143 return myParameters; 144 } 145 146 public Object getProvider() { 147 return myProvider; 148 } 149 150 /** 151 * Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific 152 */ 153 public abstract String getResourceName(); 154 155 public abstract RestOperationTypeEnum getRestOperationType(); 156 157 public abstract BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException; 158 159 /** 160 * 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. 161 */ 162 public boolean isSupportsConditional() { 163 return mySupportsConditional; 164 } 165 166 /** 167 * Does this method support conditional operations over multiple objects (basically for conditional delete) 168 */ 169 public boolean isSupportsConditionalMultiple() { 170 return mySupportsConditionalMultiple; 171 } 172 173 protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow( 174 int theStatusCode, String theResponseMimeType, InputStream theResponseInputStream) { 175 BaseServerResponseException ex; 176 switch (theStatusCode) { 177 case Constants.STATUS_HTTP_400_BAD_REQUEST: 178 ex = new InvalidRequestException("Server responded with HTTP 400"); 179 break; 180 case Constants.STATUS_HTTP_404_NOT_FOUND: 181 ex = new ResourceNotFoundException("Server responded with HTTP 404"); 182 break; 183 case Constants.STATUS_HTTP_405_METHOD_NOT_ALLOWED: 184 ex = new MethodNotAllowedException("Server responded with HTTP 405"); 185 break; 186 case Constants.STATUS_HTTP_409_CONFLICT: 187 ex = new ResourceVersionConflictException("Server responded with HTTP 409"); 188 break; 189 case Constants.STATUS_HTTP_412_PRECONDITION_FAILED: 190 ex = new PreconditionFailedException("Server responded with HTTP 412"); 191 break; 192 case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY: 193 IParser parser = createAppropriateParserForParsingResponse( 194 theResponseMimeType, theResponseInputStream, theStatusCode, null); 195 // TODO: handle if something other than OO comes back 196 BaseOperationOutcome operationOutcome = 197 (BaseOperationOutcome) parser.parseResource(theResponseInputStream); 198 ex = new UnprocessableEntityException(myContext, operationOutcome); 199 break; 200 default: 201 ex = new UnclassifiedServerFailureException( 202 theStatusCode, "Server responded with HTTP " + theStatusCode); 203 break; 204 } 205 206 populateException(ex, theResponseInputStream); 207 return ex; 208 } 209 210 /** For unit tests only */ 211 public void setParameters(List<IParameter> theParameters) { 212 myParameters = theParameters; 213 } 214 215 @SuppressWarnings("unchecked") 216 public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) { 217 Read read = theMethod.getAnnotation(Read.class); 218 Search search = theMethod.getAnnotation(Search.class); 219 Metadata conformance = theMethod.getAnnotation(Metadata.class); 220 Create create = theMethod.getAnnotation(Create.class); 221 Update update = theMethod.getAnnotation(Update.class); 222 Delete delete = theMethod.getAnnotation(Delete.class); 223 History history = theMethod.getAnnotation(History.class); 224 Validate validate = theMethod.getAnnotation(Validate.class); 225 AddTags addTags = theMethod.getAnnotation(AddTags.class); 226 DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class); 227 Transaction transaction = theMethod.getAnnotation(Transaction.class); 228 Operation operation = theMethod.getAnnotation(Operation.class); 229 GetPage getPage = theMethod.getAnnotation(GetPage.class); 230 Patch patch = theMethod.getAnnotation(Patch.class); 231 232 // ** if you add another annotation above, also add it to the next line: 233 if (!verifyMethodHasZeroOrOneOperationAnnotation( 234 theMethod, 235 read, 236 search, 237 conformance, 238 create, 239 update, 240 delete, 241 history, 242 validate, 243 addTags, 244 deleteTags, 245 transaction, 246 operation, 247 getPage, 248 patch)) { 249 return null; 250 } 251 252 if (getPage != null) { 253 return new PageMethodBinding(theContext, theMethod); 254 } 255 256 Class<? extends IBaseResource> returnType; 257 258 Class<? extends IBaseResource> returnTypeFromRp = null; 259 260 Class<?> returnTypeFromMethod = theMethod.getReturnType(); 261 if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) { 262 // returns a method outcome 263 } else if (void.class.equals(returnTypeFromMethod)) { 264 // returns a bundle 265 } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) { 266 returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); 267 if (returnTypeFromMethod == null) { 268 ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod); 269 } else if (!verifyIsValidResourceReturnType(returnTypeFromMethod) 270 && !isResourceInterface(returnTypeFromMethod)) { 271 throw new ConfigurationException( 272 Msg.code(1427) + "Method '" + theMethod.getName() + "' from client type " 273 + theMethod.getDeclaringClass().getCanonicalName() 274 + " returns a collection with generic type " + toLogString(returnTypeFromMethod) 275 + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )"); 276 } 277 } else { 278 if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) { 279 throw new ConfigurationException(Msg.code(1428) + "Method '" + theMethod.getName() 280 + "' from client type " + theMethod.getDeclaringClass().getCanonicalName() 281 + " returns " + toLogString(returnTypeFromMethod) 282 + " - Must return a resource type (eg Patient, Bundle" 283 + ", etc., see the documentation for more details)"); 284 } 285 } 286 287 Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class; 288 if (read != null) { 289 returnTypeFromAnnotation = read.type(); 290 } else if (search != null) { 291 returnTypeFromAnnotation = search.type(); 292 } else if (history != null) { 293 returnTypeFromAnnotation = history.type(); 294 } else if (delete != null) { 295 returnTypeFromAnnotation = delete.type(); 296 } else if (patch != null) { 297 returnTypeFromAnnotation = patch.type(); 298 } else if (create != null) { 299 returnTypeFromAnnotation = create.type(); 300 } else if (update != null) { 301 returnTypeFromAnnotation = update.type(); 302 } else if (validate != null) { 303 returnTypeFromAnnotation = validate.type(); 304 } else if (addTags != null) { 305 returnTypeFromAnnotation = addTags.type(); 306 } else if (deleteTags != null) { 307 returnTypeFromAnnotation = deleteTags.type(); 308 } 309 310 if (!isResourceInterface(returnTypeFromAnnotation)) { 311 if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) { 312 throw new ConfigurationException(Msg.code(1429) + "Method '" + theMethod.getName() 313 + "' from client type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " 314 + toLogString(returnTypeFromAnnotation) 315 + " according to annotation - Must return a resource type"); 316 } 317 returnType = returnTypeFromAnnotation; 318 } else { 319 // if (IRestfulClient.class.isAssignableFrom(theMethod.getDeclaringClass())) { 320 // Clients don't define their methods in resource specific types, so they can 321 // infer their resource type from the method return type. 322 returnType = (Class<? extends IBaseResource>) returnTypeFromMethod; 323 // } else { 324 // This is a plain provider method returning a resource, so it should be 325 // an operation or global search presumably 326 // returnType = null; 327 } 328 329 if (read != null) { 330 return new ReadMethodBinding(returnType, theMethod, theContext, theProvider); 331 } else if (search != null) { 332 return new SearchMethodBinding(returnType, theMethod, theContext, theProvider); 333 } else if (conformance != null) { 334 return new ConformanceMethodBinding(theMethod, theContext, theProvider); 335 } else if (create != null) { 336 return new CreateMethodBinding(theMethod, theContext, theProvider); 337 } else if (update != null) { 338 return new UpdateMethodBinding(theMethod, theContext, theProvider); 339 } else if (delete != null) { 340 return new DeleteMethodBinding(theMethod, theContext, theProvider); 341 } else if (patch != null) { 342 return new PatchMethodBinding(theMethod, theContext, theProvider); 343 } else if (history != null) { 344 return new HistoryMethodBinding(theMethod, theContext, theProvider); 345 } else if (validate != null) { 346 return new ValidateMethodBindingDstu2Plus( 347 returnType, returnTypeFromRp, theMethod, theContext, theProvider, validate); 348 } else if (transaction != null) { 349 return new TransactionMethodBinding(theMethod, theContext, theProvider); 350 } else if (operation != null) { 351 return new OperationMethodBinding( 352 returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation); 353 } else { 354 throw new ConfigurationException( 355 Msg.code(1430) + "Did not detect any FHIR annotations on method '" + theMethod.getName() 356 + "' on type: " + theMethod.getDeclaringClass().getCanonicalName()); 357 } 358 359 // // each operation name must have a request type annotation and be 360 // unique 361 // if (null != read) { 362 // return rm; 363 // } 364 // 365 // SearchMethodBinding sm = new SearchMethodBinding(); 366 // if (null != search) { 367 // sm.setRequestType(SearchMethodBinding.RequestType.GET); 368 // } else if (null != theMethod.getAnnotation(PUT.class)) { 369 // sm.setRequestType(SearchMethodBinding.RequestType.PUT); 370 // } else if (null != theMethod.getAnnotation(POST.class)) { 371 // sm.setRequestType(SearchMethodBinding.RequestType.POST); 372 // } else if (null != theMethod.getAnnotation(DELETE.class)) { 373 // sm.setRequestType(SearchMethodBinding.RequestType.DELETE); 374 // } else { 375 // return null; 376 // } 377 // 378 // return sm; 379 } 380 381 public static boolean isResourceInterface(Class<?> theReturnTypeFromMethod) { 382 return theReturnTypeFromMethod.equals(IBaseResource.class) 383 || theReturnTypeFromMethod.equals(IResource.class) 384 || theReturnTypeFromMethod.equals(IAnyResource.class); 385 } 386 387 private static void populateException(BaseServerResponseException theEx, InputStream theResponseInputStream) { 388 try { 389 String responseText = IOUtils.toString(theResponseInputStream); 390 theEx.setResponseBody(responseText); 391 } catch (IOException e) { 392 ourLog.debug("Failed to read response", e); 393 } 394 } 395 396 private static String toLogString(Class<?> theType) { 397 if (theType == null) { 398 return null; 399 } 400 return theType.getCanonicalName(); 401 } 402 403 private static boolean verifyIsValidResourceReturnType(Class<?> theReturnType) { 404 if (theReturnType == null) { 405 return false; 406 } 407 if (!IBaseResource.class.isAssignableFrom(theReturnType)) { 408 return false; 409 } 410 return true; 411 // boolean retVal = Modifier.isAbstract(theReturnType.getModifiers()) == false; 412 // return retVal; 413 } 414 415 public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) { 416 Object obj1 = null; 417 for (Object object : theAnnotations) { 418 if (object != null) { 419 if (obj1 == null) { 420 obj1 = object; 421 } else { 422 throw new ConfigurationException(Msg.code(1431) + "Method " + theNextMethod.getName() + " on type '" 423 + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" 424 + obj1.getClass().getSimpleName() + " and @" 425 + object.getClass().getSimpleName() + ". Can not have both."); 426 } 427 } 428 } 429 if (obj1 == null) { 430 return false; 431 // throw new ConfigurationException(Msg.code(1432) + "Method '" + 432 // theNextMethod.getName() + "' on type '" + 433 // theNextMethod.getDeclaringClass().getSimpleName() + 434 // " has no FHIR method annotations."); 435 } 436 return true; 437 } 438}