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