You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2026/03/29 19:53:43 UTC

(tapestry-5) 03/03: TAP5-2813: NPE when @Cached applied to method returning a generic type

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

thiagohp pushed a commit to branch 5.9.x-javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit bffc3c8539a4964e0b2fed5e7589a5fd15232843
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Sat Oct 4 17:51:07 2025 -0300

    TAP5-2813: NPE when @Cached applied to method returning a generic type
    
    in multiple classloader mode
---
 .../internal/GuavaGenericsResolver.java            |  2 +-
 .../services/ComponentModelSourceImpl.java         | 10 ++++++++--
 .../tapestry5/internal/transform/CachedWorker.java |  8 +++++++-
 tapestry-core/src/test/app1/CachedGenericsDemo.tml |  3 +++
 .../tapestry5/integration/app1/CacheTests.java     |  7 +++++++
 .../tapestry5/integration/app1/GenericsClass.java  |  6 ++++++
 .../tapestry5/integration/app1/GenericsEntity.java | 23 ++++++++++++++++++++++
 .../app1/base/AbstractCachedGenerics.java          | 20 +++++++++++++++++++
 .../app1/components/CachedGenerics.java            | 18 +++++++++++++++++
 .../integration/app1/pages/CachedGenericsDemo.java |  5 +++++
 .../tapestry5/integration/app1/pages/Index.java    |  4 +++-
 .../app1/base/AbstractCachedGenerics.tml           | 11 +++++++++++
 .../groovy/ioc/specs/ChainBuilderImplSpec.groovy   |  2 +-
 13 files changed, 113 insertions(+), 6 deletions(-)

