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