org.glassfish.jersey.client.JerseyInvocation Maven / Gradle / Ivy
Show all versions of jaxrs-ri Show documentation
/*
* Copyright (c) 2011, 2024 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.List;
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 jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.NotAcceptableException;
import jakarta.ws.rs.NotAllowedException;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.NotSupportedException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.RedirectionException;
import jakarta.ws.rs.ServerErrorException;
import jakarta.ws.rs.ServiceUnavailableException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.client.CompletionStageRxInvoker;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.InvocationCallback;
import jakarta.ws.rs.client.ResponseProcessingException;
import jakarta.ws.rs.client.RxInvoker;
import jakarta.ws.rs.client.RxInvokerProvider;
import jakarta.ws.rs.client.SyncInvoker;
import jakarta.ws.rs.core.CacheControl;
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.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.Bindings;
import org.glassfish.jersey.internal.inject.DisposableSupplier;
import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.internal.inject.ServiceHolder;
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.ExecutorProviders;
import org.glassfish.jersey.process.internal.RequestScope;
import org.glassfish.jersey.spi.ExecutorServiceProvider;
/**
* Jersey implementation of {@link jakarta.ws.rs.client.Invocation JAX-RS client-side
* request invocation} contract.
*
* @author Marek Potociar
*/
public class JerseyInvocation implements jakarta.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 boolean ignoreResponseException;
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;
Object value = builder.requestContext.getConfiguration()
.getProperty(ClientProperties.IGNORE_EXCEPTION_RESPONSE);
if (value != null) {
Boolean booleanValue = PropertiesHelper.convertValue(value, Boolean.class);
if (booleanValue != null) {
this.ignoreResponseException = booleanValue;
}
}
}
private enum EntityPresence {
MUST_BE_NULL,
MUST_BE_PRESENT,
OPTIONAL
}
private static final Map METHODS = initializeMap();
private static Map initializeMap() {
final Map 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.OPTIONAL);
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 jakarta.ws.rs.client.Invocation.Builder client invocation builder}.
*/
public static class Builder implements jakarta.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 jakarta.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 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 headers) {
requestContext.replaceHeaders(headers);
return this;
}
@Override
public Response get() throws ProcessingException {
return method("GET");
}
@Override
public T get(final Class responseType) throws ProcessingException, WebApplicationException {
return method("GET", responseType);
}
@Override
public T get(final GenericType responseType) throws ProcessingException, WebApplicationException {
return method("GET", responseType);
}
@Override
public Response put(final Entity> entity) throws ProcessingException {
return method("PUT", entity);
}
@Override
public T put(final Entity> entity, final Class responseType)
throws ProcessingException, WebApplicationException {
return method("PUT", entity, responseType);
}
@Override
public T put(final Entity> entity, final GenericType 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 post(final Entity> entity, final Class responseType)
throws ProcessingException, WebApplicationException {
return method("POST", entity, responseType);
}
@Override
public T post(final Entity> entity, final GenericType responseType)
throws ProcessingException, WebApplicationException {
return method("POST", entity, responseType);
}
@Override
public Response delete() throws ProcessingException {
return method("DELETE");
}
@Override
public T delete(final Class responseType) throws ProcessingException, WebApplicationException {
return method("DELETE", responseType);
}
@Override
public T delete(final GenericType 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 options(final Class responseType) throws ProcessingException, WebApplicationException {
return method("OPTIONS", responseType);
}
@Override
public T options(final GenericType responseType) throws ProcessingException, WebApplicationException {
return method("OPTIONS", responseType);
}
@Override
public Response trace() throws ProcessingException {
return method("TRACE");
}
@Override
public T trace(final Class responseType) throws ProcessingException, WebApplicationException {
return method("TRACE", responseType);
}
@Override
public T trace(final GenericType 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 method(final String name, final Class 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 method(final String name, final GenericType 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 method(final String name, final Entity> entity, final Class 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 method(final String name, final Entity> entity, final GenericType 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 rx(Class clazz) {
if (clazz == JerseyCompletionStageRxInvoker.class) {
final ExecutorService configured = request().getClientConfig().getExecutorService();
if (configured == null) {
final ExecutorService provided = executorService();
if (provided != null) {
((ClientConfig) request().getConfiguration()).executorService(provided);
}
}
return (T) new JerseyCompletionStageRxInvoker(this);
}
return createRxInvoker(clazz, executorService());
}
private T rx(Class 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;
}
final List> serviceHolders =
this.requestContext.getInjectionManager().getAllServiceHolders(ExecutorServiceProvider.class);
BestServiceHolder best = serviceHolders.stream()
.map(BestServiceHolder::new).sorted((a, b) -> a.isBetterThen(b) ? -1 : 1).findFirst().get();
return best.provider.getExecutorService();
}
/*
* Priority goes to: 1) user async
* 2) user nonasync
* 3) default async
*/
private static final class BestServiceHolder {
private final ExecutorServiceProvider provider;
private final int value;
private BestServiceHolder(ServiceHolder holder) {
provider = holder.getInstance();
boolean isDefault = DefaultClientAsyncExecutorProvider.class.equals(holder.getImplementationClass())
|| ClientExecutorProvidersConfigurator.ClientExecutorServiceProvider.class
.equals(holder.getImplementationClass());
boolean isAsync = holder.getImplementationClass().getAnnotation(ClientAsyncExecutor.class) != null;
value = 10 * (isDefault ? 0 : 1) + (isAsync ? 1 : 0);
}
public boolean isBetterThen(BestServiceHolder other) {
return this.value > other.value;
}
}
/**
* Create {@link RxInvoker} from provided {@code RxInvoker} subclass.
*
* 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 {@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 createRxInvoker(Class extends RxInvoker> clazz,
ExecutorService executorService) {
if (clazz == null) {
throw new IllegalArgumentException(LocalizationMessages.NULL_INPUT_PARAMETER("clazz"));
}
Iterable 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()));
}
/**
* Sets Future that backs {@link ClientRequest} {@link ClientRequest#isCancelled()} method. Can be used for instance
* by {@link CompletionStageRxInvoker} to pass the created {@link CompletableFuture} to the provided {@link SyncInvoker}.
* @param cancellable the {@link Future} whose result of {@link Future#cancel(boolean)} will be available by
* {@link ClientRequest#isCancelled()}.
* @return the updated builder.
*/
public Builder setCancellable(Future cancellable) {
requestContext.setCancellable(cancellable);
return this;
}
}
/* package */ static class AsyncInvoker extends CompletableFutureAsyncInvoker implements jakarta.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 method(final String name) {
builder.requestContext.setMethod(name);
return (CompletableFuture) new JerseyInvocation(builder).submit();
}
@Override
public CompletableFuture method(final String name, final Class responseType) {
if (responseType == null) {
throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL());
}
builder.requestContext.setMethod(name);
return (CompletableFuture) new JerseyInvocation(builder).submit(responseType);
}
@Override
public CompletableFuture method(final String name, final GenericType responseType) {
if (responseType == null) {
throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL());
}
builder.requestContext.setMethod(name);
return (CompletableFuture) new JerseyInvocation(builder).submit(responseType);
}
@Override
public CompletableFuture method(final String name, final InvocationCallback callback) {
builder.requestContext.setMethod(name);
return (CompletableFuture) new JerseyInvocation(builder).submit(callback);
}
@Override
public CompletableFuture method(final String name, final Entity> entity) {
builder.requestContext.setMethod(name);
builder.storeEntity(entity);
return (CompletableFuture) new JerseyInvocation(builder).submit();
}
@Override
public CompletableFuture method(final String name, final Entity> entity, final Class responseType) {
if (responseType == null) {
throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL());
}
builder.requestContext.setMethod(name);
builder.storeEntity(entity);
return (CompletableFuture) new JerseyInvocation(builder).submit(responseType);
}
@Override
public CompletableFuture method(final String name, final Entity> entity, final GenericType responseType) {
if (responseType == null) {
throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL());
}
builder.requestContext.setMethod(name);
builder.storeEntity(entity);
return (CompletableFuture) new JerseyInvocation(builder).submit(responseType);
}
@Override
public CompletableFuture method(final String name, final Entity> entity, final InvocationCallback callback) {
builder.requestContext.setMethod(name);
builder.storeEntity(entity);
return (CompletableFuture) 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) () ->
new InboundJaxrsResponse(runtime.invoke(requestForCall(requestContext)), requestScope)),
requestScope);
}
@Override
public T invoke(final Class 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 invoke(final GenericType 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 runInScope(Producer producer, RequestScope scope) throws ProcessingException, WebApplicationException {
return scope.runInScope(() -> call(producer, scope));
}
private T call(Producer 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 submit() {
final CompletableFuture responseFuture = new CompletableFuture<>();
final ClientRuntime runtime = request().getClientRuntime();
requestContext.setCancellable(responseFuture);
runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext),
new InvocationResponseCallback<>(responseFuture, (request, scope) -> translate(request, scope, Response.class))));
return responseFuture;
}
@Override
public Future submit(final Class responseType) {
if (responseType == null) {
throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL());
}
final CompletableFuture responseFuture = new CompletableFuture<>();
final ClientRuntime runtime = request().getClientRuntime();
requestContext.setCancellable(responseFuture);
runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext),
new InvocationResponseCallback(responseFuture, (request, scope) -> translate(request, scope, responseType))));
return responseFuture;
}
private T translate(final ClientResponse response, final RequestScope scope, final Class 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 Future submit(final GenericType responseType) {
if (responseType == null) {
throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL());
}
final CompletableFuture responseFuture = new CompletableFuture<>();
final ClientRuntime runtime = request().getClientRuntime();
requestContext.setCancellable(responseFuture);
runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext),
new InvocationResponseCallback(responseFuture, (request, scope) -> translate(request, scope, responseType))));
return responseFuture;
}
private T translate(final ClientResponse response, final RequestScope scope, final GenericType 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 Future submit(final InvocationCallback 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.
*
* 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<>() {...}}.
*
*
* @param 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 Future submit(final GenericType responseType, final InvocationCallback callback) {
final CompletableFuture responseFuture = new CompletableFuture<>();
try {
final ReflectionHelper.DeclaringClassInterfacePair pair =
ReflectionHelper.getClass(callback.getClass(), InvocationCallback.class);
final Type callbackParamType;
final Class 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(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();
requestContext.setCancellable(responseFuture);
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) {
// Use an empty response if ignoring response in exception
final int statusCode = response.getStatus();
final Response finalResponse = ignoreResponseException ? Response.status(statusCode).build() : response;
try {
// Buffer and close entity input stream (if any) to prevent
// leaking connections (see JERSEY-2157).
response.bufferEntity();
final WebApplicationException webAppException;
final Response.Status status = Response.Status.fromStatusCode(statusCode);
if (status == null) {
final Response.Status.Family statusFamily = finalResponse.getStatusInfo().getFamily();
webAppException = createExceptionForFamily(finalResponse, statusFamily);
} else {
switch (status) {
case BAD_REQUEST:
webAppException = new BadRequestException(finalResponse);
break;
case UNAUTHORIZED:
webAppException = new NotAuthorizedException(finalResponse);
break;
case FORBIDDEN:
webAppException = new ForbiddenException(finalResponse);
break;
case NOT_FOUND:
webAppException = new NotFoundException(finalResponse);
break;
case METHOD_NOT_ALLOWED:
webAppException = new NotAllowedException(finalResponse);
break;
case NOT_ACCEPTABLE:
webAppException = new NotAcceptableException(finalResponse);
break;
case UNSUPPORTED_MEDIA_TYPE:
webAppException = new NotSupportedException(finalResponse);
break;
case INTERNAL_SERVER_ERROR:
webAppException = new InternalServerErrorException(finalResponse);
break;
case SERVICE_UNAVAILABLE:
webAppException = new ServiceUnavailableException(finalResponse);
break;
default:
final Response.Status.Family statusFamily = finalResponse.getStatusInfo().getFamily();
webAppException = createExceptionForFamily(finalResponse, statusFamily);
}
}
return new ResponseProcessingException(finalResponse, webAppException);
} catch (final Throwable t) {
return new ResponseProcessingException(finalResponse,
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 implements ResponseCallback {
private final CompletableFuture responseFuture;
private final BiFunction producer;
private InvocationResponseCallback(CompletableFuture responseFuture,
BiFunction 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);
}
}
}
}