From ec4235de14ed214939b48fdeb3aec3036231e52c Mon Sep 17 00:00:00 2001 From: Jan Supol Date: Wed, 13 Nov 2019 21:04:18 +0100 Subject: [PATCH] new InvocationBuilderListener SPI Signed-off-by: Jan Supol --- .../InvocationBuilderListenerStage.java | 126 +++++++++++++ .../jersey/client/JerseyInvocation.java | 37 ++-- .../client/spi/InvocationBuilderListener.java | 171 ++++++++++++++++++ .../spi/InvocationBuilderListenerTest.java | 124 +++++++++++++ 4 files changed, 442 insertions(+), 16 deletions(-) create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java create mode 100644 core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java diff --git a/core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java b/core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java new file mode 100644 index 00000000000..2a730bcf28f --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2011, 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.spi.InvocationBuilderListener; +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.client.Invocation; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import java.util.Iterator; +import java.util.Locale; + +/** + * Client Request processing stage. During a request completion, when the {@link Invocation.Builder} + * would create an {@link Invocation}, this class is utilized. + */ +/* package */ class InvocationBuilderListenerStage { + final Iterator invocationBuilderListenerIterator; + + /* package */ InvocationBuilderListenerStage(InjectionManager injectionManager) { + final RankedComparator comparator = + new RankedComparator<>(RankedComparator.Order.ASCENDING); + invocationBuilderListenerIterator = Providers + .getAllProviders(injectionManager, InvocationBuilderListener.class, comparator).iterator(); + } + + /* package */ void invokeListener(Invocation.Builder builder) { + while (invocationBuilderListenerIterator.hasNext()) { + invocationBuilderListenerIterator.next().onInvocation(new InvocationBuilderContextImpl(builder)); + } + } + + private static class InvocationBuilderContextImpl implements InvocationBuilderListener.InvocationBuilderContext { + private final Invocation.Builder builder; + + private InvocationBuilderContextImpl(Invocation.Builder builder) { + this.builder = builder; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext accept(String... mediaTypes) { + builder.accept(mediaTypes); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext accept(MediaType... mediaTypes) { + builder.accept(mediaTypes); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext acceptLanguage(Locale... locales) { + builder.acceptLanguage(locales); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext acceptLanguage(String... locales) { + builder.acceptLanguage(locales); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext acceptEncoding(String... encodings) { + builder.acceptEncoding(encodings); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext cookie(Cookie cookie) { + builder.cookie(cookie); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext cookie(String name, String value) { + builder.cookie(name, value); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext cacheControl(CacheControl cacheControl) { + builder.cacheControl(cacheControl); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext header(String name, Object value) { + builder.header(name, value); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext headers(MultivaluedMap headers) { + builder.headers(headers); + return this; + } + + @Override + public InvocationBuilderListener.InvocationBuilderContext property(String name, Object value) { + builder.property(name, value); + return this; + } + } +} + diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java index 474b600d996..b3a4fd29df6 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java @@ -178,45 +178,45 @@ private void storeEntity(final Entity entity) { @Override public JerseyInvocation build(final String method) { requestContext.setMethod(method); - return new JerseyInvocation(this, true); + return new JerseyInvocation(onInvocation(this), true); } @Override public JerseyInvocation build(final String method, final Entity entity) { requestContext.setMethod(method); storeEntity(entity); - return new JerseyInvocation(this, true); + return new JerseyInvocation(onInvocation(this), true); } @Override public JerseyInvocation buildGet() { requestContext.setMethod("GET"); - return new JerseyInvocation(this, true); + return new JerseyInvocation(onInvocation(this), true); } @Override public JerseyInvocation buildDelete() { requestContext.setMethod("DELETE"); - return new JerseyInvocation(this, true); + return new JerseyInvocation(onInvocation(this), true); } @Override public JerseyInvocation buildPost(final Entity entity) { requestContext.setMethod("POST"); storeEntity(entity); - return new JerseyInvocation(this, true); + return new JerseyInvocation(onInvocation(this), true); } @Override public JerseyInvocation buildPut(final Entity entity) { requestContext.setMethod("PUT"); storeEntity(entity); - return new JerseyInvocation(this, true); + return new JerseyInvocation(onInvocation(this), true); } @Override public javax.ws.rs.client.AsyncInvoker async() { - return new AsyncInvoker(this); + return new AsyncInvoker(onInvocation(this)); } @Override @@ -392,7 +392,7 @@ public T trace(final GenericType responseType) throws ProcessingException @Override public Response method(final String name) throws ProcessingException { requestContext.setMethod(name); - return new JerseyInvocation(this).invoke(); + return new JerseyInvocation(onInvocation(this)).invoke(); } @Override @@ -401,7 +401,7 @@ public T method(final String name, final Class responseType) throws Proce throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); } requestContext.setMethod(name); - return new JerseyInvocation(this).invoke(responseType); + return new JerseyInvocation(onInvocation(this)).invoke(responseType); } @Override @@ -411,14 +411,14 @@ public T method(final String name, final GenericType responseType) throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); } requestContext.setMethod(name); - return new JerseyInvocation(this).invoke(responseType); + return new JerseyInvocation(onInvocation(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(); + return new JerseyInvocation(onInvocation(this)).invoke(); } @Override @@ -429,7 +429,7 @@ public T method(final String name, final Entity entity, final Class re } requestContext.setMethod(name); storeEntity(entity); - return new JerseyInvocation(this).invoke(responseType); + return new JerseyInvocation(onInvocation(this)).invoke(responseType); } @Override @@ -440,7 +440,7 @@ public T method(final String name, final Entity entity, final GenericType } requestContext.setMethod(name); storeEntity(entity); - return new JerseyInvocation(this).invoke(responseType); + return new JerseyInvocation(onInvocation(this)).invoke(responseType); } @Override @@ -451,13 +451,13 @@ public Builder property(final String name, final Object value) { @Override public CompletionStageRxInvoker rx() { - return new JerseyCompletionStageRxInvoker(this); + return new JerseyCompletionStageRxInvoker(onInvocation(this)); } @Override public T rx(Class clazz) { if (clazz == JerseyCompletionStageRxInvoker.class) { - return (T) new JerseyCompletionStageRxInvoker(this); + return (T) new JerseyCompletionStageRxInvoker(onInvocation(this)); } return createRxInvoker(clazz, executorService()); } @@ -508,7 +508,7 @@ private T createRxInvoker(Class clazz for (RxInvokerProvider invokerProvider : allProviders) { if (invokerProvider.isProviderFor(clazz)) { - RxInvoker rxInvoker = invokerProvider.getRxInvoker(this, executorService); + RxInvoker rxInvoker = invokerProvider.getRxInvoker(onInvocation(this), executorService); if (rxInvoker == null) { throw new IllegalStateException(LocalizationMessages.CLIENT_RX_PROVIDER_NULL()); @@ -521,6 +521,11 @@ private T createRxInvoker(Class clazz throw new IllegalStateException( LocalizationMessages.CLIENT_RX_PROVIDER_NOT_REGISTERED(clazz.getSimpleName())); } + + private static JerseyInvocation.Builder onInvocation(JerseyInvocation.Builder builder) { + new InvocationBuilderListenerStage(builder.requestContext.getInjectionManager()).invokeListener(builder); + return builder; + } } /* package */ static class AsyncInvoker extends CompletableFutureAsyncInvoker implements javax.ws.rs.client.AsyncInvoker { diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java new file mode 100644 index 00000000000..f182dd4bd31 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java @@ -0,0 +1,171 @@ +/* + * 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.Invocation; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import java.util.Locale; + +/** + * Implementations of this interface will be notified when Invocation.Builder + * creates an Invocation. This will allow implementations to access the invocation builders, + * and is intended for global providers. For example, the Invocation.Builder properties can be + * accessed to set properties that are available on the {@link javax.ws.rs.client.ClientRequestContext}. + * + * In order for the InvocationBuilderListener to be called, the implementation of the interface needs + * to be registered on the {@code Client} the same way the {@code ClientRequestFilter} is registered, for instance. + * + * @since 2.30 + */ +@Beta +@Contract +@ConstrainedTo(RuntimeType.CLIENT) +public interface InvocationBuilderListener { + + /** + * An {@link javax.ws.rs.client.Invocation.Builder} subset of setter methods. + */ + public interface InvocationBuilderContext { + /** + * Add the accepted response media types. + * + * @param mediaTypes accepted response media types. + * @return the updated context. + */ + InvocationBuilderContext accept(String... mediaTypes); + + /** + * Add the accepted response media types. + * + * @param mediaTypes accepted response media types. + * @return the updated context. + */ + InvocationBuilderContext accept(MediaType... mediaTypes); + + /** + * Add acceptable languages. + * + * @param locales an array of the acceptable languages. + * @return the updated context. + */ + InvocationBuilderContext acceptLanguage(Locale... locales); + + /** + * Add acceptable languages. + * + * @param locales an array of the acceptable languages. + * @return the updated context. + */ + InvocationBuilderContext acceptLanguage(String... locales); + + /** + * Add acceptable encodings. + * + * @param encodings an array of the acceptable encodings. + * @return the updated context. + */ + InvocationBuilderContext acceptEncoding(String... encodings); + + /** + * Add a cookie to be set. + * + * @param cookie to be set. + * @return the updated context. + */ + InvocationBuilderContext cookie(Cookie cookie); + + /** + * Add a cookie to be set. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @return the updated context. + */ + InvocationBuilderContext cookie(String name, String value); + + /** + * Set the cache control data of the message. + * + * @param cacheControl the cache control directives, if {@code null} + * any existing cache control directives will be removed. + * @return the updated context. + */ + InvocationBuilderContext cacheControl(CacheControl cacheControl); + + /** + * Add an arbitrary header. + * + * @param name the name of the header + * @param value the value of the header, the header will be serialized + * using a {@link javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if + * one is available via {@link javax.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} + * for the class of {@code value} or using its {@code toString} method + * if a header delegate is not available. If {@code value} is {@code null} + * then all current headers of the same name will be removed. + * @return the updated context. + */ + InvocationBuilderContext header(String name, Object value); + + /** + * Replaces all existing headers with the newly supplied headers. + * + * @param headers new headers to be set, if {@code null} all existing + * headers will be removed. + * @return the updated context. + */ + InvocationBuilderContext headers(MultivaluedMap headers); + + /** + * Set a new property in the context of a request represented by this invocation builder. + *

+ * The property is available for a later retrieval via {@link ClientRequestContext#getProperty(String)} + * or {@link javax.ws.rs.ext.InterceptorContext#getProperty(String)}. + * If a property with a given name is already set in the request context, + * the existing value of the property will be updated. + * Setting a {@code null} value into a property effectively removes the property + * from the request property bag. + *

+ * + * @param name property name. + * @param value (new) property value. {@code null} value removes the property + * with the given name. + * @return the updated context. + * @see Invocation#property(String, Object) + */ + InvocationBuilderContext property(String name, Object value); + } + + /** + * Whenever a method on {@link Invocation.Builder} that does not return the {@link Invocation.Builder} + * is called, (i.e. methods that are not part of the {@link InvocationBuilderContext}, for instance + * {@link Invocation.Builder#async()}, {@link Invocation.Builder#build(String)}, + * {@link Invocation.Builder#invoke()}, {@link Invocation.Builder#rx()}, or + * {@link Invocation.Builder#submit()}), this method would be called. + * @param context the updated {@link InvocationBuilderContext}. + */ + void onInvocation(InvocationBuilderContext context); + +} diff --git a/core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java b/core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java new file mode 100644 index 00000000000..51eb7e878c2 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java @@ -0,0 +1,124 @@ +/* + * 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.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +public class InvocationBuilderListenerTest { + + private static final String PROPERTY_NAME = "test_property"; + private static final String ONE = "one"; + + private WebTarget target; + + @Before + public void setUp() { + target = ClientBuilder.newClient().target("http://localhost:8080").register(AbortRequestFilter.class) + .register(new PropertySetterInvocationBuilderListener(a -> a.property(key(ONE), ONE))); + } + + @Test + public void testAsync() throws ExecutionException, InterruptedException { + try (Response r = target.request().async().get().get()) { + assertDefault(r); + } + } + + @Test + public void testBuild() { + try (Response r = target.request().build("GET").invoke()) { + assertDefault(r); + } + } + + @Test + public void testBuildEntity() { + try (Response r = target.request().build("POST", Entity.entity("ANY", MediaType.TEXT_PLAIN_TYPE)).invoke()) { + assertDefault(r); + } + } + + @Test + public void testMethod() { + try (Response r = target.request().method("GET")) { + assertDefault(r); + } + } + + @Test + public void testMethodEntity() { + try (Response r = target.request().method("POST", Entity.entity("ANY", MediaType.TEXT_PLAIN_TYPE))) { + assertDefault(r); + } + } + + @Test + public void testRx() throws ExecutionException, InterruptedException { + try (Response r = target.request().rx().get().toCompletableFuture().get()) { + assertDefault(r); + } + } + + private void assertDefault(Response response) { + Assert.assertEquals(key(ONE) + "=" + ONE, response.readEntity(String.class)); + } + + private static String key(String keySuffix) { + return new StringBuilder().append(PROPERTY_NAME).append('_').append(keySuffix).toString(); + } + + public static class PropertySetterInvocationBuilderListener implements InvocationBuilderListener { + + private final Consumer builderConsumer; + + public PropertySetterInvocationBuilderListener(Consumer builderConsumer) { + this.builderConsumer = builderConsumer; + } + + @Override + public void onInvocation(InvocationBuilderContext context) { + builderConsumer.accept(context); + } + } + + public static class AbortRequestFilter implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + StringBuilder sb = new StringBuilder(); + for (String propertyName : requestContext.getPropertyNames()) { + if (propertyName.startsWith(PROPERTY_NAME)) { + sb.append(propertyName).append("=").append(requestContext.getProperty(propertyName)); + } + } + requestContext.abortWith(Response.ok().entity(sb.toString()).build()); + } + } +}