All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.resteasy.client.jaxrs.internal.ClientInvocation Maven / Gradle / Ivy

There is a newer version: 7.0.0.Alpha4
Show newest version
package org.jboss.resteasy.client.jaxrs.internal;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Function;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.NotAllowedException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.NotSupportedException;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.RedirectionException;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.ServiceUnavailableException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.InvocationCallback;
import javax.ws.rs.client.ResponseProcessingException;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.Providers;
import javax.ws.rs.ext.WriterInterceptor;

import org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper;
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.engines.AsyncClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.engines.AsyncClientHttpEngine.ResultExtractor;
import org.jboss.resteasy.client.jaxrs.engines.ReactiveClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.i18n.LogMessages;
import org.jboss.resteasy.client.jaxrs.internal.proxy.ClientInvoker;
import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.core.ResteasyContext.CloseableContext;
import org.jboss.resteasy.core.interception.jaxrs.AbstractWriterInterceptorContext;
import org.jboss.resteasy.core.interception.jaxrs.ClientWriterInterceptorContext;
import org.jboss.resteasy.plugins.providers.sse.EventInput;
import org.jboss.resteasy.specimpl.MultivaluedTreeMap;
import org.jboss.resteasy.spi.util.Types;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.DelegatingOutputStream;
import org.reactivestreams.Publisher;

/**
 * @author Bill Burke
 * @version $Revision: 1 $
 */
public class ClientInvocation implements Invocation
{
   protected RESTEasyTracingLogger tracingLogger;

   protected ResteasyClient client;

   protected ClientRequestHeaders headers;

   protected String method;

   protected Object entity;

   protected Type entityGenericType;

   protected Class entityClass;

   protected Annotation[] entityAnnotations;

   protected ClientConfiguration configuration;

   protected URI uri;

   protected boolean chunked;

   protected ClientInvoker clientInvoker;

   protected WebTarget actualTarget;

   // todo need a better solution for this.  Apache Http Client 4 does not let you obtain the OutputStream before executing this request.
   // That is problematic for wrapping the output stream in e.g. a RequestFilter for transparent compressing.
   protected DelegatingOutputStream delegatingOutputStream = new DelegatingOutputStream();

   protected OutputStream entityStream = delegatingOutputStream;

   public ClientInvocation(final ResteasyClient client, final URI uri, final ClientRequestHeaders headers, final ClientConfiguration parent)
   {
      this.uri = uri;
      this.client = client;
      this.configuration = new ClientConfiguration(parent);
      this.headers = headers;

      initTracingSupport();
   }

   private void initTracingSupport() {
      final RESTEasyTracingLogger tracingLogger;

      if (RESTEasyTracingLogger.isTracingConfigALL(configuration)) {
         tracingLogger = RESTEasyTracingLogger.create(this.toString(),
                 configuration,
                 this.toString());
      } else {
         tracingLogger = RESTEasyTracingLogger.empty();
      }

      this.tracingLogger = tracingLogger;

   }

   protected ClientInvocation(final ClientInvocation clientInvocation)
   {
      this.client = clientInvocation.client;
      this.configuration = new ClientConfiguration(clientInvocation.configuration);
      this.headers = new ClientRequestHeaders(this.configuration);
      MultivaluedTreeMap.copy(clientInvocation.headers.getHeaders(), this.headers.headers);
      this.method = clientInvocation.method;
      this.entity = clientInvocation.entity;
      this.entityGenericType = clientInvocation.entityGenericType;
      this.entityClass = clientInvocation.entityClass;
      this.entityAnnotations = clientInvocation.entityAnnotations;
      this.uri = clientInvocation.uri;
      this.chunked = clientInvocation.chunked;
      this.tracingLogger = clientInvocation.tracingLogger;
      this.clientInvoker = clientInvocation.clientInvoker;
   }