diff --git a/genericsresolver-guava/src/main/java/org/apache/tapestry5/genericsresolverguava/internal/GuavaGenericsResolver.java b/genericsresolver-guava/src/main/java/org/apache/tapestry5/genericsresolverguava/internal/GuavaGenericsResolver.java
index 65338b23e..b5cf116e3 100644
--- a/genericsresolver-guava/src/main/java/org/apache/tapestry5/genericsresolverguava/internal/GuavaGenericsResolver.java
+++ b/genericsresolver-guava/src/main/java/org/apache/tapestry5/genericsresolverguava/internal/GuavaGenericsResolver.java
@@ -22,7 +22,7 @@ import org.apache.tapestry5.genericsresolverguava.internal.GuavaGenericsResolver
 import com.google.common.reflect.TypeToken;
 
 /**
- * {@link GuavaGenericsResolver} implementation using Guava.
+ * {@link GenericsResolver} implementation using Guava.
  */
 public class GuavaGenericsResolver implements GenericsResolver {
 
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
index dcbfbc47e..bec14d236 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
@@ -14,6 +14,7 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.util.HashSet;
 import java.util.Set;
 
 import org.apache.tapestry5.SymbolConstants;
@@ -33,6 +34,9 @@ public class ComponentModelSourceImpl implements ComponentModelSource
     private final PageSource pageSource;
     
     private final boolean multipleClassLoaders;
+    
+    private final static ThreadLocal<Set<String>> CALL_STACK = 
+            ThreadLocal.withInitial(HashSet::new);    
 
     public ComponentModelSourceImpl(ComponentClassResolver resolver, ComponentInstantiatorSource source,
             ComponentDependencyRegistry componentDependencyRegistry,
@@ -51,15 +55,16 @@ public class ComponentModelSourceImpl implements ComponentModelSource
     {
         if (multipleClassLoaders && isPage(componentClassName))
         {
-            
             final Set<String> superclasses = componentDependencyRegistry.getDependencies(
                     componentClassName, DependencyType.SUPERCLASS);
             
             if (!superclasses.isEmpty())
             {
                 final String superclass = superclasses.iterator().next();
-                if (isPage(superclass))
+                final Set<String> callStack = CALL_STACK.get();
+                if (!callStack.contains(superclass) && isPage(superclass))
                 {
+                    callStack.add(superclass);
                     getModel(superclass);
                     try
                     {
@@ -73,6 +78,7 @@ public class ComponentModelSourceImpl implements ComponentModelSource
                         // so the objective of the line above is already
                         // fulfilled and we can safely ignore the exception
                     }
+                    callStack.remove(superclass);
                 }
             }
         }
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/CachedWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/CachedWorker.java
index 045188d09..643abdd63 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/CachedWorker.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/CachedWorker.java
@@ -274,11 +274,17 @@ public class CachedWorker implements ComponentClassTransformWorker2
     private static JSONObject toJSONObject(PlasticMethod method) 
     {
         final MethodDescription description = method.getDescription();
+        
+        // TAP5-2813
+        final String genericSignature = description.genericSignature != null ?
+                description.genericSignature.replaceAll("<[^>]+>", "") : null;
+        
+        
         return new JSONObject(
                 MODIFIERS, description.modifiers,
                 RETURN_TYPE, description.returnType,
                 NAME, description.methodName,
-                GENERIC_SIGNATURE, description.genericSignature,
+                GENERIC_SIGNATURE, genericSignature,
                 ARGUMENT_TYPES, new JSONArray(description.argumentTypes),
                 CHECKED_EXCEPTION_TYPES, new JSONArray(description.checkedExceptionTypes),
                 WATCH, method.getAnnotation(Cached.class).watch());
diff --git a/tapestry-core/src/test/app1/CachedGenericsDemo.tml b/tapestry-core/src/test/app1/CachedGenericsDemo.tml
new file mode 100644
index 000000000..9f5b46817
--- /dev/null
+++ b/tapestry-core/src/test/app1/CachedGenericsDemo.tml
@@ -0,0 +1,3 @@
+<html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
+	<t:cachedGenerics/>
+</html>
\ No newline at end of file
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CacheTests.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CacheTests.java
index 6dccd8301..e7c2381af 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CacheTests.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/CacheTests.java
@@ -66,4 +66,11 @@ public class CacheTests extends App1TestCase
                 "Method org.apache.tapestry5.integration.app1.pages.ParamsMethodWithCached.invalidMethod(java.lang.String)",
                 "may not be used with @Cached because it has parameters.");
     }
+    
+    @Test 
+    public void at_cached_at_method_returning_generic_types()
+    {
+        // Fails without the fix
+        openLinks("@Cached on method returning type with generics");
+    }
 }
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/GenericsClass.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/GenericsClass.java
new file mode 100644
index 000000000..dd004b952
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/GenericsClass.java
@@ -0,0 +1,6 @@
+package org.apache.tapestry5.integration.app1;
+
+
+public class GenericsClass<T, H> {
+
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/GenericsEntity.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/GenericsEntity.java
new file mode 100644
index 000000000..32123c66e
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/GenericsEntity.java
@@ -0,0 +1,23 @@
+package org.apache.tapestry5.integration.app1;
+
+public class GenericsEntity<T> {
+
+    private final String id_;
+
+    private final String label_;
+
+    public GenericsEntity(String id, String label) {
+        id_ = id;
+        label_ = label;
+    }
+
+    @SuppressWarnings("unused")
+    private String getId() {
+        return id_;
+    }
+
+    @SuppressWarnings("unused")
+    private String getLabel() {
+        return label_;
+    }
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/AbstractCachedGenerics.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/AbstractCachedGenerics.java
new file mode 100644
index 000000000..053279e6d
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/AbstractCachedGenerics.java
@@ -0,0 +1,20 @@
+package org.apache.tapestry5.integration.app1.base;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tapestry5.annotations.Cached;
+import org.apache.tapestry5.integration.app1.GenericsClass;
+
+public abstract class AbstractCachedGenerics<T, H> 
+{
+
+    protected abstract GenericsClass<T, H> createTable(List<T> itemsInCurrentPage);
+
+    @Cached
+    public GenericsClass<T, H> getEmptyTable() 
+    {
+        return createTable(new ArrayList<>());
+    }
+    
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/CachedGenerics.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/CachedGenerics.java
new file mode 100644
index 000000000..8e0fef614
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/CachedGenerics.java
@@ -0,0 +1,18 @@
+package org.apache.tapestry5.integration.app1.components;
+
+import java.util.List;
+
+import org.apache.tapestry5.integration.app1.GenericsClass;
+import org.apache.tapestry5.integration.app1.GenericsEntity;
+import org.apache.tapestry5.integration.app1.base.AbstractCachedGenerics;
+
+public class CachedGenerics extends AbstractCachedGenerics<GenericsEntity, String> 
+{
+    
+    @Override
+    protected GenericsClass<GenericsEntity, String> createTable(List<GenericsEntity> itemsInCurrentPage) 
+    {
+        return new GenericsClass<>();
+    }
+
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/CachedGenericsDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/CachedGenericsDemo.java
new file mode 100644
index 000000000..1e4cfa4b0
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/CachedGenericsDemo.java
@@ -0,0 +1,5 @@
+package org.apache.tapestry5.integration.app1.pages;
+
+public class CachedGenericsDemo
+{
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
index 025b7eb07..b9ddb8d32 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
@@ -630,7 +630,9 @@ public class Index
                     
                     new Item("RecursiveDemo","Recursive Demo","Recursive component example"),
                     
-                    new Item("SelfRecursiveDemo", "Self-Recursive Demo", "check for handling of self-recursive components")
+                    new Item("SelfRecursiveDemo", "Self-Recursive Demo", "check for handling of self-recursive components"),
+                    
+                    new Item("CachedGenericsDemo", "@Cached on method returning type with generics", "tests fix for TAP5-2813")
                 );
 
     static
diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/base/AbstractCachedGenerics.tml b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/base/AbstractCachedGenerics.tml
new file mode 100644
index 000000000..87b8639d0
--- /dev/null
+++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/base/AbstractCachedGenerics.tml
@@ -0,0 +1,11 @@
+<div xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
+          xmlns:p="tapestry:parameter">
+          
+    <p>
+		${EmptyTable}
+	</p>
+	
+    <p>
+		${EmptyTable}
+	</p>
+</div>
\ No newline at end of file
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ChainBuilderImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ChainBuilderImplSpec.groovy
index 12ff3a7dc..4277cd0e9 100644
--- a/tapestry-ioc/src/test/groovy/ioc/specs/ChainBuilderImplSpec.groovy
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ChainBuilderImplSpec.groovy
@@ -118,7 +118,7 @@ class ChainBuilderImplSpec extends AbstractSharedRegistrySpecification {
     chain.toString() == "<Command chain of ioc.specs.ChainCommand>"
   }
   
-  final private static class InterfaceWithStaticMethodImpl extends InterfaceWithStaticMethod 
+  final private static class InterfaceWithStaticMethodImpl implements InterfaceWithStaticMethod 
   {
     public int something() { return 2; }
   }