Enable CompletionStage unwrap in MBW
Signed-off-by: Jan Supol <jan.supol@oracle.com>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
index 012ef33..d002a0c 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
@@ -48,7 +48,10 @@
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.inject.InjectionManagerSupplier;
import org.glassfish.jersey.internal.util.ExceptionUtils;
-import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.internal.PropertiesResolver;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
import org.glassfish.jersey.message.MessageBodyWorkers;
import org.glassfish.jersey.message.internal.HeaderUtils;
import org.glassfish.jersey.message.internal.OutboundMessageContext;
@@ -58,7 +61,8 @@
*
* @author Marek Potociar
*/
-public class ClientRequest extends OutboundMessageContext implements ClientRequestContext, HttpHeaders, InjectionManagerSupplier {
+public class ClientRequest extends OutboundMessageContext
+ implements ClientRequestContext, HttpHeaders, InjectionManagerSupplier, PropertiesResolver {
// Request-scoped configuration instance
private final ClientConfig clientConfig;
@@ -82,6 +86,10 @@
private Iterable<ReaderInterceptor> readerInterceptors;
// do not add user-agent header (if not directly set) to the request.
private boolean ignoreUserAgent;
+ // lazy PropertiesResolver
+ private LazyValue<PropertiesResolver> propertiesResolver = Values.lazy(
+ (Value<PropertiesResolver>) () -> PropertiesResolver.create(getConfiguration(), getPropertiesDelegate())
+ );
private static final Logger LOGGER = Logger.getLogger(ClientRequest.class.getName());
@@ -120,65 +128,14 @@
this.ignoreUserAgent = original.ignoreUserAgent;
}
- /**
- * Resolve a property value for the specified property {@code name}.
- *
- * <p>
- * The method returns the value of the property registered in the request-specific
- * property bag, if available. If no property for the given property name is found
- * in the request-specific property bag, the method looks at the properties stored
- * in the {@link #getConfiguration() global client-runtime configuration} this request
- * belongs to. If there is a value defined in the client-runtime configuration,
- * it is returned, otherwise the method returns {@code null} if no such property is
- * registered neither in the client runtime nor in the request-specific property bag.
- * </p>
- *
- * @param name property name.
- * @param type expected property class type.
- * @param <T> property Java type.
- * @return resolved property value or {@code null} if no such property is registered.
- */
+ @Override
public <T> T resolveProperty(final String name, final Class<T> type) {
- return resolveProperty(name, null, type);
+ return propertiesResolver.get().resolveProperty(name, type);
}
- /**
- * Resolve a property value for the specified property {@code name}.
- *
- * <p>
- * The method returns the value of the property registered in the request-specific
- * property bag, if available. If no property for the given property name is found
- * in the request-specific property bag, the method looks at the properties stored
- * in the {@link #getConfiguration() global client-runtime configuration} this request
- * belongs to. If there is a value defined in the client-runtime configuration,
- * it is returned, otherwise the method returns {@code defaultValue} if no such property is
- * registered neither in the client runtime nor in the request-specific property bag.
- * </p>
- *
- * @param name property name.
- * @param defaultValue default value to return if the property is not registered.
- * @param <T> property Java type.
- * @return resolved property value or {@code defaultValue} if no such property is registered.
- */
- @SuppressWarnings("unchecked")
+ @Override
public <T> T resolveProperty(final String name, final T defaultValue) {
- return resolveProperty(name, defaultValue, (Class<T>) defaultValue.getClass());
- }
-
- private <T> T resolveProperty(final String name, Object defaultValue, final Class<T> type) {
- // Check runtime configuration first
- Object result = getConfiguration().getProperty(name);
- if (result != null) {
- defaultValue = result;
- }
-
- // Check request properties next
- result = propertiesDelegate.getProperty(name);
- if (result == null) {
- result = defaultValue;
- }
-
- return (result == null) ? null : PropertiesHelper.convertValue(result, type);
+ return propertiesResolver.get().resolveProperty(name, defaultValue);
}
@Override
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/PropertiesResolver.java b/core-common/src/main/java/org/glassfish/jersey/internal/PropertiesResolver.java
new file mode 100644
index 0000000..4b1727f
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/PropertiesResolver.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.internal;
+
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+
+import javax.ws.rs.core.Configuration;
+
+/**
+ * Resolver of a property value for the specified property {@code name} from the
+ * request-specific property bag, or the {@link Configuration global runtime configuration}.
+ */
+public interface PropertiesResolver {
+ /**
+ * Resolve a property value for the specified property {@code name}.
+ *
+ * <p>
+ * The method returns the value of the property registered in the request-specific
+ * property bag, if available. If no property for the given property name is found
+ * in the request-specific property bag, the method looks at the properties stored
+ * in the {@link Configuration global runtime configuration} this request
+ * belongs to. If there is a value defined in the runtime configuration,
+ * it is returned, otherwise the method returns {@code null} if no such property is
+ * registered neither in the runtime nor in the request-specific property bag.
+ * </p>
+ *
+ * @param name property name.
+ * @param type expected property class type.
+ * @param <T> property Java type.
+ * @return resolved property value or {@code null} if no such property is registered.
+ */
+ public <T> T resolveProperty(final String name, final Class<T> type);
+
+ /**
+ * Resolve a property value for the specified property {@code name}.
+ *
+ * <p>
+ * The method returns the value of the property registered in the request-specific
+ * property bag, if available. If no property for the given property name is found
+ * in the request-specific property bag, the method looks at the properties stored
+ * in the {@link Configuration global runtime configuration} this request
+ * belongs to. If there is a value defined in the runtime configuration,
+ * it is returned, otherwise the method returns {@code defaultValue} if no such property is
+ * registered neither in the runtime nor in the request-specific property bag.
+ * </p>
+ *
+ * @param name property name.
+ * @param defaultValue default value to return if the property is not registered.
+ * @param <T> property Java type.
+ * @return resolved property value or {@code defaultValue} if no such property is registered.
+ */
+ public <T> T resolveProperty(final String name, final T defaultValue);
+
+ /**
+ * Return new instance of {@link PropertiesResolver}.
+ * @param configuration Runtime {@link Configuration}.
+ * @param delegate Request scoped {@link PropertiesDelegate properties delegate}.
+ * @return A new instance of {@link PropertiesResolver}.
+ */
+ public static PropertiesResolver create(Configuration configuration, PropertiesDelegate delegate) {
+ return new PropertiesResolver() {
+ @Override
+ public <T> T resolveProperty(String name, Class<T> type) {
+ return resolveProperty(name, null, type);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T resolveProperty(String name, T defaultValue) {
+ return resolveProperty(name, defaultValue, (Class<T>) defaultValue.getClass());
+ }
+
+ private <T> T resolveProperty(final String name, Object defaultValue, final Class<T> type) {
+ // Check runtime configuration first
+ Object result = configuration.getProperty(name);
+ if (result != null) {
+ defaultValue = result;
+ }
+
+ // Check request properties next
+ result = delegate.getProperty(name);
+ if (result == null) {
+ result = defaultValue;
+ }
+
+ return (result == null) ? null : PropertiesHelper.convertValue(result, type);
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java b/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java
index fbc86c2..d67e8e7 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -49,8 +49,12 @@
import org.glassfish.jersey.internal.PropertiesDelegate;
import org.glassfish.jersey.internal.guava.Preconditions;
+import org.glassfish.jersey.internal.PropertiesResolver;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.internal.util.collection.Refs;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
import org.glassfish.jersey.message.internal.AcceptableMediaType;
import org.glassfish.jersey.message.internal.HttpHeaderReader;
import org.glassfish.jersey.message.internal.InboundMessageContext;
@@ -80,7 +84,7 @@
* @author Marek Potociar
*/
public class ContainerRequest extends InboundMessageContext
- implements ContainerRequestContext, Request, HttpHeaders, PropertiesDelegate {
+ implements ContainerRequestContext, Request, HttpHeaders, PropertiesDelegate, PropertiesResolver {
private static final URI DEFAULT_BASE_URI = URI.create("/");
@@ -114,6 +118,10 @@
private ContainerResponseWriter responseWriter;
// True if the request is used in the response processing phase (for example in ContainerResponseFilter)
private boolean inResponseProcessingPhase;
+ // lazy PropertiesResolver
+ private LazyValue<PropertiesResolver> propertiesResolver = Values.lazy(
+ (Value<PropertiesResolver>) () -> PropertiesResolver.create(getConfiguration(), getPropertiesDelegate())
+ );
private static final String ERROR_REQUEST_SET_ENTITY_STREAM_IN_RESPONSE_PHASE =
LocalizationMessages.ERROR_REQUEST_SET_ENTITY_STREAM_IN_RESPONSE_PHASE();
@@ -275,6 +283,16 @@
}
@Override
+ public <T> T resolveProperty(final String name, final Class<T> type) {
+ return propertiesResolver.get().resolveProperty(name, type);
+ }
+
+ @Override
+ public <T> T resolveProperty(final String name, final T defaultValue) {
+ return propertiesResolver.get().resolveProperty(name, defaultValue);
+ }
+
+ @Override
public Object getProperty(final String name) {
return propertiesDelegate.getProperty(name);
}
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java b/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java
index 861f022..391d063 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -724,6 +724,23 @@
public static final String LOCATION_HEADER_RELATIVE_URI_RESOLUTION_DISABLED =
"jersey.config.server.headers.location.relative.resolution.disabled";
+
+ /**
+ * If {@code true} message body writer will not use {@code CompletionStage} as a generic type.
+ * The {@code CompletionStage} value will be unwrapped and the message body writer will be invoked with the unwrapped type.
+ *
+ * <p>
+ * The default value is {@code false}.
+ * </p>
+ * <p>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ * </p>
+ *
+ * @since 2.33
+ */
+ public static final String UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE =
+ "jersey.config.server.unwrap.completion.stage.writer.enable";
+
private ServerProperties() {
// prevents instantiation
}
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
index 9090196..0372940 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2011, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -59,6 +59,7 @@
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.glassfish.jersey.server.internal.ProcessingProviders;
import org.glassfish.jersey.server.internal.inject.ConfiguredValidator;
@@ -83,6 +84,8 @@
private final Annotation[] methodAnnotations;
private final Type invocableResponseType;
private final boolean canUseInvocableResponseType;
+ private final boolean isCompletionStageResponseType;
+ private final Type completionStageResponseType;
private final ResourceMethodDispatcher dispatcher;
private final Method resourceMethod;
private final Class<?> resourceClass;
@@ -306,7 +309,10 @@
&& Void.class != invocableResponseType
&& // Do NOT change the entity type for Response or it's subclasses.
!((invocableResponseType instanceof Class) && Response.class.isAssignableFrom((Class) invocableResponseType));
-
+ this.isCompletionStageResponseType = ParameterizedType.class.isInstance(invocableResponseType)
+ && CompletionStage.class.isAssignableFrom((Class<?>) ((ParameterizedType) invocableResponseType).getRawType());
+ this.completionStageResponseType =
+ isCompletionStageResponseType ? ((ParameterizedType) invocableResponseType).getActualTypeArguments()[0] : null;
}
private <T> void addNameBoundProviders(
@@ -459,7 +465,7 @@
if (canUseInvocableResponseType
&& response.hasEntity()
&& !(response.getEntityType() instanceof ParameterizedType)) {
- response.setEntityType(invocableResponseType);
+ response.setEntityType(unwrapInvocableResponseType(context.request()));
}
return response;
@@ -478,6 +484,14 @@
return jaxrsResponse;
}
+ private Type unwrapInvocableResponseType(ContainerRequest request) {
+ if (isCompletionStageResponseType
+ && request.resolveProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.FALSE)) {
+ return completionStageResponseType;
+ }
+ return invocableResponseType;
+ }
+
/**
* Get all bound request filters applicable to the {@link #getResourceMethod() resource method}
* wrapped by this invoker.
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java
index ee43891..cf7ade3 100644
--- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java
@@ -16,6 +16,28 @@
package org.glassfish.jersey.tests.e2e.server;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.MessageBodyWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
@@ -26,18 +48,10 @@
import java.util.function.Consumer;
import java.util.function.Function;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Application;
-import javax.ws.rs.core.Response;
-
-import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.test.JerseyTest;
-
-import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
/**
* @author Pavel Bucek
@@ -50,7 +64,7 @@
@Override
protected Application configure() {
- return new ResourceConfig(CompletionStageResource.class);
+ return new ResourceConfig(CompletionStageResource.class, DataBeanWriter.class);
}
@Test
@@ -134,6 +148,14 @@
assertThat(response.getStatus(), is(406));
}
+ @Test
+ public void testCompletionStageUnwrappedInGenericType() {
+ try (Response r = target("cs/databeanlist").request().get()){
+ assertEquals(200, r.getStatus());
+ assertTrue(r.readEntity(String.class).startsWith(ENTITY));
+ }
+ }
+
@Path("/cs")
public static class CompletionStageResource {
@@ -229,6 +251,13 @@
return new CustomCompletionStage<>(CompletableFuture.completedFuture(ENTITY));
}
+ @GET
+ @Path("/databeanlist")
+ public CompletionStage<List<DataBean>> getDataBeanList(@Context ContainerRequestContext requestContext) {
+ requestContext.setProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
+ return CompletableFuture.completedFuture(Collections.singletonList(new DataBean(ENTITY)));
+ }
+
/**
* Return uncompleted CompletionStage which doesn't support #toCompletableFuture().
*
@@ -466,4 +495,33 @@
throw new UnsupportedOperationException();
}
}
+
+ private static class DataBean {
+ private final String data;
+
+ private DataBean(String data) {
+ this.data = data;
+ }
+ }
+
+ private static class DataBeanWriter implements MessageBodyWriter<List<DataBean>> {
+
+ @Override
+ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return List.class.isAssignableFrom(type)
+ && ParameterizedType.class.isInstance(genericType)
+ && ((ParameterizedType) genericType).getRawType() != CompletionStage.class;
+ }
+
+ @Override
+ public void writeTo(List<DataBean> dataBeans, Class<?> type, Type genericType, Annotation[] annotations,
+ MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
+ OutputStream entityStream) throws IOException, WebApplicationException {
+ for (DataBean bean: dataBeans) {
+ entityStream.write(bean.data.getBytes());
+ entityStream.write(',');
+ }
+ entityStream.flush();
+ }
+ }
}