   /**
    * Extracts result from response throwing an appropriate exception if not a successful response.
    *
    * @param responseType generic type
    * @param response response entity
    * @param annotations array of annotations
    * @param  type
    * @return extracted result of type T
    */
   public static  T extractResult(GenericType responseType, Response response, Annotation[] annotations)
   {
      int status = response.getStatus();
      if (status >= 200 && status < 300)
      {
         try
         {
            if (response.getMediaType() == null)
            {
               return null;
            }
            else
            {
               T rtn = response.readEntity(responseType, annotations);
               if (InputStream.class.isInstance(rtn) || Reader.class.isInstance(rtn)
                     || EventInput.class.isInstance(rtn) || Publisher.class.isInstance(rtn))
               {
                  if (response instanceof ClientResponse)
                  {
                     ClientResponse clientResponse = (ClientResponse) response;
                     clientResponse.noReleaseConnection();
                  }
               }
               return rtn;

            }
         }
         catch (WebApplicationException wae)
         {
            try
            {
               response.close();
            }
            catch (Exception e)
            {

            }
            throw wae;
         }
         catch (Throwable throwable)
         {
            try
            {
               response.close();
            }
            catch (Exception e)
            {

            }
            throw new ResponseProcessingException(response, throwable);
         }
         finally
         {
            if (response.getMediaType() == null)
               response.close();
         }
      }
      try
      {
         // Buffer the entity for any exception thrown as the response may have any entity the user wants
         // We don't want to leave the connection open though.
         String s = String.class.cast(response.getHeaders().getFirst("resteasy.buffer.exception.entity"));
         if (s == null || Boolean.parseBoolean(s))
         {
            response.bufferEntity();
         }
         else
         {
            // close connection
            if (response instanceof ClientResponse)
            {
               try
               {
                  ClientResponse.class.cast(response).releaseConnection();
               }
               catch (IOException e)
               {
                  // Ignore
               }
            }
         }
         if (status >= 300 && status < 400)
         {
            throw WebApplicationExceptionWrapper.wrap(new RedirectionException(response));
         }

         return handleErrorStatus(response);
      }
      finally
      {
         // close if no content
         if (response.getMediaType() == null)
            response.close();
      }

   }

   /**
    * Throw an exception.  Expecting a status of 400 or greater.
    *
    * @param response response entity
    * @param  type
    * @return unreachable
    */
   public static  T handleErrorStatus(Response response)
   {
      final int status = response.getStatus();
      switch (status)
      {
         case 400 :
            throw WebApplicationExceptionWrapper.wrap(new BadRequestException(response));
         case 401 :
            throw WebApplicationExceptionWrapper.wrap(new NotAuthorizedException(response));
         case 403 :
            throw WebApplicationExceptionWrapper.wrap(new ForbiddenException(response));
         case 404 :
            throw WebApplicationExceptionWrapper.wrap(new NotFoundException(response));
         case 405 :
            throw WebApplicationExceptionWrapper.wrap(new NotAllowedException(response));
         case 406 :
            throw WebApplicationExceptionWrapper.wrap(new NotAcceptableException(response));
         case 415 :
            throw WebApplicationExceptionWrapper.wrap(new NotSupportedException(response));
         case 500 :
            throw WebApplicationExceptionWrapper.wrap(new InternalServerErrorException(response));
         case 503 :
            throw WebApplicationExceptionWrapper.wrap(new ServiceUnavailableException(response));
         default :
            break;
      }

      if (status >= 400 && status < 500)
         throw WebApplicationExceptionWrapper.wrap(new ClientErrorException(response));
      if (status >= 500)
         throw WebApplicationExceptionWrapper.wrap(new ServerErrorException(response));

      throw WebApplicationExceptionWrapper.wrap(new WebApplicationException(response));
   }

   public ClientConfiguration getClientConfiguration()
   {
      return configuration;
   }

   public ResteasyClient getClient()
   {
      return client;
   }

   public DelegatingOutputStream getDelegatingOutputStream()
   {
      return delegatingOutputStream;
   }

   public void setDelegatingOutputStream(DelegatingOutputStream delegatingOutputStream)
   {
      this.delegatingOutputStream = delegatingOutputStream;
   }

   public OutputStream getEntityStream()
   {
      return entityStream;
   }

   public void setEntityStream(OutputStream entityStream)
   {
      this.entityStream = entityStream;
   }

   public URI getUri()
   {
      return uri;
   }

   public void setUri(URI uri)
   {
      this.uri = uri;
   }

   public Annotation[] getEntityAnnotations()
   {
      return entityAnnotations;
   }

   public void setEntityAnnotations(Annotation[] entityAnnotations)
   {
      this.entityAnnotations = entityAnnotations;
   }

   public String getMethod()
   {
      return method;
   }

   public void setMethod(String method)
   {
      this.method = method;
   }

   public void setHeaders(ClientRequestHeaders headers)
   {
      this.headers = headers;
   }

   public Map getMutableProperties()
   {
      return configuration.getMutableProperties();
   }

   public Object getEntity()
   {
      return entity;
   }

   public Type getEntityGenericType()
   {
      return entityGenericType;
   }

   public Class getEntityClass()
   {
      return entityClass;
   }

   public ClientRequestHeaders getHeaders()
   {
      return headers;
   }

