diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java index 672e6bb05c..c53f871c02 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java @@ -17,22 +17,18 @@ package org.glassfish.jersey.client; import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.ClientRequestFilter; import javax.ws.rs.client.ClientResponseFilter; import javax.ws.rs.client.ResponseProcessingException; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ReaderInterceptor; -import org.glassfish.jersey.client.internal.routing.AbortedRequestMediaTypeDeterminer; +import org.glassfish.jersey.client.internal.routing.ClientResponseMediaTypeDeterminer; import org.glassfish.jersey.internal.inject.InjectionManager; import org.glassfish.jersey.internal.inject.Providers; -import org.glassfish.jersey.message.internal.HeaderUtils; -import org.glassfish.jersey.message.internal.InboundMessageContext; -import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; import org.glassfish.jersey.model.internal.RankedComparator; import org.glassfish.jersey.process.internal.AbstractChainableStage; import org.glassfish.jersey.process.internal.ChainableStage; @@ -62,6 +58,29 @@ static ChainableStage createRequestFilteringStage(InjectionManage return requestFilters.iterator().hasNext() ? new RequestFilteringStage(requestFilters) : null; } + /** + * Create client request filtering stage using the injection manager. May return {@code null}. + * + * @param firstFilter Non null {@link ClientRequestFilter client request filter} to be executed + * in the client request filtering stage. + * @param injectionManager injection manager to be used. + * @return configured request filtering stage, or {@code null} in case there are no + * {@link ClientRequestFilter client request filters} registered in the injection manager + * and {@code firstFilter} is null. + */ + static ChainableStage createRequestFilteringStage(ClientRequestFilter firstFilter, + InjectionManager injectionManager) { + RankedComparator comparator = new RankedComparator<>(RankedComparator.Order.ASCENDING); + Iterable requestFilters = + Providers.getAllProviders(injectionManager, ClientRequestFilter.class, comparator); + if (firstFilter != null && !requestFilters.iterator().hasNext()) { + return new RequestFilteringStage(Collections.singletonList(firstFilter)); + } else if (firstFilter != null && requestFilters.iterator().hasNext()) { + return new RequestFilteringStage(prependFilter(firstFilter, requestFilters)); + } + return null; + } + /** * Create client response filtering stage using the injection manager. May return {@code null}. * @@ -76,6 +95,39 @@ static ChainableStage createResponseFilteringStage(InjectionMana return responseFilters.iterator().hasNext() ? new ResponseFilterStage(responseFilters) : null; } + /** + * Prepend an filter to a given iterable. + * @param filter to be prepend. + * @param filters the iterable the given filter is to be prependto + * @param filter type + * @return iterable with first item of prepended filter. + */ + private static Iterable prependFilter(T filter, Iterable filters) { + return new Iterable() { + boolean wasInterceptorFilterNext = false; + final Iterator filterIterator = filters.iterator(); + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return !wasInterceptorFilterNext || filterIterator.hasNext(); + } + + @Override + public T next() { + if (wasInterceptorFilterNext) { + return filterIterator.next(); + } else { + wasInterceptorFilterNext = true; + return filter; + } + } + }; + } + }; + } + private static final class RequestFilteringStage extends AbstractChainableStage { private final Iterable requestFilters; @@ -91,24 +143,9 @@ public Continuation apply(ClientRequest requestContext) { filter.filter(requestContext); final Response abortResponse = requestContext.getAbortResponse(); if (abortResponse != null) { - if (abortResponse.hasEntity() && abortResponse.getMediaType() == null) { - final InboundMessageContext headerContext - = new InboundMessageContext(requestContext.getConfiguration()) { - @Override - protected Iterable getReaderInterceptors() { - return null; - } - }; - headerContext.headers( - HeaderUtils.asStringHeaders(abortResponse.getHeaders(), requestContext.getConfiguration()) - ); - - final AbortedRequestMediaTypeDeterminer determiner = new AbortedRequestMediaTypeDeterminer( - requestContext.getWorkers()); - final MediaType mediaType = determiner.determineResponseMediaType(abortResponse.getEntity(), - headerContext.getQualifiedAcceptableMediaTypes()); - abortResponse.getHeaders().add(HttpHeaders.CONTENT_TYPE, mediaType); - } + final ClientResponseMediaTypeDeterminer determiner = new ClientResponseMediaTypeDeterminer( + requestContext.getWorkers()); + determiner.setResponseMediaTypeIfNotSet(abortResponse, requestContext.getConfiguration()); throw new AbortException(new ClientResponse(requestContext, abortResponse)); } } catch (IOException ex) { diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java index fb90a0305a..9d5ef02077 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java @@ -78,6 +78,9 @@ class ClientRuntime implements JerseyClient.ShutdownHook, ClientExecutor { private final ManagedObjectsFinalizer managedObjectsFinalizer; private final InjectionManager injectionManager; + private final InvocationInterceptorStages.PreInvocationInterceptorStage preInvocationInterceptorStage; + private final InvocationInterceptorStages.PostInvocationInterceptorStage postInvocationInterceptorStage; + /** * Create new client request processing runtime. * @@ -95,7 +98,14 @@ public ClientRuntime(final ClientConfig config, final Connector connector, final Stage.Builder requestingChainBuilder = Stages.chain(requestProcessingInitializationStage); - ChainableStage requestFilteringStage = ClientFilteringStages.createRequestFilteringStage(injectionManager); + preInvocationInterceptorStage = InvocationInterceptorStages.createPreInvocationInterceptorStage(injectionManager); + postInvocationInterceptorStage = InvocationInterceptorStages.createPostInvocationInterceptorStage(injectionManager); + + ChainableStage requestFilteringStage = preInvocationInterceptorStage.hasPreInvocationInterceptors() + ? ClientFilteringStages.createRequestFilteringStage( + preInvocationInterceptorStage.createPreInvocationInterceptorFilter(), injectionManager) + : ClientFilteringStages.createRequestFilteringStage(injectionManager); + this.requestProcessingRoot = requestFilteringStage != null ? requestingChainBuilder.build(requestFilteringStage) : requestingChainBuilder.build(); @@ -136,14 +146,22 @@ public ClientRuntime(final ClientConfig config, final Connector connector, final * @return {@code Runnable} to be submitted for async processing using {@link #submit(Runnable)}. */ Runnable createRunnableForAsyncProcessing(ClientRequest request, final ResponseCallback callback) { + try { + requestScope.runInScope(() -> preInvocationInterceptorStage.beforeRequest(request)); + } catch (Throwable throwable) { + return () -> requestScope.runInScope(() -> processFailure(request, throwable, callback)); + } + return () -> requestScope.runInScope(() -> { + RuntimeException runtimeException = null; try { ClientRequest processedRequest; + try { processedRequest = Stages.process(request, requestProcessingRoot); processedRequest = addUserAgent(processedRequest, connector.getName()); } catch (final AbortException aborted) { - processResponse(aborted.getAbortResponse(), callback); + processResponse(request, aborted.getAbortResponse(), callback); return; } @@ -151,18 +169,18 @@ Runnable createRunnableForAsyncProcessing(ClientRequest request, final ResponseC @Override public void response(final ClientResponse response) { - requestScope.runInScope(() -> processResponse(response, callback)); + requestScope.runInScope(() -> processResponse(request, response, callback)); } @Override public void failure(final Throwable failure) { - requestScope.runInScope(() -> processFailure(failure, callback)); + requestScope.runInScope(() -> processFailure(request, failure, callback)); } }; connector.apply(processedRequest, connectorCallback); } catch (final Throwable throwable) { - processFailure(throwable, callback); + processFailure(request, throwable, callback); } }); } @@ -192,17 +210,38 @@ public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) return backgroundScheduler.get().schedule(command, delay, unit); } - private void processResponse(final ClientResponse response, final ResponseCallback callback) { - final ClientResponse processedResponse; + private void processResponse(final ClientRequest request, final ClientResponse response, final ResponseCallback callback) { + ClientResponse processedResponse = null; + Throwable caught = null; try { processedResponse = Stages.process(response, responseProcessingRoot); } catch (final Throwable throwable) { + caught = throwable; + } + + try { + processedResponse = postInvocationInterceptorStage.afterRequest(request, processedResponse, caught); + } catch (Throwable throwable) { processFailure(throwable, callback); return; } callback.completed(processedResponse, requestScope); } + private void processFailure(final ClientRequest request, final Throwable failure, final ResponseCallback callback) { + if (postInvocationInterceptorStage.hasPostInvocationInterceptor()) { + try { + final ClientResponse clientResponse = postInvocationInterceptorStage.afterRequest(request, null, failure); + callback.completed(clientResponse, requestScope); + } catch (RuntimeException e) { + final Throwable t = e.getSuppressed().length == 1 && e.getSuppressed()[0] == failure ? failure : e; + processFailure(t, callback); + } + } else { + processFailure(failure, callback); + } + } + private void processFailure(final Throwable failure, final ResponseCallback callback) { callback.failed(failure instanceof ProcessingException ? (ProcessingException) failure : new ProcessingException(failure)); @@ -248,19 +287,25 @@ private ClientRequest addUserAgent(final ClientRequest clientRequest, final Stri * @throws javax.ws.rs.ProcessingException in case of an invocation failure. */ public ClientResponse invoke(final ClientRequest request) { - ClientResponse response; + ProcessingException processingException = null; + ClientResponse response = null; try { + preInvocationInterceptorStage.beforeRequest(request); + try { response = connector.apply(addUserAgent(Stages.process(request, requestProcessingRoot), connector.getName())); } catch (final AbortException aborted) { response = aborted.getAbortResponse(); } - return Stages.process(response, responseProcessingRoot); + response = Stages.process(response, responseProcessingRoot); } catch (final ProcessingException pe) { - throw pe; + processingException = pe; } catch (final Throwable t) { - throw new ProcessingException(t.getMessage(), t); + processingException = new ProcessingException(t.getMessage(), t); + } finally { + response = postInvocationInterceptorStage.afterRequest(request, response, processingException); + return response; } } diff --git a/core-client/src/main/java/org/glassfish/jersey/client/InvocationInterceptorStages.java b/core-client/src/main/java/org/glassfish/jersey/client/InvocationInterceptorStages.java new file mode 100644 index 0000000000..670d9e0680 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/InvocationInterceptorStages.java @@ -0,0 +1,615 @@ +/* + * Copyright (c) 2019 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 org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.internal.routing.ClientResponseMediaTypeDeterminer; +import org.glassfish.jersey.client.spi.PostInvocationInterceptor; +import org.glassfish.jersey.client.spi.PreInvocationInterceptor; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.model.internal.RankedComparator; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.Collection; +import java.util.Date; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility class for {@link PreInvocationInterceptor} and {@link PostInvocationInterceptor} execution. + * + * @since 2.30 + */ +class InvocationInterceptorStages { + + private static final Logger LOGGER = Logger.getLogger(InvocationInterceptorStages.class.getName()); + private InvocationInterceptorStages() { + // prevent instantiation + } + + /** + * Create a {@link PreInvocationInterceptorStage} executing all {@link PreInvocationInterceptor PreInvocationInterceptors}. + * + * @param injectionManager the injection manager providing the registered {@code PreInvocationInterceptors}. + * @return {@code PreInvocationInterceptorStage} class to execute all the {@code PreInvocationInterceptors}. + */ + static PreInvocationInterceptorStage createPreInvocationInterceptorStage(InjectionManager injectionManager) { + return new PreInvocationInterceptorStage(injectionManager); + } + + /** + * Create a {@link PostInvocationInterceptorStage} executing all {@link PostInvocationInterceptor PostInvocationInterceptors}. + * + * @param injectionManager the injection manager providing the registered {@code PostInvocationInterceptors}. + * @return {@code PostInvocationInterceptorStage} class to execute all the {@code PostInvocationInterceptors}. + */ + static PostInvocationInterceptorStage createPostInvocationInterceptorStage(InjectionManager injectionManager) { + return new PostInvocationInterceptorStage(injectionManager); + } + + /** + * The stage to execute all the {@link PreInvocationInterceptor PreInvocationInterceptors}. + */ + static class PreInvocationInterceptorStage { + private Iterator preInvocationInterceptors; + private PreInvocationInterceptorStage(InjectionManager injectionManager) { + final RankedComparator comparator = + new RankedComparator<>(RankedComparator.Order.DESCENDING); + preInvocationInterceptors = Providers.getAllProviders(injectionManager, PreInvocationInterceptor.class, comparator) + .iterator(); + } + + /** + * Returns {@code true} if there is a {@link PreInvocationInterceptor} registered not yet executed in the request. + * + * @return {@code true} if there is a {@link PreInvocationInterceptor} yet to be executed. + */ + boolean hasPreInvocationInterceptors() { + return preInvocationInterceptors.hasNext(); + } + + /** + * Execute the {@link PreInvocationInterceptor PreInvocationInterceptors}. + * + * @param request {@link javax.ws.rs.client.ClientRequestContext} to be passed to {@code PreInvocationInterceptor}. + */ + void beforeRequest(ClientRequest request) { + final LinkedList throwables = new LinkedList<>(); + final ClientRequestContext requestContext = new InvocationInterceptorRequestContext(request); + while (preInvocationInterceptors.hasNext()) { + try { + preInvocationInterceptors.next().beforeRequest(requestContext); + } catch (Throwable throwable) { + LOGGER.log(Level.FINE, LocalizationMessages.PREINVOCATION_INTERCEPTOR_EXCEPTION(), throwable); + throwables.add(throwable); + } + } + if (!throwables.isEmpty()) { + throw suppressExceptions(throwables); + } + } + + /** + * Create an empty {@link ClientRequestFilter} to executed the first after all {@code PreInvocationInterceptors} + * for the runtime to handle {@link ClientRequestContext#abortWith(Response)} utilization. + * + * @return an empty {@link ClientRequestFilter}. + */ + ClientRequestFilter createPreInvocationInterceptorFilter() { + return new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + // do nothing, the filtering stage will handle requestContext#abortWith + // set in the PreInvocationInterceptor + } + }; + } + } + + /** + * The stage to execute all the {@link PostInvocationInterceptor PostInvocationInterceptors}. + */ + static class PostInvocationInterceptorStage { + private final Iterator postInvocationInterceptors; + + private PostInvocationInterceptorStage(InjectionManager injectionManager) { + final RankedComparator comparator = + new RankedComparator<>(RankedComparator.Order.ASCENDING); + final Iterable postInvocationInterceptors + = Providers.getAllProviders(injectionManager, PostInvocationInterceptor.class, comparator); + this.postInvocationInterceptors = postInvocationInterceptors.iterator(); + } + + /** + * Returns {@code true} if there is a {@link PostInvocationInterceptor} registered not yet executed in the request. + * + * @return {@code true} if there is a {@link PostInvocationInterceptor} yet to be executed. + */ + boolean hasPostInvocationInterceptor() { + return postInvocationInterceptors.hasNext(); + } + + private ClientResponse afterRequestWithoutException(Iterator postInvocationInterceptors, + InvocationInterceptorRequestContext requestContext, + PostInvocationExceptionContext exceptionContext) { + boolean withoutException = true; + if (postInvocationInterceptors.hasNext()) { + final PostInvocationInterceptor postInvocationInterceptor = postInvocationInterceptors.next(); + try { + postInvocationInterceptor.afterRequest(requestContext, exceptionContext.getResponseContext().get()); + } catch (Throwable throwable) { + LOGGER.log(Level.FINE, LocalizationMessages.POSTINVOCATION_INTERCEPTOR_EXCEPTION(), throwable); + withoutException = false; + exceptionContext.throwables.add(throwable); + } finally { + return withoutException + ? afterRequestWithoutException(postInvocationInterceptors, requestContext, exceptionContext) + : afterRequestWithException(postInvocationInterceptors, requestContext, exceptionContext); + } + } else { + return exceptionContext.responseContext; + } + } + + private ClientResponse afterRequestWithException(Iterator postInvocationInterceptors, + InvocationInterceptorRequestContext requestContext, + PostInvocationExceptionContext exceptionContext) { + Throwable caught = null; + if (postInvocationInterceptors.hasNext()) { + final PostInvocationInterceptor postInvocationInterceptor = postInvocationInterceptors.next(); + try { + postInvocationInterceptor.onException(requestContext, exceptionContext); + } catch (Throwable throwable) { + LOGGER.log(Level.FINE, LocalizationMessages.POSTINVOCATION_INTERCEPTOR_EXCEPTION(), throwable); + caught = throwable; // keep this if handleResponse clears the Throwables + } + + try { + resolveResponse(requestContext, exceptionContext); + } catch (Throwable throwable) { + LOGGER.log(Level.FINE, LocalizationMessages.POSTINVOCATION_INTERCEPTOR_EXCEPTION(), throwable); + exceptionContext.throwables.add(throwable); + } finally { + if (caught != null) { + exceptionContext.throwables.add(caught); + } + } + return exceptionContext.throwables.isEmpty() && exceptionContext.responseContext != null + ? afterRequestWithoutException(postInvocationInterceptors, requestContext, exceptionContext) + : afterRequestWithException(postInvocationInterceptors, requestContext, exceptionContext); + + } else { + throw suppressExceptions(exceptionContext.throwables); + } + } + + /** + * Execute all {@link PostInvocationInterceptor PostInvocationInterceptors}. + * + * @param request {@link ClientRequestContext} + * @param response {@link ClientResponseContext} + * @param previousException Any possible {@code Throwable} caught from executing the previous parts of the Client Request + * chain. + * @return the actual {@link ClientResponseContext} provided by the previous parts of the Client Request chain or by + * {@link PostInvocationInterceptor.ExceptionContext#resolve(Response)}. + * @throws {@code RuntimeException} if {@code previousException} or any new {@code Exception} was not + * {@link PostInvocationInterceptor.ExceptionContext#resolve(Response) resolved}. + */ + ClientResponse afterRequest(ClientRequest request, ClientResponse response, Throwable previousException) { + final PostInvocationExceptionContext exceptionContext + = new PostInvocationExceptionContext(response, previousException); + final InvocationInterceptorRequestContext requestContext = new InvocationInterceptorRequestContext(request); + return previousException != null + ? afterRequestWithException(postInvocationInterceptors, requestContext, exceptionContext) + : afterRequestWithoutException(postInvocationInterceptors, requestContext, exceptionContext); + } + + private static boolean resolveResponse(InvocationInterceptorRequestContext requestContext, + PostInvocationExceptionContext exceptionContext) { + if (exceptionContext.response != null) { + exceptionContext.throwables.clear(); + final ClientResponseMediaTypeDeterminer determiner = new ClientResponseMediaTypeDeterminer( + requestContext.clientRequest.getWorkers()); + determiner.setResponseMediaTypeIfNotSet(exceptionContext.response, requestContext.getConfiguration()); + + final ClientResponse response = new ClientResponse(requestContext.clientRequest, exceptionContext.response); + exceptionContext.responseContext = response; + exceptionContext.response = null; + return true; + } else { + return false; + } + } + } + + private static class InvocationInterceptorRequestContext implements ClientRequestContext { + + private final ClientRequest clientRequest; + + private InvocationInterceptorRequestContext(ClientRequest clientRequestContext) { + this.clientRequest = clientRequestContext; + } + + @Override + public Object getProperty(String name) { + return clientRequest.getProperty(name); + } + + @Override + public Collection getPropertyNames() { + return clientRequest.getPropertyNames(); + } + + @Override + public void setProperty(String name, Object object) { + clientRequest.setProperty(name, object); + } + + @Override + public void removeProperty(String name) { + clientRequest.removeProperty(name); + } + + @Override + public URI getUri() { + return clientRequest.getUri(); + } + + @Override + public void setUri(URI uri) { + clientRequest.setUri(uri); + } + + @Override + public String getMethod() { + return clientRequest.getMethod(); + } + + @Override + public void setMethod(String method) { + clientRequest.setMethod(method); + } + + @Override + public MultivaluedMap getHeaders() { + return clientRequest.getHeaders(); + } + + @Override + public MultivaluedMap getStringHeaders() { + return clientRequest.getStringHeaders(); + } + + @Override + public String getHeaderString(String name) { + return clientRequest.getHeaderString(name); + } + + @Override + public Date getDate() { + return clientRequest.getDate(); + } + + @Override + public Locale getLanguage() { + return clientRequest.getLanguage(); + } + + @Override + public MediaType getMediaType() { + return clientRequest.getMediaType(); + } + + @Override + public List getAcceptableMediaTypes() { + return clientRequest.getAcceptableMediaTypes(); + } + + @Override + public List getAcceptableLanguages() { + return clientRequest.getAcceptableLanguages(); + } + + @Override + public Map getCookies() { + return clientRequest.getCookies(); + } + + @Override + public boolean hasEntity() { + return clientRequest.hasEntity(); + } + + @Override + public Object getEntity() { + return clientRequest.getEntity(); + } + + @Override + public Class getEntityClass() { + return clientRequest.getEntityClass(); + } + + @Override + public Type getEntityType() { + return clientRequest.getEntityType(); + } + + @Override + public void setEntity(Object entity) { + clientRequest.setEntity(entity); + } + + @Override + public void setEntity(Object entity, Annotation[] annotations, MediaType mediaType) { + clientRequest.setEntity(entity, annotations, mediaType); + } + + @Override + public Annotation[] getEntityAnnotations() { + return clientRequest.getEntityAnnotations(); + } + + @Override + public OutputStream getEntityStream() { + return clientRequest.getEntityStream(); + } + + @Override + public void setEntityStream(OutputStream outputStream) { + clientRequest.setEntityStream(outputStream); + } + + @Override + public Client getClient() { + return clientRequest.getClient(); + } + + @Override + public Configuration getConfiguration() { + return clientRequest.getConfiguration(); + } + + @Override + public void abortWith(Response response) { + if (clientRequest.getAbortResponse() != null) { + LOGGER.warning(LocalizationMessages.PREINVOCATION_INTERCEPTOR_MULTIPLE_ABORTIONS()); + throw new IllegalStateException(LocalizationMessages.PREINVOCATION_INTERCEPTOR_MULTIPLE_ABORTIONS()); + } + LOGGER.finer(LocalizationMessages.PREINVOCATION_INTERCEPTOR_ABORT_WITH()); + clientRequest.abortWith(response); + } + } + + private static class PostInvocationExceptionContext implements PostInvocationInterceptor.ExceptionContext { + private ClientResponse responseContext; // responseContext instance can be changed by PostInvocationInterceptor + private LinkedList throwables; + private Response response = null; + + private PostInvocationExceptionContext(ClientResponse responseContext, Throwable throwable) { + this.responseContext = responseContext; + this.throwables = new LinkedList<>(); + if (throwable != null) { + if (InvocationInterceptorException.class.isInstance(throwable)) { // from PreInvocationInterceptor + for (Throwable t : throwable.getSuppressed()) { + throwables.add(t); + } + } else { + throwables.add(throwable); + } + } + } + + @Override + public Optional getResponseContext() { + return responseContext == null + ? Optional.empty() + : Optional.of(new InvocationInterceptorResponseContext(responseContext)); + } + + @Override + public Deque getThrowables() { + return throwables; + } + + @Override + public void resolve(Response response) { + if (this.response != null) { + LOGGER.warning(LocalizationMessages.POSTINVOCATION_INTERCEPTOR_MULTIPLE_RESOLVES()); + throw new IllegalStateException(LocalizationMessages.POSTINVOCATION_INTERCEPTOR_MULTIPLE_RESOLVES()); + } + LOGGER.finer(LocalizationMessages.POSTINVOCATION_INTERCEPTOR_RESOLVE()); + this.response = response; + } + } + + private static class InvocationInterceptorResponseContext implements ClientResponseContext { + private final ClientResponse clientResponse; + + private InvocationInterceptorResponseContext(ClientResponse clientResponse) { + this.clientResponse = clientResponse; + } + + @Override + public int getStatus() { + return clientResponse.getStatus(); + } + + @Override + public void setStatus(int code) { + clientResponse.setStatus(code); + } + + @Override + public Response.StatusType getStatusInfo() { + return clientResponse.getStatusInfo(); + } + + @Override + public void setStatusInfo(Response.StatusType statusInfo) { + clientResponse.setStatusInfo(statusInfo); + } + + @Override + public MultivaluedMap getHeaders() { + return clientResponse.getHeaders(); + } + + @Override + public String getHeaderString(String name) { + return clientResponse.getHeaderString(name); + } + + @Override + public Set getAllowedMethods() { + return clientResponse.getAllowedMethods(); + } + + @Override + public Date getDate() { + return clientResponse.getDate(); + } + + @Override + public Locale getLanguage() { + return clientResponse.getLanguage(); + } + + @Override + public int getLength() { + return clientResponse.getLength(); + } + + @Override + public MediaType getMediaType() { + return clientResponse.getMediaType(); + } + + @Override + public Map getCookies() { + return clientResponse.getCookies(); + } + + @Override + public EntityTag getEntityTag() { + return clientResponse.getEntityTag(); + } + + @Override + public Date getLastModified() { + return clientResponse.getLastModified(); + } + + @Override + public URI getLocation() { + return clientResponse.getLocation(); + } + + @Override + public Set getLinks() { + return clientResponse.getLinks(); + } + + @Override + public boolean hasLink(String relation) { + return clientResponse.hasLink(relation); + } + + @Override + public Link getLink(String relation) { + return clientResponse.getLink(relation); + } + + @Override + public Link.Builder getLinkBuilder(String relation) { + return clientResponse.getLinkBuilder(relation); + } + + @Override + public boolean hasEntity() { + return clientResponse.hasEntity(); + } + + @Override + public InputStream getEntityStream() { + return clientResponse.getEntityStream(); + } + + @Override + public void setEntityStream(InputStream input) { + clientResponse.setEntityStream(input); + } + } + + private static ProcessingException createProcessingException(Throwable t) { + final ProcessingException processingException = createProcessingException(LocalizationMessages.EXCEPTION_SUPPRESSED()); + processingException.addSuppressed(t); + return processingException; + } + + private static ProcessingException createProcessingException(String message) { + return new InvocationInterceptorException(message); + } + + private static class InvocationInterceptorException extends ProcessingException { + private InvocationInterceptorException(String message) { + super(message); + } + } + + private static RuntimeException suppressExceptions(Deque throwables) { + if (throwables.size() == 1 && RuntimeException.class.isInstance(throwables.getFirst())) { + throw (RuntimeException) throwables.getFirst(); + } + final ProcessingException processingException = createProcessingException(LocalizationMessages.EXCEPTION_SUPPRESSED()); + for (Throwable throwable : throwables) { + // The first throwable is also marked as the cause for visibility in logs + if (processingException.getCause() == null) { + processingException.initCause(throwable); + } + processingException.addSuppressed(throwable); + } + return processingException; + } +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/routing/AbortedRequestMediaTypeDeterminer.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/routing/ClientResponseMediaTypeDeterminer.java similarity index 63% rename from core-client/src/main/java/org/glassfish/jersey/client/internal/routing/AbortedRequestMediaTypeDeterminer.java rename to core-client/src/main/java/org/glassfish/jersey/client/internal/routing/ClientResponseMediaTypeDeterminer.java index 7a36727ab7..248c8a963a 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/internal/routing/AbortedRequestMediaTypeDeterminer.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/routing/ClientResponseMediaTypeDeterminer.java @@ -16,34 +16,68 @@ package org.glassfish.jersey.client.internal.routing; +import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.internal.routing.CombinedMediaType; import org.glassfish.jersey.internal.routing.ContentTypeDeterminer; import org.glassfish.jersey.internal.routing.RequestSpecificConsumesProducesAcceptor; import org.glassfish.jersey.internal.util.ReflectionHelper; import org.glassfish.jersey.message.MessageBodyWorkers; import org.glassfish.jersey.message.internal.AcceptableMediaType; +import org.glassfish.jersey.message.internal.HeaderUtils; +import org.glassfish.jersey.message.internal.InboundMessageContext; import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.core.Configuration; import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ReaderInterceptor; import java.util.Collections; import java.util.List; /** - * Client side {@link Response} media type determinator utility class. + * Client side {@link Response} utility class determining the media type. * Used for determining media type when {@link ClientRequestContext#abortWith(Response)} on * {@link javax.ws.rs.client.ClientRequestFilter#filter(ClientRequestContext)} is used without specifying the response * media type. */ -public class AbortedRequestMediaTypeDeterminer extends ContentTypeDeterminer { +public class ClientResponseMediaTypeDeterminer extends ContentTypeDeterminer { private static final AbortedRouting ABORTED_ROUTING = new AbortedRouting(); - public AbortedRequestMediaTypeDeterminer(MessageBodyWorkers workers) { + /** + * Constructor providing the MessageBodyWorkers that specify the media types. + * + * @param workers MessageBodyWorkers that specify the media types. + */ + public ClientResponseMediaTypeDeterminer(MessageBodyWorkers workers) { super(workers); } + /** + * Set the Response media type if not correctly set by the user. The media type is determined by the entity type + * and provided MessageBodyWorkers. + * + * @param response the response containing the HTTP headers, entity and that is eventually updated. + * @param configuration the runtime configuration settings. + */ + public void setResponseMediaTypeIfNotSet(final Response response, Configuration configuration) { + if (response.hasEntity() && response.getMediaType() == null) { + final InboundMessageContext headerContext = new InboundMessageContext(configuration) { + @Override + protected Iterable getReaderInterceptors() { + return null; + } + }; + headerContext.headers(HeaderUtils.asStringHeaders(response.getHeaders(), configuration)); + + final MediaType mediaType = determineResponseMediaType(response.getEntity(), + headerContext.getQualifiedAcceptableMediaTypes()); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, mediaType); + } + } + /** * Determine the {@link MediaType} of the {@link Response} based on writers suitable for the given entity instance. * @@ -51,7 +85,7 @@ public AbortedRequestMediaTypeDeterminer(MessageBodyWorkers workers) { * @param acceptableMediaTypes acceptable media types from request. * @return media type of the response. */ - public MediaType determineResponseMediaType( + private MediaType determineResponseMediaType( final Object entity, final List acceptableMediaTypes) { diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/PostInvocationInterceptor.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/PostInvocationInterceptor.java new file mode 100644 index 0000000000..2dc58c6b9e --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/PostInvocationInterceptor.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019 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.spi; + +import org.glassfish.jersey.Beta; +import org.glassfish.jersey.spi.Contract; + +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.core.Response; +import java.util.Deque; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +/** + * The interceptor of a client request invocation that is executed after the request invocation itself, i.e. after the + * {@link javax.ws.rs.client.ClientResponseFilter ClientResponseFilters} are executed. + *

+ * It is ensured that all {@code PostInvocationInterceptors} are executed after the request, in the reverse order given by the + * {@link javax.annotation.Priority}, the higher the priority the later the execution. Any {@code Throwable} thrown when + * the {@link PostInvocationInterceptor#afterRequest(ClientRequestContext, ClientResponseContext)} or + * {@link PostInvocationInterceptor#onException(ClientRequestContext, ExceptionContext)} is being processed is accumulated and + * a multi RuntimeException with other {@link Throwable#addSuppressed(Throwable) exceptions supressed} is being thrown at the end + * (possibly encapsulated in a {@link javax.ws.rs.ProcessingException} if not a single {@code RuntimeException}), + * unless resolved by {@link PostInvocationInterceptor#onException(ClientRequestContext, ExceptionContext)}. During the + * {@link PostInvocationInterceptor} processing, the accumulated {@link Deque} of the {@code Throwables} is available in the + * {@link ExceptionContext}. + *

+ * For asynchronous invocation, the {@code PostInvocationInterceptor} is invoked in the request thread, i.e. in the thread + * provided by {@link javax.ws.rs.client.ClientBuilder#executorService(ExecutorService) ExecutorService}. + *

+ * When the lowest priority {@code PostInvocationInterceptor} is executed first, one of the two methods can be invoked. + * {@link PostInvocationInterceptor#afterRequest(ClientRequestContext, ClientResponseContext)} in a usual case when no previous + * {@code Throwable} was caught, or {@link PostInvocationInterceptor#onException(ClientRequestContext, ExceptionContext)} when + * the {@code Throwable} was caught. Should the {@link ExceptionContext#resolve(Response)} be utilized in that case, + * the next {@code PostInvocationInterceptor}'s + * {@link PostInvocationInterceptor#afterRequest(ClientRequestContext, ClientResponseContext) afterRequest} method will be + * invoked. Similarly, when a {@code Throwable} is caught during the {@code PostInvocationInterceptor} execution, the next + * {@code PostInvocationInterceptor}'s + * {@link PostInvocationInterceptor#onException(ClientRequestContext, ExceptionContext) onException} method will be invoked. + * + * @since 2.30 + */ +@Beta +@Contract +@ConstrainedTo(RuntimeType.CLIENT) +public interface PostInvocationInterceptor { + + /** + * The context providing information when the {@code Throwable} (typically, the {@code RuntimeException}) is caught. + */ + interface ExceptionContext { + /** + * If the {@link ClientResponseContext} has been available at the time of the {@code Throwable} occurrence, + * such as when the {@link PostInvocationInterceptor} is processed, it will be available. + * + * @return {@link ClientResponseContext} if available. + */ + Optional getResponseContext(); + + /** + * Get the mutable {@link Deque} of unhandled {@code Throwables} occurred during the request (including previous + * {@code PostInvocationInterceptor} processing). + * + * @return Unhandled {@code Throwables} occurred during the request. + */ + Deque getThrowables(); + + /** + * Resolve the {@code Throwables} with a provided {@link Response}. The Throwables in the {@code ExceptionContext} + * will be cleared. + * + * @param response the provided {@link Response} to be passed to a next {@code PostInvocationInterceptor} or the + * {@link javax.ws.rs.client.Client}. + */ + void resolve(Response response); + } + + /** + * The method is invoked after a request when no {@code Throwable} is thrown, or the {@code Throwables} are + * {@link ExceptionContext#resolve(Response) resolved} by previous {@code PostInvocationInterceptor}. + * + * @param requestContext the request context. + * @param responseContext the response context of the original {@link javax.ws.rs.core.Response} or response context + * defined by the new {@link ExceptionContext#resolve(Response) resolving} + * {@link javax.ws.rs.core.Response}. + */ + void afterRequest(ClientRequestContext requestContext, ClientResponseContext responseContext); + + /** + * The method is invoked after a {@code Throwable} is caught during the client request chain processing. + * + * @param requestContext the request context. + * @param exceptionContext the context available to handle the caught {@code Throwables}. + */ + void onException(ClientRequestContext requestContext, ExceptionContext exceptionContext); +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/PreInvocationInterceptor.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/PreInvocationInterceptor.java new file mode 100644 index 0000000000..cf1278b252 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/PreInvocationInterceptor.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 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.spi; + +import org.glassfish.jersey.Beta; +import org.glassfish.jersey.spi.Contract; + +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.core.Response; +import java.util.concurrent.ExecutorService; + +/** + * The interceptor of a client request invocation that is executed before the invocation itself, i.e. before the + * {@link javax.ws.rs.client.ClientRequestFilter} is invoked. + *

+ * It is ensured that all {@code PreInvocationInterceptors} are executed before the request, in the order given by the + * {@link javax.annotation.Priority}, the higher the priority the sooner the execution. Any {@code RuntimeException} thrown when + * the {@link PreInvocationInterceptor#beforeRequest(ClientRequestContext)} is being processed is accumulated and + * a multi RuntimeException with other {@link Throwable#addSuppressed(Throwable) exceptions supressed} is being thrown. + *

+ * For asynchronous invocation, the {@code PreInvocationInterceptor} is invoked in the main thread, i.e. not in the thread + * provided by {@link javax.ws.rs.client.ClientBuilder#executorService(ExecutorService) ExecutorService}. For reactive + * invocations, this depends on the provided {@link javax.ws.rs.client.RxInvoker}. For the default Jersey asynchronous + * {@link org.glassfish.jersey.client.JerseyCompletionStageRxInvoker}, {@code PreInvocationInterceptor} is invoked in the + * main thread, too. + *

+ * Should the {@link ClientRequestContext#abortWith(Response)} be utilized, the request abort is performed after every + * registered {@code PreInvocationInterceptor} is processed. If multiple + * {@code PreInvocationInterceptors PreInvocationInterceptor} tries to utilize {@link ClientRequestContext#abortWith(Response)} + * method, the second and every next throws {@code IllegalStateException}. + * + * @since 2.30 + */ +@Beta +@Contract +@ConstrainedTo(RuntimeType.CLIENT) +public interface PreInvocationInterceptor { + + /** + * The method invoked before the request starts. + * @param requestContext the request context shared with {@link javax.ws.rs.client.ClientRequestFilter}. + */ + void beforeRequest(ClientRequestContext requestContext); +} diff --git a/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties b/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties index ef68af03a6..4bc0ae8e30 100644 --- a/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties +++ b/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties @@ -43,6 +43,7 @@ error.http.method.entity.null=Entity must not be null for http method {0}. error.parameter.type.processing=Could not process parameter type {0}. error.service.locator.provider.instance.request=Incorrect type of request instance {0}. Parameter must be a default Jersey ClientRequestContext implementation. error.service.locator.provider.instance.response=Incorrect type of response instance {0}. Parameter must be a default Jersey ClientResponseContext implementation. +exception.suppressed=Exceptions were thrown. See suppressed exceptions for the list. ignored.async.threadpool.size=Zero or negative asynchronous thread pool size specified in the client configuration property: [{0}] \ Using default cached thread pool. negative.chunk.size=Negative chunked HTTP transfer coding chunk size value specified in the client configuration property: [{0}] \ @@ -59,6 +60,12 @@ null.keystore.pasword=Custom key store password must not be null. null.truststore=Custom trust store, if set, must not be null. httpurlconnection.replaces.get.with.entity=Detected non-empty entity on a HTTP GET request. The underlying HTTP \ transport connector may decide to change the request method to POST. +preinvocation.interceptor.abortWith=PreInvocationInterceptor utilized ClientRequestContext#abortWith. +preinvocation.interceptor.exception=PreInvocationInterceptor threw exception. +preinvocation.interceptor.multiple.abortions=ClientRequestContext#abortWith has been utilized multiple times. +postinvocation.interceptor.exception=PostInvocationInterceptor threw exception. +postinvocation.interceptor.multiple.resolves=ExceptionContext#resolve has been utilized multiple times. +postinvocation.interceptor.resolve=ExceptionContext#resolve has been utilized. request.entity.writer.null=The entity of the client request is null. response.to.exception.conversion.failed=Failed to convert a response into an exception. response.type.is.null=Requested response type is null. diff --git a/core-client/src/test/java/org/glassfish/jersey/client/spi/PostInvocationInterceptorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/spi/PostInvocationInterceptorTest.java new file mode 100644 index 0000000000..497fdb2537 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/spi/PostInvocationInterceptorTest.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2019 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.spi; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.ConnectException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public class PostInvocationInterceptorTest { + private static final String URL = "http://localhost:8080"; + private static final String PROPERTY_NAME = "property_name"; + private static final String PROPERTY_VALUE = "property_value"; + private static final String EXECUTOR_THREAD_NAME = "custom-executor-name"; + + private AtomicInteger counter; + + @Before + public void setup() { + counter = new AtomicInteger(); + } + + @Test + public void testSyncNoConnectionPostInvocationInterceptor() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPostInvocationInterceptor((a, b) -> false, (a, b) -> true)) + .build().target(URL).request(); + try (Response r = builder.get()) { + Assert.fail(); + } catch (ProcessingException pe) { + Assert.assertEquals(1000, counter.get()); + Assert.assertEquals(ConnectException.class, pe.getCause().getClass()); + } + } + + @Test + public void testPreThrowsPostFixes() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPreInvocationInterceptor(a -> { throw new IllegalStateException(); })) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, (a, b) -> { b.resolve(Response.accepted().build()); return true; })) + .build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus()); + Assert.assertEquals(1000, counter.get()); // counter.increment would be after ISE + } + } + + @Test + public void testPreThrowsPostFixesAsync() throws ExecutionException, InterruptedException { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPreInvocationInterceptor(a -> { throw new IllegalStateException(); })) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, (a, b) -> { b.resolve(Response.accepted().build()); return true; })) + .build().target(URL).request(); + try (Response response = builder.async().get().get()) { + Assert.assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus()); + Assert.assertEquals(1000, counter.get()); // counter.increment would be after ISE + } + } + + @Test + public void testFilterThrowsPostFixesAsync() throws ExecutionException, InterruptedException { + final ClientRequestFilter filter = (requestContext) -> {throw new IllegalStateException(); }; + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(filter) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, (a, b) -> { b.resolve(Response.accepted().build()); return true; })) + .build().target(URL).request(); + try (Response response = builder.async() + .get(new TestInvocationCallback(a -> a.getStatus() == Response.Status.ACCEPTED.getStatusCode(), a -> false)) + .get()) { + Assert.assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus()); + Assert.assertEquals(1000, counter.get()); // counter.increment would be after ISE + } + } + + @Test + public void testPostThrowsFixesThrowsFixes() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(AbortRequestFilter.class) + .register(new CounterPostInvocationInterceptor( + (a, b) -> { throw new IllegalStateException(); }, + (a, b) -> false) {}, + 100) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, + (a, b) -> { if (b.getThrowables().getFirst().getClass() == IllegalStateException.class) { + b.resolve(Response.accepted().build()); + } + return true; }) {}, + 200) + .register(new CounterPostInvocationInterceptor( + (a, b) -> { if (b.getStatus() == Response.Status.ACCEPTED.getStatusCode()) { + throw new IllegalArgumentException(); + } + return false; }, + (a, b) -> false) {}, + 300) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, + (a, b) -> { if (b.getThrowables().getFirst().getClass() == IllegalArgumentException.class) { + b.resolve(Response.noContent().build()); + } + return true; }) {}, + 400) + .build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + Assert.assertEquals(2000, counter.get()); + } + } + + @Test + public void testMultipleResolvesThrows() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(AbortRequestFilter.class) + .register(new CounterPostInvocationInterceptor( + (a, b) -> { throw new IllegalStateException(); }, + (a, b) -> false) {}, + 100) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, + (a, b) -> { b.resolve(Response.ok().build()); b.resolve(Response.ok().build()); return true; }) {}, + 200) + .build().target(URL).request(); + try (Response response = builder.get()) { + Assert.fail(); + } catch (IllegalStateException pe) { + // expected + } + } + + @Test + public void testPostChangesStatusAndEntity() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(AbortRequestFilter.class) + .register(new CounterPostInvocationInterceptor( + (a, b) -> { b.setStatus(Response.Status.CONFLICT.getStatusCode()); + b.setEntityStream(new ByteArrayInputStream("HELLO".getBytes())); return true; }, + (a, b) -> false)) + .build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus()); + Assert.assertEquals(1, counter.get()); + Assert.assertEquals("HELLO", response.readEntity(String.class)); + } + } + + @Test + public void testPostOnExceptionWhenNoThrowableAndNoResponseContext() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, + (a, b) -> { b.getThrowables().clear(); return true; }) {}, + 200) + .register(new CounterPostInvocationInterceptor( + (a, b) -> false, + (a, b) -> { b.resolve(Response.accepted().build()); return true; }) {}, + 300) + .build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus()); + Assert.assertEquals(2000, counter.get()); + } + } + + @Test + public void testAsyncNoConnectionPostInvocationInterceptor() throws InterruptedException { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPostInvocationInterceptor((a, b) -> false, (a, b) -> true)) + .build().target(URL).request(); + try (Response r = builder.async().get(new TestInvocationCallback(a -> false, a -> true)).get()) { + Assert.fail(); + } catch (ExecutionException ee) { + Assert.assertEquals(1000, counter.get()); + Assert.assertEquals(ProcessingException.class, ee.getCause().getClass()); + Assert.assertEquals(ConnectException.class, ee.getCause().getCause().getClass()); + } + } + + @Test + public void testPreThrowsPostResolves() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPreInvocationInterceptor(a -> { throw new IllegalArgumentException(); }) {}) + .register(new CounterPreInvocationInterceptor(a -> {throw new IllegalStateException(); }) {}) + .register(new CounterPostInvocationInterceptor((a, b) -> false, (a, b) -> { + b.resolve(Response.accepted().build()); return b.getThrowables().size() == 2; + })) + .build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus()); + } + } + + private static class TestInvocationCallback implements InvocationCallback { + private final Predicate responsePredicate; + private final Predicate throwablePredicate; + + private TestInvocationCallback(Predicate responsePredicate, Predicate throwablePredicate) { + this.responsePredicate = responsePredicate; + this.throwablePredicate = throwablePredicate; + } + + @Override + public void completed(Response response) { + Assert.assertTrue(responsePredicate.test(response)); + } + + @Override + public void failed(Throwable throwable) { + Assert.assertTrue(throwablePredicate.test(throwable)); + } + } + + private class CounterPostInvocationInterceptor implements PostInvocationInterceptor { + private final BiPredicate afterRequest; + private final BiPredicate onException; + + private CounterPostInvocationInterceptor(BiPredicate afterRequest, + BiPredicate onException) { + this.afterRequest = afterRequest; + this.onException = onException; + } + + @Override + public void afterRequest(ClientRequestContext requestContext, ClientResponseContext responseContext) { + Assert.assertTrue(afterRequest.test(requestContext, responseContext)); + counter.getAndIncrement(); + } + + @Override + public void onException(ClientRequestContext requestContext, ExceptionContext exceptionContext) { + Assert.assertTrue(onException.test(requestContext, exceptionContext)); + counter.addAndGet(1000); + } + } + + private class CounterPreInvocationInterceptor implements PreInvocationInterceptor { + private final Predicate predicate; + + private CounterPreInvocationInterceptor(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public void beforeRequest(ClientRequestContext requestContext) { + Assert.assertTrue(predicate.test(requestContext)); + counter.incrementAndGet(); + } + } + + private static class AbortRequestFilter implements ClientRequestFilter { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok().build()); + } + } +} diff --git a/core-client/src/test/java/org/glassfish/jersey/client/spi/PreInvocationInterceptorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/spi/PreInvocationInterceptorTest.java new file mode 100644 index 0000000000..9a1f20c610 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/spi/PreInvocationInterceptorTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2019 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.spi; + +import org.glassfish.jersey.spi.ThreadPoolExecutorProvider; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.annotation.Priority; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +public class PreInvocationInterceptorTest { + private static final String URL = "http://localhost:8080"; + private static final String PROPERTY_NAME = "property_name"; + private static final String PROPERTY_VALUE = "property_value"; + private static final String EXECUTOR_THREAD_NAME = "custom-executor-name"; + + private AtomicInteger counter; + + @Before + public void setup() { + counter = new AtomicInteger(); + } + + @Test + public void testPreInvocationInterceptorExecutedWhenBuilderBuild() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPreInvocationInterceptor(a -> a.get() == 0)) + .register(new CounterRequestFilter(a -> a.get() == 1)) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.build("GET").invoke()) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorExecutedWhenMethodGET() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new CounterPreInvocationInterceptor(a -> a.get() == 0)) + .register(new CounterRequestFilter(a -> a.get() == 1)) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.method("GET")) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorPropertySet() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .register(new PropertyPreInvocationInterceptor(a -> PROPERTY_VALUE.equals(a.getProperty(PROPERTY_NAME)))) + .register(new PropertyRequestFilter(a -> PROPERTY_VALUE.equals(a.getProperty(PROPERTY_NAME)))) + .register(AbortRequestFilter.class).build().target(URL).request().property(PROPERTY_NAME, PROPERTY_VALUE); + try (Response response = builder.method("GET")) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorHasInjection() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .property(PROPERTY_NAME, PROPERTY_VALUE) + .register(InjectedPreInvocationInterceptor.class) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.method("GET")) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorInTheSameThreadInAsync() throws ExecutionException, InterruptedException { + final String currentThreadName = Thread.currentThread().getName(); + final Invocation.Builder builder = ClientBuilder.newBuilder() + .executorService(new CustomClientExecutorProvider().getExecutorService()) + .register(new PropertyPreInvocationInterceptor(a -> currentThreadName.equals(Thread.currentThread().getName()))) + .register(new PropertyRequestFilter(a -> Thread.currentThread().getName().startsWith(EXECUTOR_THREAD_NAME))) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.async().method("GET").get()) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorInTheSameThreadInJerseyRx() throws ExecutionException, InterruptedException { + final String currentThreadName = Thread.currentThread().getName(); + final Invocation.Builder builder = ClientBuilder.newBuilder() + .executorService(new CustomClientExecutorProvider().getExecutorService()) + .register(new PropertyPreInvocationInterceptor(a -> currentThreadName.equals(Thread.currentThread().getName()))) + .register(new PropertyRequestFilter(a -> Thread.currentThread().getName().startsWith(EXECUTOR_THREAD_NAME))) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.rx().method("GET").toCompletableFuture().get()) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorAbortWith() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .executorService(new CustomClientExecutorProvider().getExecutorService()) + .register(new PropertyPreInvocationInterceptor(a -> {a.abortWith(Response.noContent().build()); return true; })) + .register(new PropertyRequestFilter(a -> {throw new IllegalStateException(); })) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testPreInvocationInterceptorAbortWithThrowsInMultiple() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .executorService(new CustomClientExecutorProvider().getExecutorService()) + .register( + new PropertyPreInvocationInterceptor(a -> {a.abortWith(Response.noContent().build()); return true; }){}, + 200 + ) + .register( + new PropertyPreInvocationInterceptor(a -> {a.abortWith(Response.accepted().build()); return true; }){}, + 100 + ) + .register(new PropertyRequestFilter(a -> {throw new IllegalStateException(); })) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.get()) { + Assert.fail(); + } catch (ProcessingException exception) { + Assert.assertEquals(IllegalStateException.class, exception.getCause().getClass()); + } + } + + @Test + public void testPrioritiesOnPreInvocationInterceptor() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .executorService(new CustomClientExecutorProvider().getExecutorService()) + .register(new Priority200PreInvocationInterceptor(a -> a.get() < 2){}) + .register(new Priority100PreInvocationInterceptor(a -> a.getAndIncrement() == 2)) + .register(new Priority200PreInvocationInterceptor(a -> a.get() < 2){}) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.get()) { + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testMultiExceptionInPreInvocationInterceptor() { + final Invocation.Builder builder = ClientBuilder.newBuilder() + .executorService(new CustomClientExecutorProvider().getExecutorService()) + .register(new Priority200PreInvocationInterceptor(a -> {throw new RuntimeException("ONE"); })) + .register(new Priority100PreInvocationInterceptor(a -> {throw new RuntimeException("TWO"); })) + .register(AbortRequestFilter.class).build().target(URL).request(); + try (Response response = builder.get()) { + Assert.fail(); + } catch (RuntimeException e) { + Assert.assertEquals("ONE", e.getSuppressed()[0].getMessage()); + Assert.assertEquals("TWO", e.getSuppressed()[1].getMessage()); + } + } + + private static class AbortRequestFilter implements ClientRequestFilter { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok().build()); + } + } + + private class CounterRequestFilter implements ClientRequestFilter { + private final Predicate consumer; + + private CounterRequestFilter(Predicate consumer) { + this.consumer = consumer; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Assert.assertTrue(consumer.test(counter)); + counter.getAndIncrement(); + } + } + + private class CounterPreInvocationInterceptor implements PreInvocationInterceptor { + private final Predicate predicate; + + private CounterPreInvocationInterceptor(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public void beforeRequest(ClientRequestContext requestContext) { + Assert.assertTrue(predicate.test(counter)); + counter.getAndIncrement(); + } + } + + private static class PropertyRequestFilter implements ClientRequestFilter { + private final Predicate predicate; + + private PropertyRequestFilter(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Assert.assertTrue(predicate.test(requestContext)); + } + } + + private static class PropertyPreInvocationInterceptor implements PreInvocationInterceptor { + private final Predicate predicate; + + private PropertyPreInvocationInterceptor(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public void beforeRequest(ClientRequestContext requestContext) { + Assert.assertTrue(predicate.test(requestContext)); + } + } + + private static class InjectedPreInvocationInterceptor implements PreInvocationInterceptor { + @Context + Configuration configuration; + + @Override + public void beforeRequest(ClientRequestContext requestContext) { + Assert.assertNotNull(configuration); + Assert.assertEquals(PROPERTY_VALUE, configuration.getProperty(PROPERTY_NAME)); + } + } + + @Priority(100) + private class Priority100PreInvocationInterceptor extends CounterPreInvocationInterceptor { + private Priority100PreInvocationInterceptor(Predicate predicate) { + super(predicate); + } + } + + @Priority(200) + private class Priority200PreInvocationInterceptor extends CounterPreInvocationInterceptor { + private Priority200PreInvocationInterceptor(Predicate predicate) { + super(predicate); + } + } + + private static class CustomClientExecutorProvider extends ThreadPoolExecutorProvider { + CustomClientExecutorProvider() { + super(EXECUTOR_THREAD_NAME); + } + } +}