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