   public void setEntity(Entity entity)
   {
      if (entity == null)
      {
         this.entity = null;
         this.entityAnnotations = null;
         this.entityClass = null;
         this.entityGenericType = null;
      }
      else
      {
         Object ent = entity.getEntity();
         setEntityObject(ent);
         this.entityAnnotations = entity.getAnnotations();
         Variant v = entity.getVariant();
         headers.setMediaType(v.getMediaType());
         headers.setLanguage(v.getLanguage());
         headers.header("Content-Encoding", null);
         headers.header("Content-Encoding", v.getEncoding());
      }

   }

   public void setEntityObject(Object ent)
   {
      if (ent instanceof GenericEntity)
      {
         GenericEntity genericEntity = (GenericEntity) ent;
         entityClass = genericEntity.getRawType();
         entityGenericType = genericEntity.getType();
         this.entity = genericEntity.getEntity();
      }
      else
      {
         if (ent == null)
         {
            this.entity = null;
            this.entityClass = null;
            this.entityGenericType = null;
         }
         else
         {
            this.entity = ent;
            this.entityClass = ent.getClass();
            this.entityGenericType = ent.getClass();
         }
      }
   }

   public void writeRequestBody(OutputStream outputStream) throws IOException
   {
      if (entity == null)
      {
         return;
      }

      WriterInterceptor[] interceptors = getWriterInterceptors();
      AbstractWriterInterceptorContext ctx = new ClientWriterInterceptorContext(interceptors,
            configuration.getProviderFactory(), entity, entityClass, entityGenericType, entityAnnotations,
            headers.getMediaType(), headers.getHeaders(), outputStream, getMutableProperties(), tracingLogger);

      final long timestamp = tracingLogger.timestamp("WI_SUMMARY");
      try {
         ctx.proceed();
      } finally {
         tracingLogger.logDuration("WI_SUMMARY", timestamp,
                 ctx.getProcessedInterceptorCount());
      }
   }

   public WriterInterceptor[] getWriterInterceptors()
   {
      return configuration.getWriterInterceptors(null, null);
   }

   public ClientRequestFilter[] getRequestFilters()
   {
      return configuration.getRequestFilters(null, null);
   }

   public ClientResponseFilter[] getResponseFilters()
   {
      return configuration.getResponseFilters(null, null);
   }

   // Invocation methods

   public Configuration getConfiguration()
   {
      return configuration;
   }

   public boolean isChunked()
   {
      return chunked;
   }

   public void setChunked(boolean chunked)
   {
      this.chunked = chunked;
   }

   @Override
   public ClientResponse invoke()
   {
      try(CloseableContext ctx = pushProvidersContext())
      {
         ClientRequestContextImpl requestContext = new ClientRequestContextImpl(this);
         ClientResponse aborted = filterRequest(requestContext);

         // spec requires that aborted response go through filter/interceptor chains.
         ClientResponse response = (aborted != null) ? aborted : (ClientResponse)client.httpEngine().invoke(this);
         return filterResponse(requestContext, response);
      }
      catch (ResponseProcessingException e)
      {
         if (e.getResponse() != null)
         {
            e.getResponse().close();
         }
         throw e;
      }
   }

   @SuppressWarnings("unchecked")
   @Override
   public  T invoke(Class responseType)
   {
      Response response = invoke();
      if (Response.class.equals(responseType))
         return (T) response;
      return extractResult(new GenericType(responseType), response, null);
   }

   @SuppressWarnings("unchecked")
   @Override
   public  T invoke(GenericType responseType)
   {
      Response response = invoke();
      if (responseType.getRawType().equals(Response.class))
         return (T) response;
      return extractResult(responseType, response, null);
   }

   @Override
   public Future submit()
   {
      return doSubmit(false, null, result -> result);
   }

   @Override
   public  Future submit(final Class responseType)
   {
      return doSubmit(false, null, getResponseTypeExtractor(responseType));
   }

   @Override
   public  Future submit(final GenericType responseType)
   {
      return doSubmit(false, null, getGenericTypeExtractor(responseType));
   }

   @SuppressWarnings({"rawtypes", "unchecked"})
   @Override
   public  Future submit(final InvocationCallback callback)
   {
      GenericType genericType = (GenericType) new GenericType()
      {
      };
      Type[] typeInfo = Types.getActualTypeArgumentsOfAnInterface(callback.getClass(), InvocationCallback.class);
      if (typeInfo != null)
      {
         genericType = new GenericType(typeInfo[0]);
      }

      return doSubmit(true, callback, getGenericTypeExtractor(genericType));
   }

