allow for resource methods to return <? extends XYZ>

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java b/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java
index 5e0a1e8..cf6799e 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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
@@ -21,6 +21,8 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
 import java.net.URI;
 import java.util.Date;
 import java.util.Locale;
@@ -308,6 +310,18 @@
             if (parameterizedType.getRawType().equals(GenericEntity.class)) {
                 t = parameterizedType.getActualTypeArguments()[0];
             }
+        } else if (type instanceof TypeVariable) {
+           final TypeVariable typeVariable = (TypeVariable) type;
+           final Type[] bounds = typeVariable.getBounds();
+           if (bounds.length == 1) {
+               t = bounds[0];
+           }
+        } else if (type instanceof WildcardType) {
+            final WildcardType wildcardType = (WildcardType) type;
+            final Type[] bounds = wildcardType.getUpperBounds();
+            if (bounds.length == 1) {
+                t = bounds[0];
+            }
         }
 
         messageContext.setEntityType(t);
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 262bd0e..205a5ff 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2023 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
@@ -18,7 +18,6 @@
 
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.server.ServerProperties;
-import org.glassfish.jersey.server.ServerRuntime;
 import org.glassfish.jersey.test.JerseyTest;
 import org.junit.jupiter.api.Test;
 
@@ -160,7 +159,15 @@
 
     @Test
     public void testCompletionStageUnwrappedInGenericType() {
-        try (Response r = target("cs/databeanlist").request().get()){
+        try (Response r = target("cs/databeanlist").request().get()) {
+            assertEquals(200, r.getStatus());
+            assertTrue(r.readEntity(String.class).startsWith(ENTITY));
+        }
+    }
+
+    @Test
+    void testExtends() {
+        try (Response r = target("cs/csextends").request().get()) {
             assertEquals(200, r.getStatus());
             assertTrue(r.readEntity(String.class).startsWith(ENTITY));
         }
@@ -291,6 +298,12 @@
             return cs;
         }
 
+        @GET
+        @Path("csextends")
+        public CompletionStage<? extends CharSequence> csExtends() {
+            return CompletableFuture.completedFuture(ENTITY);
+        }
+
         private void delaySubmit(Runnable runnable) {
             EXECUTOR_SERVICE.submit(() -> {
                 try {
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/TypedVariableTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/TypedVariableTest.java
new file mode 100644
index 0000000..bb8369c
--- /dev/null
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/TypedVariableTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 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.tests.e2e.server;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TypedVariableTest extends JerseyTest {
+
+    private static final String ENTITY = "entity";
+
+    @Path("/typed")
+    public static class Resource {
+        @GET
+        @Path("extends")
+        public <T extends CharSequence> T justExtends() {
+            return (T) ENTITY;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Resource.class);
+    }
+
+    @Test
+    void testExtends() {
+        try (Response r = target("typed/extends").request().get()) {
+            assertEquals(200, r.getStatus());
+            assertTrue(r.readEntity(String.class).startsWith(ENTITY));
+        }
+    }
+}