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

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

commit 108baa34d8f3883cfeb7c24e1706939f4ea79181
Author: Ben Weidig <[email protected]>
AuthorDate: Sun Apr 5 10:49:12 2026 +0200

    TAP5-2825: Java 21 support
---
 .../groovy/ioc/specs/ClasspathResourceSpec.groovy  |   4 +-
 tapestry-latest-java-tests/build.gradle            |   7 +-
 .../ioc/services/Java18To21ConcreteService.java    | 137 +++++++++++++++++
 .../ioc/services/Java18To21NewFeatureTests.java    | 166 +++++++++++++++++++++
 .../tapestry5/ioc/services/Java18To21Service.java  |  55 +++++++
 .../ioc/services/Java18To21ServiceImpl.java        | 142 ++++++++++++++++++
 6 files changed, 506 insertions(+), 5 deletions(-)

diff --git 
a/tapestry-ioc/src/test/groovy/ioc/specs/ClasspathResourceSpec.groovy 
b/tapestry-ioc/src/test/groovy/ioc/specs/ClasspathResourceSpec.groovy
index 1550adfae..f465ffae5 100644
--- a/tapestry-ioc/src/test/groovy/ioc/specs/ClasspathResourceSpec.groovy
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ClasspathResourceSpec.groovy
@@ -288,7 +288,9 @@ class ClasspathResourceSpec extends Specification {
       def currentCl = Thread.currentThread().contextClassLoader 
       def resourcePath = 'META-INF/maven/org.slf4j/slf4j-api/pom.xml'
       
-      def resourceURLs = currentCl.findResources resourcePath
+      // TAP5-2825: This failes on Java 21, as Groovy can no longer access the
+      // protected methods `findResources`, so we use `getResources` instead
+      def resourceURLs = currentCl.getResources(resourcePath).toList()
       def slf4jApiURL = resourceURLs.find{it.toString().contains('.jar!')} 
       ClassLoader cl = new URLClassLoader(slf4jApiURL as URL[], (ClassLoader) 
null)
      
diff --git a/tapestry-latest-java-tests/build.gradle 
b/tapestry-latest-java-tests/build.gradle
index c1e5c9ebc..4192d0201 100644
--- a/tapestry-latest-java-tests/build.gradle
+++ b/tapestry-latest-java-tests/build.gradle
@@ -5,15 +5,14 @@ plugins {
 description = 'Test suite for making sure Tapestry runs on latest Java'
 
 java {
-    toolchain {
-        languageVersion = JavaLanguageVersion.of(17)
-    }
+    sourceCompatibility = JavaVersion.VERSION_21
+    targetCompatibility = JavaVersion.VERSION_21
 
     withSourcesJar()
 }
 
 tasks.withType(JavaCompile).configureEach {
-    options.release = 17
+    options.release = 21
 }
 
 tasks.withType(JavaCompile).configureEach {
diff --git 
a/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21ConcreteService.java
 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21ConcreteService.java
new file mode 100644
index 000000000..2de1c3dab
--- /dev/null
+++ 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21ConcreteService.java
@@ -0,0 +1,137 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.ioc.services;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Java18To21ConcreteService
+{
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(Java18To21ConcreteService.class);
+
+    /**
+     * For testing SequencedCollection as a return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    public SequencedCollection<String> getSequencedCollection()
+    {
+        SequencedCollection<String> list = new ArrayList<>();
+        list.add("first");
+        list.add("middle");
+        list.add("last");
+        return list;
+    }
+
+    /**
+     * For testing SequencedMap as a return type (JEP 431).
+     * https://openjdk.org/jeps/431
+     */
+    public SequencedMap<String, Integer> getSequencedMap()
+    {
+        SequencedMap<String, Integer> map = new LinkedHashMap<>();
+        map.put("one", 1);
+        map.put("two", 2);
+        map.put("three", 3);
+        return map;
+    }
+
+    /**
+     * For testing record patterns in instanceof expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    public void recordPatternsInInstanceOf()
+    {
+        IntExpression expr = new ConstantIntExpression(42);
+        if (expr instanceof ConstantIntExpression(int v))
+        {
+            LOGGER.info("Constant int value: " + v);
+        }
+
+        IntTuple tuple = new IntTuple(3, 7);
+        if (tuple instanceof IntTuple(int x, int y))
+        {
+            LOGGER.info("Tuple sum: " + (x + y));
+        }
+    }
+
+    /**
+     * For testing record patterns in switch expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    public void recordPatternsInSwitch()
+    {
+        IntExpression expr = new PlusExpression(new ConstantIntExpression(2), 
new ConstantIntExpression(3));
+        int result = evalIntExpression(expr);
+        LOGGER.info("Switch record pattern result: " + result);
+
+        IntExpression squareExpr = new SquareExpression(new 
ConstantIntExpression(5));
+        LOGGER.info("Square result: " + evalIntExpression(squareExpr));
+    }
+
+    private int evalIntExpression(IntExpression e)
+    {
+        return switch (e)
+        {
+            case ConstantIntExpression(int v)        -> v;
+            case PlusExpression(var left, var right) -> 
evalIntExpression(left) + evalIntExpression(right);
+            case SquareExpression(var inner) ->
+            {
+                int v = evalIntExpression(inner); yield v * v;
+            }
+        };
+    }
+    /**
+     * For testing pattern matching for switch with a sealed class hierarchy 
(JEP 441).
+     * @see https://openjdk.org/jeps/441
+     */
+    public void patternMatchingForSwitchWithSealedClass()
+    {
+        for (Appliance appliance : new Appliance[] { new WashingMachine(), new 
Freezer(), new Refrigerator() })
+        {
+            String description = switch (appliance)
+            {
+                case WashingMachine wm -> "Washer: " + wm.getName();
+                case Freezer f         -> "Freezer: " + f.getName();
+                case Refrigerator r    -> "Fridge: " + r.getName();
+                case CoolingMachine cm -> "Cooler: " + cm.getName();
+            };
+            LOGGER.info(description);
+        }
+    }
+
+    /**
+     * For testing pattern matching for switch with a sealed interface (JEP 
441).
+     * @see https://openjdk.org/jeps/441
+     */
+    public void patternMatchingForSwitchWithSealedInterface()
+    {
+        BinaryExpression expr = new AndExpression(
+                new ConstantBinaryExpression(true),
+                new OrExpression(new ConstantBinaryExpression(false), new 
ConstantBinaryExpression(true)));
+
+        String description = switch (expr)
+        {
+            case ConstantBinaryExpression c -> "Constant: " + c.evaluate();
+            case AndExpression a            -> "And: " + a.evaluate();
+            case OrExpression o             -> "Or: " + o.evaluate();
+        };
+        LOGGER.info(description);
+    }
+}
diff --git 
a/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21NewFeatureTests.java
 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21NewFeatureTests.java
new file mode 100644
index 000000000..9ea0ed495
--- /dev/null
+++ 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21NewFeatureTests.java
@@ -0,0 +1,166 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.ioc.services;
+
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.tapestry5.ioc.Registry;
+import org.apache.tapestry5.ioc.RegistryBuilder;
+import org.apache.tapestry5.ioc.ServiceBinder;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests for Tapestry-IoC with Java language features introduced between Java 
SE 18 and 21.
+ * <ul>
+ *   <li>JEP 431: Sequenced collections</li>
+ *   <li>JEP 440: Record patterns</li>
+ *   <li>JEP 441: Pattern matching</li>
+ *   <li>JEP 444: Virtual threads</li>
+ * </ul>
+ */
+class Java18To21NewFeatureTests
+{
+    public static class Java18To21Module
+    {
+        public static void bind(ServiceBinder binder)
+        {
+            binder.bind(Java18To21Service.class, Java18To21ServiceImpl.class);
+            binder.bind(Java18To21ConcreteService.class);
+        }
+    }
+
+    private static Java18To21Service java18To21Service;
+
+    private static Java18To21ConcreteService java18To21ConcreteService;
+
+    private static Registry registry;
+
+    @BeforeAll
+    public static void setup()
+    {
+        registry = 
RegistryBuilder.buildAndStartupRegistry(Java18To21Module.class);
+        java18To21Service = registry.getService(Java18To21Service.class);
+        java18To21ConcreteService = 
registry.getService(Java18To21ConcreteService.class);
+    }
+
+    /**
+     * Tests SequencedCollection as a service return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    @Test
+    public void sequencedCollectionReturnType()
+    {
+        SequencedCollection<String> serviceCol = 
java18To21Service.getSequencedCollection();
+
+        assertNotNull(serviceCol);
+        assertEquals("first", serviceCol.getFirst());
+        assertEquals("last", serviceCol.getLast());
+
+        SequencedCollection<String> concreteCol = 
java18To21ConcreteService.getSequencedCollection();
+        assertNotNull(concreteCol);
+        assertEquals("first", concreteCol.getFirst());
+        assertEquals("last", concreteCol.getLast());
+    }
+
+    /**
+     * Tests SequencedMap as a service return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    @Test
+    public void sequencedMapReturnType()
+    {
+        SequencedMap<String, Integer> serviceMap = 
java18To21Service.getSequencedMap();
+        assertNotNull(serviceMap);
+        assertEquals("one", serviceMap.firstEntry().getKey());
+        assertEquals("three", serviceMap.lastEntry().getKey());
+
+        SequencedMap<String, Integer> concreteMap = 
java18To21ConcreteService.getSequencedMap();
+        assertNotNull(concreteMap);
+        assertEquals("one", concreteMap.firstEntry().getKey());
+        assertEquals("three", concreteMap.lastEntry().getKey());
+    }
+
+    /**
+     * Tests record patterns in switch expressions (JEP 440).
+     * https://openjdk.org/jeps/440
+     */
+    @Test
+    public void recordPatternsInSwitch()
+    {
+        java18To21Service.recordPatternsInSwitch();
+        java18To21ConcreteService.recordPatternsInSwitch();
+    }
+
+    /**
+     * Tests pattern matching for switch with a sealed class hierarchy (JEP 
441).
+     * @see https://openjdk.org/jeps/441
+     */
+    @Test
+    public void patternMatchingForSwitchWithSealedClass()
+    {
+        java18To21Service.patternMatchingForSwitchWithSealedClass();
+        java18To21ConcreteService.patternMatchingForSwitchWithSealedClass();
+    }
+
+    /**
+     * Tests pattern matching for switch with a sealed interface (JEP 441).
+     * @see https://openjdk.org/jeps/441
+     */
+    @Test
+    public void patternMatchingForSwitchWithSealedInterface()
+    {
+        java18To21Service.patternMatchingForSwitchWithSealedInterface();
+        
java18To21ConcreteService.patternMatchingForSwitchWithSealedInterface();
+    }
+
+    /**
+     * Tests record patterns in instanceof expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    @Test
+    public void recordPatternsInInstanceOf()
+    {
+        java18To21Service.recordPatternsInInstanceOf();
+        java18To21ConcreteService.recordPatternsInInstanceOf();
+    }
+
+    /**
+     * Tests that Tapestry service proxies are callable from virtual threads 
(JEP 444).
+     * @see https://openjdk.org/jeps/444
+     */
+    @Test
+    public void virtualThreadServiceCall() throws InterruptedException
+    {
+        AtomicReference<String> serviceResult = new AtomicReference<>();
+        AtomicReference<String> concreteResult = new AtomicReference<>();
+
+        Thread vt1 = Thread.ofVirtual().start(() ->
+                
serviceResult.set(java18To21Service.getSequencedCollection().getFirst()));
+        Thread vt2 = Thread.ofVirtual().start(() ->
+                
concreteResult.set(java18To21ConcreteService.getSequencedCollection().getFirst()));
+
+        vt1.join();
+        vt2.join();
+
+        assertEquals("first", serviceResult.get());
+        assertEquals("first", concreteResult.get());
+    }
+
+}
diff --git 
a/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21Service.java
 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21Service.java
new file mode 100644
index 000000000..705b47aef
--- /dev/null
+++ 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21Service.java
@@ -0,0 +1,55 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.ioc.services;
+
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+
+public interface Java18To21Service
+{
+
+    /**
+     * Tests SequencedCollection as a return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    SequencedCollection<String> getSequencedCollection();
+
+    /**
+     * Tests SequencedMap as a return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    SequencedMap<String, Integer> getSequencedMap();
+
+    /**
+     * Tests record patterns in instanceof expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    void recordPatternsInInstanceOf();
+
+    /**
+     * Tests record patterns in switch expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    void recordPatternsInSwitch();
+
+    /**
+     * Tests pattern matching for switch with a sealed class hierarchy (JEP 
441).
+     * @see https://openjdk.org/jeps/441
+     */
+    void patternMatchingForSwitchWithSealedClass();
+
+    /**
+     * Tests pattern matching for switch with a sealed interface (JEP 441).
+     * @see https://openjdk.org/jeps/441
+     */
+    void patternMatchingForSwitchWithSealedInterface();
+}
diff --git 
a/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21ServiceImpl.java
 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21ServiceImpl.java
new file mode 100644
index 000000000..850696aa4
--- /dev/null
+++ 
b/tapestry-latest-java-tests/src/test/java/org/apache/tapestry5/ioc/services/Java18To21ServiceImpl.java
@@ -0,0 +1,142 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.ioc.services;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Java18To21ServiceImpl implements Java18To21Service
+{
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(Java18To21ServiceImpl.class);
+
+    /**
+     * Tests SequencedCollection as a return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    @Override
+    public SequencedCollection<String> getSequencedCollection()
+    {
+        SequencedCollection<String> list = new ArrayList<>();
+        list.add("first");
+        list.add("middle");
+        list.add("last");
+        return list;
+    }
+
+    /**
+     * Tests SequencedMap as a return type (JEP 431).
+     * @see https://openjdk.org/jeps/431
+     */
+    @Override
+    public SequencedMap<String, Integer> getSequencedMap()
+    {
+        SequencedMap<String, Integer> map = new LinkedHashMap<>();
+        map.put("one", 1);
+        map.put("two", 2);
+        map.put("three", 3);
+        return map;
+    }
+
+    /**
+     * Tests record patterns in instanceof expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    @Override
+    public void recordPatternsInInstanceOf()
+    {
+        IntExpression expr = new ConstantIntExpression(42);
+        if (expr instanceof ConstantIntExpression(int v))
+        {
+            LOGGER.info("Constant int value: " + v);
+        }
+
+        IntTuple tuple = new IntTuple(3, 7);
+        if (tuple instanceof IntTuple(int x, int y))
+        {
+            LOGGER.info("Tuple sum: " + (x + y));
+        }
+    }
+
+    /**
+     * Tests record patterns in switch expressions (JEP 440).
+     * @see https://openjdk.org/jeps/440
+     */
+    @Override
+    public void recordPatternsInSwitch()
+    {
+        IntExpression expr = new PlusExpression(new ConstantIntExpression(2), 
new ConstantIntExpression(3));
+        int result = evalIntExpression(expr);
+        LOGGER.info("Switch record pattern result: " + result);
+
+        IntExpression squareExpr = new SquareExpression(new 
ConstantIntExpression(5));
+        LOGGER.info("Square result: " + evalIntExpression(squareExpr));
+    }
+
+    private int evalIntExpression(IntExpression e)
+    {
+        return switch (e)
+        {
+            case ConstantIntExpression(int v)            -> v;
+            case PlusExpression(var left, var right)     -> 
evalIntExpression(left) + evalIntExpression(right);
+            case SquareExpression(var inner)              -> { int v = 
evalIntExpression(inner); yield v * v; }
+        };
+    }
+
+    /**
+     * Tests pattern matching for switch with a sealed class hierarchy (JEP 
441).
+     * @see https://openjdk.org/jeps/441
+     */
+    @Override
+    public void patternMatchingForSwitchWithSealedClass()
+    {
+        for (Appliance appliance : new Appliance[] { new WashingMachine(), new 
Freezer(), new Refrigerator() })
+        {
+            String description = switch (appliance)
+            {
+                case WashingMachine wm -> "Washer: " + wm.getName();
+                case Freezer f         -> "Freezer: " + f.getName();
+                case Refrigerator r    -> "Fridge: " + r.getName();
+                case CoolingMachine cm -> "Cooler: " + cm.getName();
+            };
+            LOGGER.info(description);
+        }
+    }
+
+    /**
+     * Tests pattern matching for switch with a sealed interface (JEP 441).
+        }
+     * https://openjdk.org/jeps/441
+     */
+    @Override
+    public void patternMatchingForSwitchWithSealedInterface()
+    {
+        BinaryExpression expr = new AndExpression(
+                new ConstantBinaryExpression(true),
+                new OrExpression(new ConstantBinaryExpression(false), new 
ConstantBinaryExpression(true)));
+
+        String description = switch (expr)
+        {
+            case ConstantBinaryExpression c -> "Constant: " + c.evaluate();
+            case AndExpression a            -> "And: " + a.evaluate();
+            case OrExpression o             -> "Or: " + o.evaluate();
+        };
+        LOGGER.info(description);
+    }
+}

Reply via email to