   private  Future doSubmit(boolean buffered, InvocationCallback callback, ResultExtractor extractor) {
      if (client.httpEngine() instanceof AsyncClientHttpEngine)
      {
         return submitRequest(extractor, buffered)
                 .whenComplete((response, error) -> {
                    if (callback != null) {
                       if (error != null) {
                          callback.failed(error);
                       } else {
                          try {
                             callback.completed(response);
                          } catch (Throwable t) {
                             LogMessages.LOGGER.exceptionIgnored(t);
                          }
                       }
                    }
                 })
                 .toCompletableFuture();
      }
      else
      {
         return executorSubmit(asyncInvocationExecutor(), callback, extractor);
      }
   }

   public ExecutorService asyncInvocationExecutor() {
       return client.asyncInvocationExecutor();
   }

   private  Function, Publisher> getPublisherExtractorFunction(boolean buffered) {
      final ClientHttpEngine httpEngine = client.httpEngine();
      return (httpEngine instanceof ReactiveClientHttpEngine)
          ? ext -> ((ReactiveClientHttpEngine) httpEngine).submitRx(this, buffered, ext) : null;
   }

   @SuppressWarnings("unchecked")
   protected static  ResultExtractor getGenericTypeExtractor(GenericType responseType) {
      return response -> {
         if (responseType.getRawType().equals(Response.class))
            return (T) response;
         return ClientInvocation.extractResult(responseType, response, null);
      };
   }

   @SuppressWarnings("unchecked")
   protected static  ResultExtractor getResponseTypeExtractor(Class responseType) {
      return response -> {
         if (Response.class.equals(responseType))
            return (T) response;
         return ClientInvocation.extractResult(new GenericType(responseType), response, null);
      };
   }

   public CompletableFuture submitCF()
   {
      return doSubmit(response -> response, false);
   }

   public  CompletableFuture submitCF(final Class responseType)
   {
      return doSubmit(getResponseTypeExtractor(responseType), true);
   }

   public  CompletableFuture submitCF(final GenericType responseType)
   {
      return doSubmit(getGenericTypeExtractor(responseType), true);
   }

   private  CompletableFuture doSubmit(ResultExtractor extractor, boolean buffered) {
      if (client.httpEngine() instanceof AsyncClientHttpEngine)
      {
         return submitRequest(extractor, buffered)
                 .toCompletableFuture();
      }
      else
      {
         return executorSubmit(asyncInvocationExecutor(), null, extractor);
      }
   }

   class ReactiveInvocation {
      private final ReactiveClientHttpEngine reactiveEngine;

      ReactiveInvocation(final ReactiveClientHttpEngine reactiveEngine) {
         this.reactiveEngine = reactiveEngine;
      }

      public Publisher submit()
      {
         return doSubmitRx(response -> response, false);
      }

      public  Publisher submit(final Class responseType)
      {
         return doSubmitRx(getResponseTypeExtractor(responseType), true);
      }

      public  Publisher submit(final GenericType responseType)
      {
         return doSubmitRx(getGenericTypeExtractor(responseType), true);
      }

      private  Publisher doSubmitRx(ResultExtractor extractor, boolean buffered) {
         return rxSubmit(
             reactiveEngine,
             getPublisherExtractorFunction(buffered),
             extractor
         );
      }

      private  Publisher rxSubmit(
          final ReactiveClientHttpEngine reactiveEngine,
          final Function, Publisher> asyncHttpEngineSubmitFn,
          final ResultExtractor extractor
      ) {
         final ClientRequestContextImpl requestContext = new ClientRequestContextImpl(ClientInvocation.this);
         try(CloseableContext ctx = pushProvidersContext())
         {
            ClientResponse aborted = filterRequest(requestContext);
            if (aborted != null)
            {
               // spec requires that aborted response go through filter/interceptor chains.
               aborted = filterResponse(requestContext, aborted);
               T result = extractor.extractResult(aborted);
               return reactiveEngine.just(result);
            }
         }
         catch (Exception ex)
         {
            return reactiveEngine.error(ex);
         }

         return asyncHttpEngineSubmitFn.apply(response -> {
            try(CloseableContext ctx = pushProvidersContext())
            {
               return extractor.extractResult(filterResponse(requestContext, response));
            }
         });
      }
   }

   /**
    * If the client's HTTP engine implements {@link ReactiveClientHttpEngine} then you can access
    * the latter's {@link Publisher} via this method.
    */
   public Optional reactive() {
      if (client.httpEngine() instanceof ReactiveClientHttpEngine) {
         return Optional.of(new ReactiveInvocation((ReactiveClientHttpEngine)client.httpEngine()));
      }
      return Optional.empty();
   }

