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