| /* |
| * Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0, which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the |
| * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, |
| * version 2 with the GNU Classpath Exception, which is available at |
| * https://www.gnu.org/software/classpath/license.html. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| */ |
| |
| package org.glassfish.jersey.client; |
| |
| import java.lang.reflect.Type; |
| import java.net.URI; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.CancellationException; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Future; |
| import java.util.function.BiFunction; |
| import java.util.logging.Logger; |
| |
| 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.CompletionStageRxInvoker; |
| 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.RxInvoker; |
| import javax.ws.rs.client.RxInvokerProvider; |
| import javax.ws.rs.client.SyncInvoker; |
| import javax.ws.rs.core.CacheControl; |
| import javax.ws.rs.core.Cookie; |
| import javax.ws.rs.core.GenericType; |
| import javax.ws.rs.core.HttpHeaders; |
| import javax.ws.rs.core.MediaType; |
| import javax.ws.rs.core.MultivaluedMap; |
| import javax.ws.rs.core.Response; |
| |
| import org.glassfish.jersey.client.internal.ClientResponseProcessingException; |
| import org.glassfish.jersey.client.internal.LocalizationMessages; |
| import org.glassfish.jersey.internal.MapPropertiesDelegate; |
| import org.glassfish.jersey.internal.inject.Providers; |
| import org.glassfish.jersey.internal.util.Producer; |
| import org.glassfish.jersey.internal.util.PropertiesHelper; |
| import org.glassfish.jersey.internal.util.ReflectionHelper; |
| import org.glassfish.jersey.process.internal.RequestScope; |
| import org.glassfish.jersey.spi.ExecutorServiceProvider; |
| |
| /** |
| * Jersey implementation of {@link javax.ws.rs.client.Invocation JAX-RS client-side |
| * request invocation} contract. |
| * |
| * @author Marek Potociar |
| */ |
| public class JerseyInvocation implements javax.ws.rs.client.Invocation { |
| |
| private static final Logger LOGGER = Logger.getLogger(JerseyInvocation.class.getName()); |
| |
| private final ClientRequest requestContext; |
| // Copy request context when invoke or submit methods are invoked. |
| private final boolean copyRequestContext; |
| |
| private JerseyInvocation(final Builder builder) { |
| this(builder, false); |
| } |
| |
| private JerseyInvocation(final Builder builder, final boolean copyRequestContext) { |
| validateHttpMethodAndEntity(builder.requestContext); |
| |
| this.requestContext = new ClientRequest(builder.requestContext); |
| this.copyRequestContext = copyRequestContext; |
| } |
| |
| private enum EntityPresence { |
| MUST_BE_NULL, |
| MUST_BE_PRESENT, |
| OPTIONAL |
| } |
| |
| private static final Map<String, EntityPresence> METHODS = initializeMap(); |
| |
| private static Map<String, EntityPresence> initializeMap() { |
| final Map<String, EntityPresence> map = new HashMap<>(); |
| |
| map.put("DELETE", EntityPresence.MUST_BE_NULL); |
| map.put("GET", EntityPresence.MUST_BE_NULL); |
| map.put("HEAD", EntityPresence.MUST_BE_NULL); |
| map.put("OPTIONS", EntityPresence.MUST_BE_NULL); |
| map.put("PATCH", EntityPresence.MUST_BE_PRESENT); |
| map.put("POST", EntityPresence.OPTIONAL); // we allow to post null instead of entity |
| map.put("PUT", EntityPresence.MUST_BE_PRESENT); |
| map.put("TRACE", EntityPresence.MUST_BE_NULL); |
| return map; |
| } |
| |
| private void validateHttpMethodAndEntity(final ClientRequest request) { |
| boolean suppressExceptions; |
| suppressExceptions = PropertiesHelper.isProperty( |
| request.getConfiguration().getProperty(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION)); |
| |
| final Object shcvProperty = request.getProperty(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION); |
| if (shcvProperty != null) { // override global configuration with request-specific |
| suppressExceptions = PropertiesHelper.isProperty(shcvProperty); |
| } |
| |
| final String method = request.getMethod(); |
| |
| final EntityPresence entityPresence = METHODS.get(method.toUpperCase(Locale.ROOT)); |
| if (entityPresence == EntityPresence.MUST_BE_NULL && request.hasEntity()) { |
| if (suppressExceptions) { |
| LOGGER.warning(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NOT_NULL(method)); |
| } else { |
| throw new IllegalStateException(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NOT_NULL(method)); |
| } |
| } else if (entityPresence == EntityPresence.MUST_BE_PRESENT && !request.hasEntity()) { |
| if (suppressExceptions) { |
| LOGGER.warning(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NULL(method)); |
| } else { |
| throw new IllegalStateException(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NULL(method)); |
| } |
| } |
| } |
| |
| /** |
| * Jersey-specific {@link javax.ws.rs.client.Invocation.Builder client invocation builder}. |
| */ |
| public static class Builder implements javax.ws.rs.client.Invocation.Builder { |
| |
| private final ClientRequest requestContext; |
| |
| /** |
| * Create new Jersey-specific client invocation builder. |
| * |
| * @param uri invoked request URI. |
| * @param configuration Jersey client configuration. |
| */ |
| protected Builder(final URI uri, final ClientConfig configuration) { |
| this.requestContext = new ClientRequest(uri, configuration, new MapPropertiesDelegate()); |
| } |
| |
| /** |
| * Returns a reference to the mutable request context to be invoked. |
| * |
| * @return mutable request context to be invoked. |
| */ |
| ClientRequest request() { |
| return requestContext; |
| } |
| |
| private void storeEntity(final Entity<?> entity) { |
| if (entity != null) { |
| requestContext.variant(entity.getVariant()); |
| requestContext.setEntity(entity.getEntity()); |
| requestContext.setEntityAnnotations(entity.getAnnotations()); |
| } |
| } |
| |
| @Override |
| public JerseyInvocation build(final String method) { |
| requestContext.setMethod(method); |
| return new JerseyInvocation(this, true); |
| } |
| |
| @Override |
| public JerseyInvocation build(final String method, final Entity<?> entity) { |
| requestContext.setMethod(method); |
| storeEntity(entity); |
| return new JerseyInvocation(this, true); |
| } |
| |
| @Override |
| public JerseyInvocation buildGet() { |
| requestContext.setMethod("GET"); |
| return new JerseyInvocation(this, true); |
| } |
| |
| @Override |
| public JerseyInvocation buildDelete() { |
| requestContext.setMethod("DELETE"); |
| return new JerseyInvocation(this, true); |
| } |
| |
| @Override |
| public JerseyInvocation buildPost(final Entity<?> entity) { |
| requestContext.setMethod("POST"); |
| storeEntity(entity); |
| return new JerseyInvocation(this, true); |
| } |
| |
| @Override |
| public JerseyInvocation buildPut(final Entity<?> entity) { |
| requestContext.setMethod("PUT"); |
| storeEntity(entity); |
| return new JerseyInvocation(this, true); |
| } |
| |
| @Override |
| public javax.ws.rs.client.AsyncInvoker async() { |
| return new AsyncInvoker(this); |
| } |
| |
| @Override |
| public Builder accept(final String... mediaTypes) { |
| requestContext.accept(mediaTypes); |
| return this; |
| } |
| |
| @Override |
| public Builder accept(final MediaType... mediaTypes) { |
| requestContext.accept(mediaTypes); |
| return this; |
| } |
| |
| @Override |
| public Invocation.Builder acceptEncoding(final String... encodings) { |
| requestContext.getHeaders().addAll(HttpHeaders.ACCEPT_ENCODING, (Object[]) encodings); |
| return this; |
| } |
| |
| @Override |
| public Builder acceptLanguage(final Locale... locales) { |
| requestContext.acceptLanguage(locales); |
| return this; |
| } |
| |
| @Override |
| public Builder acceptLanguage(final String... locales) { |
| requestContext.acceptLanguage(locales); |
| return this; |
| } |
| |
| @Override |
| public Builder cookie(final Cookie cookie) { |
| requestContext.cookie(cookie); |
| return this; |
| } |
| |
| @Override |
| public Builder cookie(final String name, final String value) { |
| requestContext.cookie(new Cookie(name, value)); |
| return this; |
| } |
| |
| @Override |
| public Builder cacheControl(final CacheControl cacheControl) { |
| requestContext.cacheControl(cacheControl); |
| return this; |
| } |
| |
| @Override |
| public Builder header(final String name, final Object value) { |
| final MultivaluedMap<String, Object> headers = requestContext.getHeaders(); |
| |
| if (value == null) { |
| headers.remove(name); |
| } else { |
| headers.add(name, value); |
| } |
| |
| if (HttpHeaders.USER_AGENT.equalsIgnoreCase(name)) { |
| requestContext.ignoreUserAgent(value == null); |
| } |
| |
| return this; |
| } |
| |
| @Override |
| public Builder headers(final MultivaluedMap<String, Object> headers) { |
| requestContext.replaceHeaders(headers); |
| return this; |
| } |
| |
| @Override |
| public Response get() throws ProcessingException { |
| return method("GET"); |
| } |
| |
| @Override |
| public <T> T get(final Class<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("GET", responseType); |
| } |
| |
| @Override |
| public <T> T get(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("GET", responseType); |
| } |
| |
| @Override |
| public Response put(final Entity<?> entity) throws ProcessingException { |
| return method("PUT", entity); |
| } |
| |
| @Override |
| public <T> T put(final Entity<?> entity, final Class<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| return method("PUT", entity, responseType); |
| } |
| |
| @Override |
| public <T> T put(final Entity<?> entity, final GenericType<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| return method("PUT", entity, responseType); |
| } |
| |
| @Override |
| public Response post(final Entity<?> entity) throws ProcessingException { |
| return method("POST", entity); |
| } |
| |
| @Override |
| public <T> T post(final Entity<?> entity, final Class<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| return method("POST", entity, responseType); |
| } |
| |
| @Override |
| public <T> T post(final Entity<?> entity, final GenericType<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| return method("POST", entity, responseType); |
| } |
| |
| @Override |
| public Response delete() throws ProcessingException { |
| return method("DELETE"); |
| } |
| |
| @Override |
| public <T> T delete(final Class<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("DELETE", responseType); |
| } |
| |
| @Override |
| public <T> T delete(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("DELETE", responseType); |
| } |
| |
| @Override |
| public Response head() throws ProcessingException { |
| return method("HEAD"); |
| } |
| |
| @Override |
| public Response options() throws ProcessingException { |
| return method("OPTIONS"); |
| } |
| |
| @Override |
| public <T> T options(final Class<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("OPTIONS", responseType); |
| } |
| |
| @Override |
| public <T> T options(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("OPTIONS", responseType); |
| } |
| |
| @Override |
| public Response trace() throws ProcessingException { |
| return method("TRACE"); |
| } |
| |
| @Override |
| public <T> T trace(final Class<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("TRACE", responseType); |
| } |
| |
| @Override |
| public <T> T trace(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { |
| return method("TRACE", responseType); |
| } |
| |
| @Override |
| public Response method(final String name) throws ProcessingException { |
| requestContext.setMethod(name); |
| return new JerseyInvocation(this).invoke(); |
| } |
| |
| @Override |
| public <T> T method(final String name, final Class<T> responseType) throws ProcessingException, WebApplicationException { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| requestContext.setMethod(name); |
| return new JerseyInvocation(this).invoke(responseType); |
| } |
| |
| @Override |
| public <T> T method(final String name, final GenericType<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| requestContext.setMethod(name); |
| return new JerseyInvocation(this).invoke(responseType); |
| } |
| |
| @Override |
| public Response method(final String name, final Entity<?> entity) throws ProcessingException { |
| requestContext.setMethod(name); |
| storeEntity(entity); |
| return new JerseyInvocation(this).invoke(); |
| } |
| |
| @Override |
| public <T> T method(final String name, final Entity<?> entity, final Class<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| requestContext.setMethod(name); |
| storeEntity(entity); |
| return new JerseyInvocation(this).invoke(responseType); |
| } |
| |
| @Override |
| public <T> T method(final String name, final Entity<?> entity, final GenericType<T> responseType) |
| throws ProcessingException, WebApplicationException { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| requestContext.setMethod(name); |
| storeEntity(entity); |
| return new JerseyInvocation(this).invoke(responseType); |
| } |
| |
| @Override |
| public Builder property(final String name, final Object value) { |
| requestContext.setProperty(name, value); |
| return this; |
| } |
| |
| @Override |
| public CompletionStageRxInvoker rx() { |
| return rx(JerseyCompletionStageRxInvoker.class); |
| } |
| |
| @Override |
| public <T extends RxInvoker> T rx(Class<T> clazz) { |
| if (clazz == JerseyCompletionStageRxInvoker.class) { |
| final ExecutorService configured = request().getClientConfig().getExecutorService(); |
| if (configured == null) { |
| final ExecutorService provided = executorService(); |
| if (provided != null) { |
| request().getClientConfig().executorService(provided); |
| } |
| } |
| return (T) new JerseyCompletionStageRxInvoker(this); |
| } |
| return createRxInvoker(clazz, executorService()); |
| } |
| |
| private <T extends RxInvoker> T rx(Class<T> clazz, ExecutorService executorService) { |
| if (executorService == null) { |
| throw new IllegalArgumentException(LocalizationMessages.NULL_INPUT_PARAMETER("executorService")); |
| } |
| |
| return createRxInvoker(clazz, executorService); |
| } |
| |
| // get executor service from explicit configuration; if not available, get executor service from provider |
| private ExecutorService executorService() { |
| final ExecutorService result = request().getClientConfig().getExecutorService(); |
| |
| if (result != null) { |
| return result; |
| } |
| |
| return this.requestContext.getInjectionManager() |
| .getInstance(ExecutorServiceProvider.class) |
| .getExecutorService(); |
| } |
| |
| /** |
| * Create {@link RxInvoker} from provided {@code RxInvoker} subclass. |
| * <p> |
| * The method does a lookup for {@link RxInvokerProvider}, which provides given {@code RxInvoker} subclass |
| * and if found, calls {@link RxInvokerProvider#getRxInvoker(SyncInvoker, ExecutorService)} |
| * |
| * @param clazz {@code RxInvoker} subclass to be created. |
| * @param executorService to be passed to the factory method invocation. |
| * @param <T> {@code RxInvoker} subclass to be returned. |
| * @return thread safe instance of {@code RxInvoker} subclass. |
| * @throws IllegalStateException when provider for given class is not registered. |
| */ |
| private <T extends RxInvoker> T createRxInvoker(Class<? extends RxInvoker> clazz, |
| ExecutorService executorService) { |
| if (clazz == null) { |
| throw new IllegalArgumentException(LocalizationMessages.NULL_INPUT_PARAMETER("clazz")); |
| } |
| |
| Iterable<RxInvokerProvider> allProviders = Providers.getAllProviders( |
| this.requestContext.getInjectionManager(), |
| RxInvokerProvider.class); |
| |
| for (RxInvokerProvider invokerProvider : allProviders) { |
| if (invokerProvider.isProviderFor(clazz)) { |
| |
| RxInvoker rxInvoker = invokerProvider.getRxInvoker(this, executorService); |
| |
| if (rxInvoker == null) { |
| throw new IllegalStateException(LocalizationMessages.CLIENT_RX_PROVIDER_NULL()); |
| } |
| |
| return (T) rxInvoker; |
| } |
| } |
| |
| throw new IllegalStateException( |
| LocalizationMessages.CLIENT_RX_PROVIDER_NOT_REGISTERED(clazz.getSimpleName())); |
| } |
| } |
| |
| /* package */ static class AsyncInvoker extends CompletableFutureAsyncInvoker implements javax.ws.rs.client.AsyncInvoker { |
| |
| private final JerseyInvocation.Builder builder; |
| |
| /* package */ AsyncInvoker(final JerseyInvocation.Builder request) { |
| this.builder = request; |
| this.builder.requestContext.setAsynchronous(true); |
| } |
| |
| @Override |
| public CompletableFuture<Response> method(final String name) { |
| builder.requestContext.setMethod(name); |
| return (CompletableFuture<Response>) new JerseyInvocation(builder).submit(); |
| } |
| |
| @Override |
| public <T> CompletableFuture<T> method(final String name, final Class<T> responseType) { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| builder.requestContext.setMethod(name); |
| return (CompletableFuture<T>) new JerseyInvocation(builder).submit(responseType); |
| } |
| |
| @Override |
| public <T> CompletableFuture<T> method(final String name, final GenericType<T> responseType) { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| builder.requestContext.setMethod(name); |
| return (CompletableFuture<T>) new JerseyInvocation(builder).submit(responseType); |
| } |
| |
| @Override |
| public <T> CompletableFuture<T> method(final String name, final InvocationCallback<T> callback) { |
| builder.requestContext.setMethod(name); |
| return (CompletableFuture<T>) new JerseyInvocation(builder).submit(callback); |
| } |
| |
| @Override |
| public CompletableFuture<Response> method(final String name, final Entity<?> entity) { |
| builder.requestContext.setMethod(name); |
| builder.storeEntity(entity); |
| return (CompletableFuture<Response>) new JerseyInvocation(builder).submit(); |
| } |
| |
| @Override |
| public <T> CompletableFuture<T> method(final String name, final Entity<?> entity, final Class<T> responseType) { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| builder.requestContext.setMethod(name); |
| builder.storeEntity(entity); |
| return (CompletableFuture<T>) new JerseyInvocation(builder).submit(responseType); |
| } |
| |
| @Override |
| public <T> CompletableFuture<T> method(final String name, final Entity<?> entity, final GenericType<T> responseType) { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| builder.requestContext.setMethod(name); |
| builder.storeEntity(entity); |
| return (CompletableFuture<T>) new JerseyInvocation(builder).submit(responseType); |
| } |
| |
| @Override |
| public <T> CompletableFuture<T> method(final String name, final Entity<?> entity, final InvocationCallback<T> callback) { |
| builder.requestContext.setMethod(name); |
| builder.storeEntity(entity); |
| return (CompletableFuture<T>) new JerseyInvocation(builder).submit(callback); |
| } |
| } |
| |
| private ClientRequest requestForCall(final ClientRequest requestContext) { |
| return copyRequestContext ? new ClientRequest(requestContext) : requestContext; |
| } |
| |
| @Override |
| public Response invoke() throws ProcessingException, WebApplicationException { |
| final ClientRuntime runtime = request().getClientRuntime(); |
| final RequestScope requestScope = runtime.getRequestScope(); |
| |
| return runInScope(((Producer<Response>) () -> |
| new InboundJaxrsResponse(runtime.invoke(requestForCall(requestContext)), requestScope)), |
| requestScope); |
| } |
| |
| @Override |
| public <T> T invoke(final Class<T> responseType) throws ProcessingException, WebApplicationException { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| final ClientRuntime runtime = request().getClientRuntime(); |
| final RequestScope requestScope = runtime.getRequestScope(); |
| |
| return runInScope(() -> |
| translate(runtime.invoke(requestForCall(requestContext)), requestScope, responseType), requestScope); |
| } |
| |
| @Override |
| public <T> T invoke(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| final ClientRuntime runtime = request().getClientRuntime(); |
| final RequestScope requestScope = runtime.getRequestScope(); |
| |
| return runInScope(() -> |
| translate(runtime.invoke(requestForCall(requestContext)), requestScope, responseType), requestScope); |
| } |
| |
| private <T> T runInScope(Producer<T> producer, RequestScope scope) throws ProcessingException, WebApplicationException { |
| return scope.runInScope(() -> call(producer, scope)); |
| } |
| |
| private <T> T call(Producer<T> producer, RequestScope scope) |
| throws ProcessingException, WebApplicationException { |
| try { |
| return producer.call(); |
| } catch (final ClientResponseProcessingException crpe) { |
| throw new ResponseProcessingException( |
| translate(crpe.getClientResponse(), scope, Response.class), crpe.getCause() |
| ); |
| } catch (final ProcessingException ex) { |
| if (WebApplicationException.class.isInstance(ex.getCause())) { |
| throw (WebApplicationException) ex.getCause(); |
| } |
| throw ex; |
| } |
| } |
| |
| @Override |
| public Future<Response> submit() { |
| final CompletableFuture<Response> responseFuture = new CompletableFuture<>(); |
| final ClientRuntime runtime = request().getClientRuntime(); |
| runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), |
| new InvocationResponseCallback<>(responseFuture, (request, scope) -> translate(request, scope, Response.class)))); |
| |
| return responseFuture; |
| } |
| |
| @Override |
| public <T> Future<T> submit(final Class<T> responseType) { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| final CompletableFuture<T> responseFuture = new CompletableFuture<>(); |
| final ClientRuntime runtime = request().getClientRuntime(); |
| |
| runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), |
| new InvocationResponseCallback<T>(responseFuture, (request, scope) -> translate(request, scope, responseType)))); |
| |
| return responseFuture; |
| } |
| |
| private <T> T translate(final ClientResponse response, final RequestScope scope, final Class<T> responseType) |
| throws ProcessingException { |
| if (responseType == Response.class) { |
| return responseType.cast(new InboundJaxrsResponse(response, scope)); |
| } |
| |
| if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { |
| try { |
| return response.readEntity(responseType); |
| } catch (final ProcessingException ex) { |
| if (ex.getClass() == ProcessingException.class) { |
| throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex.getCause()); |
| } |
| throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex); |
| } catch (final WebApplicationException ex) { |
| throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex); |
| } catch (final Exception ex) { |
| throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), |
| LocalizationMessages.UNEXPECTED_ERROR_RESPONSE_PROCESSING(), ex); |
| } |
| } else { |
| throw convertToException(new InboundJaxrsResponse(response, scope)); |
| } |
| } |
| |
| @Override |
| public <T> Future<T> submit(final GenericType<T> responseType) { |
| if (responseType == null) { |
| throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); |
| } |
| final CompletableFuture<T> responseFuture = new CompletableFuture<>(); |
| final ClientRuntime runtime = request().getClientRuntime(); |
| |
| runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), |
| new InvocationResponseCallback<T>(responseFuture, (request, scope) -> translate(request, scope, responseType)))); |
| |
| return responseFuture; |
| } |
| |
| private <T> T translate(final ClientResponse response, final RequestScope scope, final GenericType<T> responseType) |
| throws ProcessingException { |
| if (responseType.getRawType() == Response.class) { |
| //noinspection unchecked |
| return (T) new InboundJaxrsResponse(response, scope); |
| } |
| |
| if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { |
| try { |
| return response.readEntity(responseType); |
| } catch (final ProcessingException ex) { |
| throw new ResponseProcessingException( |
| new InboundJaxrsResponse(response, scope), |
| ex.getCause() != null ? ex.getCause() : ex); |
| } catch (final WebApplicationException ex) { |
| throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex); |
| } catch (final Exception ex) { |
| throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), |
| LocalizationMessages.UNEXPECTED_ERROR_RESPONSE_PROCESSING(), ex); |
| } |
| } else { |
| throw convertToException(new InboundJaxrsResponse(response, scope)); |
| } |
| } |
| |
| @Override |
| public <T> Future<T> submit(final InvocationCallback<T> callback) { |
| return submit(null, callback); |
| } |
| |
| /** |
| * Submit the request for an asynchronous invocation and register an |
| * {@link InvocationCallback} to process the future result of the invocation. |
| * <p> |
| * Response type in this case is taken from {@code responseType} param (if not {@code null}) rather |
| * than from {@code callback}. This allows to pass callbacks like {@code new InvocationCallback<>() {...}}. |
| * </p> |
| * |
| * @param <T> response type |
| * @param responseType response type that is used instead of obtaining types from {@code callback}. |
| * @param callback invocation callback for asynchronous processing of the |
| * request invocation result. |
| * @return future response object of the specified type as a result of the |
| * request invocation. |
| */ |
| public <T> Future<T> submit(final GenericType<T> responseType, final InvocationCallback<T> callback) { |
| final CompletableFuture<T> responseFuture = new CompletableFuture<>(); |
| |
| try { |
| final ReflectionHelper.DeclaringClassInterfacePair pair = |
| ReflectionHelper.getClass(callback.getClass(), InvocationCallback.class); |
| |
| final Type callbackParamType; |
| final Class<T> callbackParamClass; |
| |
| if (responseType == null) { |
| // If we don't have response use callback to obtain param types. |
| final Type[] typeArguments = ReflectionHelper.getParameterizedTypeArguments(pair); |
| if (typeArguments == null || typeArguments.length == 0) { |
| callbackParamType = Object.class; |
| } else { |
| callbackParamType = typeArguments[0]; |
| } |
| callbackParamClass = ReflectionHelper.erasure(callbackParamType); |
| } else { |
| callbackParamType = responseType.getType(); |
| callbackParamClass = ReflectionHelper.erasure(responseType.getRawType()); |
| } |
| |
| final ResponseCallback responseCallback = new ResponseCallback() { |
| |
| @Override |
| public void completed(final ClientResponse response, final RequestScope scope) { |
| if (responseFuture.isCancelled()) { |
| response.close(); |
| failed(new ProcessingException( |
| new CancellationException(LocalizationMessages.ERROR_REQUEST_CANCELLED()))); |
| return; |
| } |
| |
| final T result; |
| if (callbackParamClass == Response.class) { |
| result = callbackParamClass.cast(new InboundJaxrsResponse(response, scope)); |
| responseFuture.complete(result); |
| callback.completed(result); |
| } else if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { |
| result = response.readEntity(new GenericType<T>(callbackParamType)); |
| responseFuture.complete(result); |
| callback.completed(result); |
| } else { |
| failed(convertToException(new InboundJaxrsResponse(response, scope))); |
| } |
| } |
| |
| @Override |
| public void failed(final ProcessingException error) { |
| Exception called = null; |
| try { |
| if (error.getCause() instanceof WebApplicationException) { |
| responseFuture.completeExceptionally(error.getCause()); |
| } else if (!responseFuture.isCancelled()) { |
| try { |
| call(() -> { throw error; }, null); |
| } catch (Exception ex) { |
| called = ex; |
| responseFuture.completeExceptionally(ex); |
| } |
| } |
| } finally { |
| callback.failed( |
| error.getCause() instanceof CancellationException |
| ? error.getCause() |
| : called != null ? called : error |
| ); |
| } |
| } |
| }; |
| final ClientRuntime runtime = request().getClientRuntime(); |
| runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), responseCallback)); |
| } catch (final Throwable error) { |
| final ProcessingException ce; |
| //noinspection ChainOfInstanceofChecks |
| if (error instanceof ClientResponseProcessingException) { |
| ce = new ProcessingException(error.getCause()); |
| responseFuture.completeExceptionally(ce); |
| } else if (error instanceof ProcessingException) { |
| ce = (ProcessingException) error; |
| responseFuture.completeExceptionally(ce); |
| } else if (error instanceof WebApplicationException) { |
| ce = new ProcessingException(error); |
| responseFuture.completeExceptionally(error); |
| } else { |
| ce = new ProcessingException(error); |
| responseFuture.completeExceptionally(ce); |
| } |
| callback.failed(ce); |
| } |
| |
| return responseFuture; |
| } |
| |
| @Override |
| public JerseyInvocation property(final String name, final Object value) { |
| requestContext.setProperty(name, value); |
| return this; |
| } |
| |
| private ProcessingException convertToException(final Response response) { |
| try { |
| // Buffer and close entity input stream (if any) to prevent |
| // leaking connections (see JERSEY-2157). |
| response.bufferEntity(); |
| |
| final WebApplicationException webAppException; |
| final int statusCode = response.getStatus(); |
| final Response.Status status = Response.Status.fromStatusCode(statusCode); |
| |
| if (status == null) { |
| final Response.Status.Family statusFamily = response.getStatusInfo().getFamily(); |
| webAppException = createExceptionForFamily(response, statusFamily); |
| } else { |
| switch (status) { |
| case BAD_REQUEST: |
| webAppException = new BadRequestException(response); |
| break; |
| case UNAUTHORIZED: |
| webAppException = new NotAuthorizedException(response); |
| break; |
| case FORBIDDEN: |
| webAppException = new ForbiddenException(response); |
| break; |
| case NOT_FOUND: |
| webAppException = new NotFoundException(response); |
| break; |
| case METHOD_NOT_ALLOWED: |
| webAppException = new NotAllowedException(response); |
| break; |
| case NOT_ACCEPTABLE: |
| webAppException = new NotAcceptableException(response); |
| break; |
| case UNSUPPORTED_MEDIA_TYPE: |
| webAppException = new NotSupportedException(response); |
| break; |
| case INTERNAL_SERVER_ERROR: |
| webAppException = new InternalServerErrorException(response); |
| break; |
| case SERVICE_UNAVAILABLE: |
| webAppException = new ServiceUnavailableException(response); |
| break; |
| default: |
| final Response.Status.Family statusFamily = response.getStatusInfo().getFamily(); |
| webAppException = createExceptionForFamily(response, statusFamily); |
| } |
| } |
| |
| return new ResponseProcessingException(response, webAppException); |
| } catch (final Throwable t) { |
| return new ResponseProcessingException(response, LocalizationMessages.RESPONSE_TO_EXCEPTION_CONVERSION_FAILED(), t); |
| } |
| } |
| |
| private WebApplicationException createExceptionForFamily(final Response response, final Response.Status.Family statusFamily) { |
| final WebApplicationException webAppException; |
| switch (statusFamily) { |
| case REDIRECTION: |
| webAppException = new RedirectionException(response); |
| break; |
| case CLIENT_ERROR: |
| webAppException = new ClientErrorException(response); |
| break; |
| case SERVER_ERROR: |
| webAppException = new ServerErrorException(response); |
| break; |
| default: |
| webAppException = new WebApplicationException(response); |
| } |
| return webAppException; |
| } |
| |
| /** |
| * Returns a reference to the mutable request context to be invoked. |
| * |
| * @return mutable request context to be invoked. |
| */ |
| ClientRequest request() { |
| return requestContext; |
| } |
| |
| @Override |
| public String toString() { |
| return "JerseyInvocation [" + request().getMethod() + ' ' + request().getUri() + "]"; |
| } |
| |
| private class InvocationResponseCallback<R> implements ResponseCallback { |
| private final CompletableFuture<R> responseFuture; |
| private final BiFunction<ClientResponse, RequestScope, R> producer; |
| |
| private InvocationResponseCallback(CompletableFuture<R> responseFuture, |
| BiFunction<ClientResponse, RequestScope, R> producer) { |
| this.responseFuture = responseFuture; |
| this.producer = producer; |
| } |
| |
| @Override |
| public void completed(final ClientResponse response, final RequestScope scope) { |
| if (responseFuture.isCancelled()) { |
| response.close(); |
| return; |
| } |
| |
| |
| try { |
| responseFuture.complete(producer.apply(response, scope)); |
| } catch (final ProcessingException ex) { |
| failed(ex); |
| } |
| } |
| |
| @Override |
| public void failed(final ProcessingException error) { |
| if (responseFuture.isCancelled()) { |
| return; |
| } |
| |
| try { |
| call(() -> { |
| throw error; |
| }, null); |
| } catch (Exception exception) { |
| responseFuture.completeExceptionally(exception); |
| } |
| } |
| } |
| } |