   @Override
   public Invocation property(String name, Object value)
   {
      configuration.property(name, value);
      return this;
   }

   public ClientInvoker getClientInvoker() {
      return clientInvoker;
   }

   public void setClientInvoker(ClientInvoker clientInvoker) {
      this.clientInvoker = clientInvoker;
   }
   // internals

   private CloseableContext pushProvidersContext()
   {
      CloseableContext ret = ResteasyContext.addCloseableContextDataLevel();
      ResteasyContext.pushContext(Providers.class, configuration);
      return ret;
   }

   protected ClientResponse filterRequest(ClientRequestContextImpl requestContext)
   {
      ClientRequestFilter[] requestFilters = getRequestFilters();
      ClientResponse aborted = null;
      if (requestFilters != null && requestFilters.length > 0)
      {
         for (ClientRequestFilter filter : requestFilters)
         {
            try
            {
               filter.filter(requestContext);
               if (requestContext.getAbortedWithResponse() != null)
               {
                  aborted = new AbortedResponse(configuration, requestContext.getAbortedWithResponse());
                  break;
               }
            }
            catch (ProcessingException e)
            {
               throw e;
            }
            catch (Throwable e)
            {
               throw new ProcessingException(e);
            }
         }
      }
      return aborted;
   }

   protected ClientResponse filterResponse(ClientRequestContextImpl requestContext, ClientResponse response)
   {
      response.setProperties(configuration.getMutableProperties());

      ClientResponseFilter[] responseFilters = getResponseFilters();
      if (responseFilters != null && responseFilters.length > 0)
      {
         ClientResponseContextImpl responseContext = new ClientResponseContextImpl(response);
         for (ClientResponseFilter filter : responseFilters)
         {
            try
            {
               filter.filter(requestContext, responseContext);
            }
            catch (ResponseProcessingException e)
            {
               throw e;
            }
            catch (Throwable e)
            {
               throw new ResponseProcessingException(response, e);
            }
         }
      }
      return response;
   }

   private  CompletionStage submitRequest(final ResultExtractor extractor, final boolean buffered) {
      final ClientRequestContextImpl requestContext = new ClientRequestContextImpl(this);
      return CompletableFuture.supplyAsync(() -> {
                 try (CloseableContext ctx = pushProvidersContext()) {
                    ClientResponse aborted = filterRequest(requestContext);
                    if (aborted != null) {
                       // spec requires that aborted response go through filter/interceptor chains.
                       aborted = filterResponse(requestContext, aborted);
                       return extractor.extractResult(aborted);
                    }
                 }
                 return null;
              }
      , asyncInvocationExecutor())
              .thenCompose(aborted -> {
                 if (aborted != null) {
                    return CompletableFuture.completedFuture(aborted);
                 }
                 final ResultExtractor wrapped = (response) -> extractor.extractResult(filterResponse(requestContext, response));
                 return ((AsyncClientHttpEngine) client.httpEngine()).submit(ClientInvocation.this, buffered, wrapped);
              });
   }

   private  CompletableFuture executorSubmit(ExecutorService executor, final InvocationCallback callback,
         final ResultExtractor extractor)
   {
      return CompletableFuture.supplyAsync(() -> {
          // FIXME: why does this have no context?
         // ensure the future and the callback see the same result
         ClientResponse response = null;
         try
         {
            response = invoke(); // does filtering too
            T result = extractor.extractResult(response);
            callCompletedNoThrow(callback, result);
            return result;
         }
         catch (Exception e)
         {
            callFailedNoThrow(callback, e);
            throw e;
         }
         finally
         {
            if (response != null && callback != null)
               response.close();
         }
      }, executor);
   }

   private static  void callCompletedNoThrow(InvocationCallback callback, T result)
   {
      if (callback != null)
      {
         try
         {
            callback.completed(result);
         }
         catch (Exception e)
         {
            //logger.error("ignoring exception in InvocationCallback", e);
         }
      }
   }

   private static  void callFailedNoThrow(InvocationCallback callback, Exception exception)
   {
      if (callback != null)
      {
         try
         {
            callback.failed(exception);
         }
         catch (Exception e)
         {
            //logger.error("ignoring exception in InvocationCallback", e);
         }
      }
   }

   public RESTEasyTracingLogger getTracingLogger() {
      return tracingLogger;
   }

   public void setActualTarget(WebTarget target)
   {
      this.actualTarget = target;
   }

   public WebTarget getActualTarget()
   {
      return this.actualTarget;
   }
}