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();
+        }
+    }
 }