From 05533431e93ef2daf1badf03e873021b855e9abf Mon Sep 17 00:00:00 2001 From: Ludovic Champenois Date: Thu, 12 Nov 2026 07:36:28 -0800 Subject: [PATCH 1/3] Allow ContainerIncludeJarPattern to match jakarta.servlet-api in Jetty 12.1.15 --- .../tools/development/jetty/ee11/DevAppEngineWebAppContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java index 341264ed..51fbb8e4 100644 --- a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java +++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/DevAppEngineWebAppContext.java @@ -67,7 +67,7 @@ public DevAppEngineWebAppContext( setAttribute(JASPER_SERVLET_CLASSPATH, buildClasspath()); setAttribute( "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", - ".*/jakarta.servlet-api-[^/]*\\.jar$|.*jakarta.servlet.jsp.jstl-.*\\.jar$"); + ".*jakarta.servlet-api-[^/]*\\.jar$|.*jakarta.servlet.jsp.jstl-.*\\.jar$"); // Make ApiProxyLocal available via the servlet context. This allows // servlets that are part of the dev appserver (like those that render the From 691abf592246bd23bf876a11a98fa03b00b6814d Mon Sep 17 00:00:00 2001 From: Ludovic Champenois Date: Thu, 12 Nov 2026 08:36:12 -0800 Subject: [PATCH 2/3] Add regression tests for Jetty SDK classpath and fix DevAppServerModulesFilter import in EE11 --- .../appengine/tools/info/Jetty121EE8Sdk.java | 1 + .../tools/development/JettySdkTest.java | 135 ++++++++++++++++++ .../jetty/ee11/JettyContainerService.java | 2 +- 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/api_dev/src/main/java/com/google/appengine/tools/info/Jetty121EE8Sdk.java b/api_dev/src/main/java/com/google/appengine/tools/info/Jetty121EE8Sdk.java index 43654a01..677f36ba 100644 --- a/api_dev/src/main/java/com/google/appengine/tools/info/Jetty121EE8Sdk.java +++ b/api_dev/src/main/java/com/google/appengine/tools/info/Jetty121EE8Sdk.java @@ -270,6 +270,7 @@ protected List getJetty121Jars(String subDir) { || f.getName().contains("-gcloud-") || f.getName().contains("-infinispan-") || f.getName().contains("-hazelcast-") + || f.getName().contains("-ethereum-") || f.getName().contains("jakarta.enterprise") || f.getName().contains("jakarta.inject") || f.getName().contains("jakarta.interceptor") diff --git a/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java b/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java index 8d324d1c..d3f48a40 100644 --- a/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java +++ b/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java @@ -17,12 +17,20 @@ import static com.google.appengine.tools.development.DevAppServerTestBase.getSdkRoot; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import com.google.appengine.tools.info.AppengineSdk; +import com.google.common.collect.ImmutableList; import java.io.File; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.List; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -158,4 +166,131 @@ public void testJetty121EE8() throws Exception { System.out.println("getImplLibs"); assertUrlsExist(sdk.getImplLibs()); } + + @Test + public void testContainerIncludeJarPatternMatchesSharedJars() { + System.setProperty("appengine.use.EE8", "false"); + System.setProperty("appengine.use.EE11", "true"); + System.setProperty("appengine.use.jetty121", "true"); + AppengineSdk.resetSdk(); + AppengineSdk sdk = AppengineSdk.getSdk(); + List sharedLibs = ImmutableList.copyOf(sdk.getSharedLibFiles()); + Pattern pattern = + Pattern.compile(".*jakarta.servlet-api-[^/]*\\.jar$|.*jakarta.servlet.jsp.jstl-.*\\.jar$"); + + boolean matchedServletApi = + sharedLibs.stream() + .map(File::toURI) + .map(URI::toString) + .anyMatch(uri -> pattern.matcher(uri).matches()); + assertWithMessage( + "Pattern %s must match a servlet-api jar in sharedLibs: %s", pattern, sharedLibs) + .that(matchedServletApi) + .isTrue(); + + boolean matchedJstlApi = + sharedLibs.stream() + .map(File::toURI) + .map(URI::toString) + .anyMatch(uri -> pattern.matcher(uri).matches()); + assertWithMessage("Pattern %s must match a JSTL jar in sharedLibs: %s", pattern, sharedLibs) + .that(matchedJstlApi) + .isTrue(); + } + + @Test + public void testNoJuliLoggingSpiInSharedOrJspLibs() throws Exception { + for (String ee : new String[] {"EE8", "EE11"}) { + System.setProperty("appengine.use.EE8", String.valueOf("EE8".equals(ee))); + System.setProperty("appengine.use.EE11", String.valueOf("EE11".equals(ee))); + System.setProperty("appengine.use.jetty121", "true"); + AppengineSdk.resetSdk(); + AppengineSdk sdk = AppengineSdk.getSdk(); + for (File jar : sdk.getSharedLibFiles()) { + try (JarFile jarFile = new JarFile(jar)) { + ZipEntry entry = jarFile.getEntry("META-INF/services/org.apache.juli.logging.Log"); + assertWithMessage( + "Jar %s in %s shared libs must not contain JULI Log SPI", jar.getName(), ee) + .that(entry) + .isNull(); + } + } + for (File jar : sdk.getUserJspLibFiles()) { + try (JarFile jarFile = new JarFile(jar)) { + ZipEntry entry = jarFile.getEntry("META-INF/services/org.apache.juli.logging.Log"); + assertWithMessage( + "Jar %s in %s user JSP libs must not contain JULI Log SPI", jar.getName(), ee) + .that(entry) + .isNull(); + } + } + } + } + + @Test + public void testJspClasspathContainsRequiredClasses() throws Exception { + // EE8 JSP classpath check + System.setProperty("appengine.use.EE8", "true"); + System.setProperty("appengine.use.EE11", "false"); + System.setProperty("appengine.use.jetty121", "true"); + AppengineSdk.resetSdk(); + AppengineSdk ee8Sdk = AppengineSdk.getSdk(); + List ee8Urls = new ArrayList<>(ee8Sdk.getUserJspLibs()); + ee8Urls.addAll(ee8Sdk.getSharedLibs()); + try (URLClassLoader cl = new URLClassLoader(ee8Urls.toArray(new URL[0]), null)) { + assertThat(cl.loadClass("javax.servlet.jsp.JspPage")).isNotNull(); + assertThat(cl.loadClass("org.apache.jasper.servlet.JspServlet")).isNotNull(); + } + + // EE11 JSP classpath check + System.setProperty("appengine.use.EE8", "false"); + System.setProperty("appengine.use.EE11", "true"); + System.setProperty("appengine.use.jetty121", "true"); + AppengineSdk.resetSdk(); + AppengineSdk ee11Sdk = AppengineSdk.getSdk(); + List ee11Urls = new ArrayList<>(ee11Sdk.getUserJspLibs()); + ee11Urls.addAll(ee11Sdk.getSharedLibs()); + try (URLClassLoader cl = new URLClassLoader(ee11Urls.toArray(new URL[0]), null)) { + assertThat(cl.loadClass("jakarta.servlet.jsp.JspPage")).isNotNull(); + assertThat(cl.loadClass("jakarta.el.RecordELResolver")).isNotNull(); + assertThat(cl.loadClass("org.glassfish.wasp.runtime.TldScanner")).isNotNull(); + } + } + + @Test + public void testForbiddenModulesNotExposed() { + List forbidden = + List.of( + "-cdi-", + "-alpn-", + "-demo-", + "-websocket-", + "-gcloud-", + "-infinispan-", + "-hazelcast-", + "-ethereum-"); + + for (String ee : new String[] {"EE8", "EE11"}) { + System.setProperty("appengine.use.EE8", String.valueOf("EE8".equals(ee))); + System.setProperty("appengine.use.EE11", String.valueOf("EE11".equals(ee))); + System.setProperty("appengine.use.jetty121", "true"); + AppengineSdk.resetSdk(); + AppengineSdk sdk = AppengineSdk.getSdk(); + + for (File jar : sdk.getSharedLibFiles()) { + for (String f : forbidden) { + assertWithMessage("Shared jar %s must not contain %s", jar.getName(), f) + .that(jar.getName().contains(f)) + .isFalse(); + } + } + for (File jar : sdk.getUserJspLibFiles()) { + for (String f : forbidden) { + assertWithMessage("JSP jar %s must not contain %s", jar.getName(), f) + .that(jar.getName().contains(f)) + .isFalse(); + } + } + } + } } diff --git a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java index 5f040c6e..e0549cc2 100644 --- a/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java +++ b/runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java @@ -24,7 +24,7 @@ import com.google.appengine.tools.development.ApiProxyLocal; import com.google.appengine.tools.development.AppContext; import com.google.appengine.tools.development.DevAppServer; -import com.google.appengine.tools.development.DevAppServerModulesFilter; +import com.google.appengine.tools.development.jakarta.DevAppServerModulesFilter; import com.google.appengine.tools.development.IsolatedAppClassLoader; import com.google.appengine.tools.development.LocalEnvironment; import com.google.appengine.tools.development.jakarta.LocalHttpRequestEnvironment; From 09ef06a263d321fae048274af52883a947fc9f2c Mon Sep 17 00:00:00 2001 From: Ludovic Champenois Date: Thu, 12 Nov 2026 09:03:56 -0800 Subject: [PATCH 3/3] Add Javadoc documentation for Jetty SDK regression tests --- .../tools/development/JettySdkTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java b/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java index d3f48a40..dba4341b 100644 --- a/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java +++ b/e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/JettySdkTest.java @@ -167,6 +167,11 @@ public void testJetty121EE8() throws Exception { assertUrlsExist(sdk.getImplLibs()); } + /** + * Verifies that the ContainerIncludeJarPattern configured on DevAppEngineWebAppContext + * successfully matches the shared container jars (e.g. jakarta.servlet-api and JSTL) in the SDK. + * This ensures that TLDs and web fragments in shared libraries are scanned and available to webapps. + */ @Test public void testContainerIncludeJarPatternMatchesSharedJars() { System.setProperty("appengine.use.EE8", "false"); @@ -198,6 +203,11 @@ public void testContainerIncludeJarPatternMatchesSharedJars() { .isTrue(); } + /** + * Verifies that no jar file exposed in shared or JSP libraries contains the Tomcat JULI Log + * ServiceLoader provider (META-INF/services/org.apache.juli.logging.Log). + * The presence of this SPI provider can hijack Apache Commons Logging and cause deadlocks. + */ @Test public void testNoJuliLoggingSpiInSharedOrJspLibs() throws Exception { for (String ee : new String[] {"EE8", "EE11"}) { @@ -227,6 +237,11 @@ public void testNoJuliLoggingSpiInSharedOrJspLibs() throws Exception { } } + /** + * Verifies that all classes required for JSP compilation and execution (e.g. Jasper servlet, + * JspPage, RecordELResolver, and TldScanner) are resolvable from the combined user JSP libs + * and shared libs classpath on both EE8 and EE11. + */ @Test public void testJspClasspathContainsRequiredClasses() throws Exception { // EE8 JSP classpath check @@ -257,6 +272,11 @@ public void testJspClasspathContainsRequiredClasses() throws Exception { } } + /** + * Verifies that optional, clustering, or experimental Jetty modules (such as CDI, ALPN, + * WebSocket, Hazelcast, Infinispan, and Ethereum) are excluded from the shared and JSP classpaths + * to prevent accidental classloader contamination or conflicts. + */ @Test public void testForbiddenModulesNotExposed() { List forbidden =