This is an automated email from the ASF dual-hosted git repository. benw pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 5ce05d7d024912ed6320ab49c13b657ae6c0dd1a Author: Ben Weidig <[email protected]> AuthorDate: Sun Apr 5 10:47:00 2026 +0200 TAP5-2825: Java 21 support --- .../groovy/ioc/specs/ClasspathResourceSpec.groovy | 5 +- 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, 507 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..d4db85cd5 100644 --- a/tapestry-ioc/src/test/groovy/ioc/specs/ClasspathResourceSpec.groovy +++ b/tapestry-ioc/src/test/groovy/ioc/specs/ClasspathResourceSpec.groovy @@ -288,7 +288,10 @@ 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, + // as it returns the same underlying Enumeration<URL>. + 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 d4924c1fa..7eff92d4c 100644 --- a/tapestry-latest-java-tests/build.gradle +++ b/tapestry-latest-java-tests/build.gradle @@ -6,15 +6,14 @@ 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); + } +}
