This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit baf12a3f8beeafc38260846b5b862a321049f8bb
Author: opencode <[email protected]>
AuthorDate: Sat Oct 17 14:01:43 2026 +0200

    Fix covariant return type overrides of annotated POJO lifecycle methods 
wrongly rejected as duplicates
    
    A sub-class method that overrides an annotated life cycle method of a
    super class is permitted by the JLS to use a covariant return type.
    isMethodOverride() in PojoMethodMapping compared return types using
    equals() so such overrides were not recognised as overrides. Re-
    declaring the annotation on the override therefore triggered a
    deployment failure ("Duplicate annotations [...]") even though an
    override with an identical return type re-declaring the annotation was
    accepted.
    
    isMethodOverride() now checks that the return types are compatible
    rather than identical. The methods are passed to isMethodOverride() in
    different orders by the various call sites so the check tests
    assignability in both directions. Methods with the same name and
    parameter types within a single class hierarchy are override
    equivalent by definition, since the compiler enforces return type
    covariance, so this cannot wrongly match methods that are not related
    by overriding.
    
    Adds regression tests covering an annotated covariant override that
    re-declares the annotation (now accepted, the sub-class method is the
    one mapped), a non-annotated covariant override (the super class
    method is ignored) and a sub-class method that is not an override
    (different name, same payload type) which continues to be rejected as
    a duplicate annotation.
---
 .../tomcat/websocket/pojo/PojoMethodMapping.java   |  9 +-
 .../websocket/pojo/TestPojoMethodMapping.java      | 98 ++++++++++++++++++++++
 2 files changed, 106 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java 
b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java
index 10af09174b..60d8ec457a 100644
--- a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java
+++ b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java
@@ -223,7 +223,14 @@ public class PojoMethodMapping {
 
 
     private boolean isMethodOverride(Method method1, Method method2) {
-        return method1.getName().equals(method2.getName()) && 
method1.getReturnType().equals(method2.getReturnType()) &&
+        // An overriding method may use a covariant return type so the return
+        // types do not have to be equal. They do need to be compatible. The
+        // methods may be passed in either order so both directions are
+        // checked.
+        Class<?> returnType1 = method1.getReturnType();
+        Class<?> returnType2 = method2.getReturnType();
+        return method1.getName().equals(method2.getName()) &&
+                (returnType1.isAssignableFrom(returnType2) || 
returnType2.isAssignableFrom(returnType1)) &&
                 Arrays.equals(method1.getParameterTypes(), 
method2.getParameterTypes());
     }
 
diff --git a/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java 
b/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java
index 9e402bcb69..da4367059d 100644
--- a/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java
+++ b/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java
@@ -123,6 +123,104 @@ public class TestPojoMethodMapping extends TomcatBaseTest 
{
     }
 
 
+    /*
+     * A sub-class method that overrides an annotated super class method using 
a covariant return
+     * type is still an override. Re-declaring the annotation on the override 
must not be treated
+     * as a duplicate annotation and the sub-class method must be the one that 
is mapped.
+     */
+    @Test
+    public void testCovariantReturnOverride() throws Exception {
+        PojoMethodMapping mapping = new 
PojoMethodMapping(CovariantReturnServer.class, new ArrayList<>(),
+                "/covariantReturn", null);
+        Assert.assertEquals(CovariantReturnServer.class.getMethod("onOpen", 
Session.class), mapping.getOnOpen());
+    }
+
+
+    /*
+     * A non-annotated sub-class method that overrides an annotated super 
class method using a
+     * covariant return type should result in the super class method being 
ignored. Note that a
+     * compiler generated bridge method previously masked this in compiled 
code.
+     */
+    @Test
+    public void testCovariantReturnOverrideWithoutAnnotation() throws 
Exception {
+        PojoMethodMapping mapping = new 
PojoMethodMapping(CovariantReturnOverrideServer.class, new ArrayList<>(),
+                "/covariantReturnOverride", null);
+        Assert.assertNull(mapping.getOnOpen());
+    }
+
+
+    /*
+     * A sub-class method that is not an override of the annotated super class 
method must still
+     * be rejected as a duplicate annotation if it handles the same message 
type.
+     */
+    @Test
+    public void testAnnotatedMethodNotAnOverride() throws Exception {
+        try {
+            new PojoMethodMapping(NotAnOverrideServer.class, new 
ArrayList<>(), "/notAnOverride", null);
+            Assert.fail("Expected a DeploymentException");
+        } catch (DeploymentException de) {
+            Assert.assertTrue("Unexpected message: " + de.getMessage(),
+                    de.getMessage().contains("Duplicate annotations"));
+        }
+    }
+
+
+    public static class CovariantReturnBase {
+
+        @OnOpen
+        public Object onOpen(Session session) {
+            return session;
+        }
+
+        @OnMessage
+        public Object onMessage(String message) {
+            return message;
+        }
+    }
+
+
+    @ServerEndpoint("/covariantReturn")
+    public static final class CovariantReturnServer extends 
CovariantReturnBase {
+
+        @Override
+        @OnOpen
+        public String onOpen(Session session) {
+            return session.toString();
+        }
+
+        @Override
+        @OnMessage
+        public String onMessage(String message) {
+            return message;
+        }
+    }
+
+
+    @ServerEndpoint("/covariantReturnOverride")
+    public static final class CovariantReturnOverrideServer extends 
CovariantReturnBase {
+
+        @Override
+        public String onOpen(Session session) {
+            return session.toString();
+        }
+
+        @Override
+        public String onMessage(String message) {
+            return message;
+        }
+    }
+
+
+    @ServerEndpoint("/notAnOverride")
+    public static final class NotAnOverrideServer extends CovariantReturnBase {
+
+        @OnMessage
+        public String otherMessage(String message) {
+            return message;
+        }
+    }
+
+
     @ServerEndpoint(value = "/{one}/{two}/{three}", configurator = 
SingletonConfigurator.class)
     public static final class Server {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to