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

markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new d9def06167 Reduce minimum Java version to 17
d9def06167 is described below

commit d9def06167fe566674e00ebdbdce8591a228cce2
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Fri Jan 19 12:15:20 2024 +0000

    Reduce minimum Java version to 17
    
    Uses some API that is deprecated in Java 21 but not 17. JreCompat has
    not been used to avoid the deprecated API if running on Java 21+.
---
 build.xml                                          |   8 +-
 conf/web.xml                                       |   4 +-
 .../authenticator/SpnegoAuthenticator.java         |  65 +++++++++---
 .../core/ThreadLocalLeakPreventionListener.java    |   1 -
 .../catalina/valves/StuckThreadDetectionValve.java |  10 +-
 .../catalina/webresources/CachedResource.java      |   3 -
 java/org/apache/coyote/Request.java                |   4 +-
 java/org/apache/jasper/EmbeddedServletOptions.java |   4 +-
 java/org/apache/jasper/JspC.java                   |   4 +-
 java/org/apache/jasper/compiler/JDTCompiler.java   |  14 +--
 .../tomcat/dbcp/dbcp2/DelegatingStatement.java     |   2 +-
 java/org/apache/tomcat/util/Diagnostics.java       |   1 -
 .../org/apache/tomcat/util/compat/Jre19Compat.java |  84 +++++++++++++++
 .../org/apache/tomcat/util/compat/Jre21Compat.java |  82 ++++++++++++++
 .../org/apache/tomcat/util/compat/Jre22Compat.java |   2 +-
 java/org/apache/tomcat/util/compat/JreCompat.java  | 118 ++++++++++++++++++++-
 .../apache/tomcat/util/net/AbstractEndpoint.java   |   1 -
 .../tomcat/util/threads/VirtualThreadExecutor.java |  11 +-
 .../TestAsyncContextImplListenerOnComplete.java    |  31 +++---
 test/org/apache/juli/TestThreadNameCache.java      |   2 +-
 webapps/docs/changelog.xml                         |  12 +++
 webapps/docs/jasper-howto.xml                      |   4 +-
 webapps/docs/tomcat-docs.xsl                       |   4 +-
 23 files changed, 398 insertions(+), 73 deletions(-)

diff --git a/build.xml b/build.xml
index 22379895f9..76d1feb07c 100644
--- a/build.xml
+++ b/build.xml
@@ -104,11 +104,11 @@
   <!-- build output directory for generated reflectionless code -->
   <property name="tomcat.xreflect"       value="${tomcat.output}/xreflect"/>
 
-  <!-- Jakarta EE 11 platform requires Java 21+ -->
+  <!-- Jakarta EE 11 platform requires Java 17+ -->
   <!-- Keep in sync with webapps/docs/tomcat-docs.xsl -->
-  <property name="compile.release" value="21"/>
-  <property name="min.java.version" value="21"/>
-  <property name="build.java.version" value="21"/>
+  <property name="compile.release" value="17"/>
+  <property name="min.java.version" value="17"/>
+  <property name="build.java.version" value="17"/>
   <property name="release.java.version" value="22"/>
 
   <!-- Check Java Build Version -->
diff --git a/conf/web.xml b/conf/web.xml
index 0a1261e3b3..788f411d48 100644
--- a/conf/web.xml
+++ b/conf/web.xml
@@ -160,9 +160,9 @@
   <!--                       pages.  See the jasper documentation for more  -->
   <!--                       information.                                   -->
   <!--                                                                      -->
-  <!--   compilerSourceVM    Compiler source VM. [21]                       -->
+  <!--   compilerSourceVM    Compiler source VM. [17]                       -->
   <!--                                                                      -->
-  <!--   compilerTargetVM    Compiler target VM. [21]                       -->
+  <!--   compilerTargetVM    Compiler target VM. [17]                       -->
   <!--                                                                      -->
   <!--   development         Is Jasper used in development mode? If true,   -->
   <!--                       the frequency at which JSPs are checked for    -->
diff --git a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 
b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
index d76edb9ecf..6377416ea4 100644
--- a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
+++ b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
@@ -19,8 +19,10 @@ package org.apache.catalina.authenticator;
 import java.io.File;
 import java.io.IOException;
 import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
 import java.util.LinkedHashMap;
-import java.util.concurrent.CompletionException;
 import java.util.regex.Pattern;
 
 import javax.security.auth.Subject;
@@ -30,6 +32,7 @@ import javax.security.auth.login.LoginException;
 import jakarta.servlet.http.HttpServletResponse;
 
 import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Realm;
 import org.apache.catalina.connector.Request;
 import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
@@ -205,16 +208,11 @@ public class SpnegoAuthenticator extends 
AuthenticatorBase {
             } else {
                 credentialLifetime = GSSCredential.DEFAULT_LIFETIME;
             }
+            final PrivilegedExceptionAction<GSSCredential> action = () -> 
manager.createCredential(null,
+                    credentialLifetime, new Oid("1.3.6.1.5.5.2"), 
GSSCredential.ACCEPT_ONLY);
+            gssContext = manager.createContext(Subject.doAs(subject, action));
 
-            gssContext = manager.createContext(Subject.callAs(subject, () -> {
-                return manager.createCredential(null, credentialLifetime, new 
Oid("1.3.6.1.5.5.2"),
-                        GSSCredential.ACCEPT_ONLY);
-            }));
-
-            final GSSContext gssContextFinal = gssContext;
-            outToken = Subject.callAs(subject, () -> {
-                return gssContextFinal.acceptSecContext(decoded, 0, 
decoded.length);
-            });
+            outToken = Subject.doAs(lc.getSubject(), new 
AcceptAction(gssContext, decoded));
 
             if (outToken == null) {
                 if (log.isDebugEnabled()) {
@@ -226,9 +224,8 @@ public class SpnegoAuthenticator extends AuthenticatorBase {
                 return false;
             }
 
-            principal = Subject.callAs(subject, () -> {
-                return context.getRealm().authenticate(gssContextFinal, 
storeDelegatedCredential);
-            });
+            principal = Subject.doAs(subject,
+                    new AuthenticateAction(context.getRealm(), gssContext, 
storeDelegatedCredential));
 
         } catch (GSSException e) {
             if (log.isDebugEnabled()) {
@@ -237,7 +234,7 @@ public class SpnegoAuthenticator extends AuthenticatorBase {
             response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
             response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
             return false;
-        } catch (CompletionException e) {
+        } catch (PrivilegedActionException e) {
             Throwable cause = e.getCause();
             if (cause instanceof GSSException) {
                 if (log.isDebugEnabled()) {
@@ -294,6 +291,46 @@ public class SpnegoAuthenticator extends AuthenticatorBase 
{
     }
 
 
+    /**
+     * This class gets a gss credential via a privileged action.
+     */
+    public static class AcceptAction implements 
PrivilegedExceptionAction<byte[]> {
+
+        GSSContext gssContext;
+
+        byte[] decoded;
+
+        public AcceptAction(GSSContext context, byte[] decodedToken) {
+            this.gssContext = context;
+            this.decoded = decodedToken;
+        }
+
+        @Override
+        public byte[] run() throws GSSException {
+            return gssContext.acceptSecContext(decoded, 0, decoded.length);
+        }
+    }
+
+
+    public static class AuthenticateAction implements 
PrivilegedAction<Principal> {
+
+        private final Realm realm;
+        private final GSSContext gssContext;
+        private final boolean storeDelegatedCredential;
+
+        public AuthenticateAction(Realm realm, GSSContext gssContext, boolean 
storeDelegatedCredential) {
+            this.realm = realm;
+            this.gssContext = gssContext;
+            this.storeDelegatedCredential = storeDelegatedCredential;
+        }
+
+        @Override
+        public Principal run() {
+            return realm.authenticate(gssContext, storeDelegatedCredential);
+        }
+    }
+
+
     /**
      * This class implements a hack around an incompatibility between the 
SPNEGO implementation in Windows and the
      * SPNEGO implementation in Java 8 update 40 onwards. It was introduced by 
the change to fix this bug:
diff --git 
a/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java 
b/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java
index 0389c3ae02..c413317186 100644
--- a/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java
+++ b/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java
@@ -119,7 +119,6 @@ public class ThreadLocalLeakPreventionListener extends 
FrameworkListener {
                 }
 
                 if (executor instanceof ThreadPoolExecutor) {
-                    @SuppressWarnings("resource")
                     ThreadPoolExecutor threadPoolExecutor = 
(ThreadPoolExecutor) executor;
                     threadPoolExecutor.contextStopping();
                 } else if (executor instanceof StandardThreadExecutor) {
diff --git a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java 
b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java
index a19629fde2..a523029d75 100644
--- a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java
+++ b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java
@@ -138,7 +138,7 @@ public class StuckThreadDetectionValve extends ValveBase {
             String msg = 
sm.getString("stuckThreadDetectionValve.notifyStuckThreadDetected",
                     monitoredThread.getThread().getName(), 
Long.valueOf(activeTime), monitoredThread.getStartTime(),
                     Integer.valueOf(numStuckThreads), 
monitoredThread.getRequestUri(), Integer.valueOf(threshold),
-                    String.valueOf(monitoredThread.getThread().threadId()));
+                    String.valueOf(monitoredThread.getThread().getId()));
             // msg += "\n" + getStackTraceAsString(trace);
             Throwable th = new Throwable();
             th.setStackTrace(monitoredThread.getThread().getStackTrace());
@@ -171,7 +171,7 @@ public class StuckThreadDetectionValve extends ValveBase {
         // GC'ing, as the reference is removed from the Map in the finally 
clause
 
         Thread currentThread = Thread.currentThread();
-        Long key = Long.valueOf(currentThread.threadId());
+        Long key = Long.valueOf(currentThread.getId());
         StringBuffer requestUrl = request.getRequestURL();
         if (request.getQueryString() != null) {
             requestUrl.append('?');
@@ -231,7 +231,7 @@ public class StuckThreadDetectionValve extends ValveBase {
         List<Long> idList = new ArrayList<>();
         for (MonitoredThread monitoredThread : activeThreads.values()) {
             if (monitoredThread.isMarkedAsStuck()) {
-                
idList.add(Long.valueOf(monitoredThread.getThread().threadId()));
+                idList.add(Long.valueOf(monitoredThread.getThread().getId()));
             }
         }
 
@@ -345,7 +345,7 @@ public class StuckThreadDetectionValve extends ValveBase {
                     String msg = 
sm.getString("stuckThreadDetectionValve.notifyStuckThreadInterrupted",
                             this.getThread().getName(), 
Long.valueOf(getActiveTimeInMillis()), this.getStartTime(),
                             this.getRequestUri(), 
Long.valueOf(interruptThreadThreshold),
-                            String.valueOf(this.getThread().threadId()));
+                            String.valueOf(this.getThread().getId()));
                     Throwable th = new Throwable();
                     th.setStackTrace(this.getThread().getStackTrace());
                     log.warn(msg, th);
@@ -371,7 +371,7 @@ public class StuckThreadDetectionValve extends ValveBase {
 
         CompletedStuckThread(Thread thread, long totalActiveTime) {
             this.threadName = thread.getName();
-            this.threadId = thread.threadId();
+            this.threadId = thread.getId();
             this.totalActiveTime = totalActiveTime;
         }
 
diff --git a/java/org/apache/catalina/webresources/CachedResource.java 
b/java/org/apache/catalina/webresources/CachedResource.java
index 96dcf0c011..14d2b378ca 100644
--- a/java/org/apache/catalina/webresources/CachedResource.java
+++ b/java/org/apache/catalina/webresources/CachedResource.java
@@ -341,9 +341,6 @@ public class CachedResource implements WebResource {
         try {
             CachedResourceURLStreamHandler handler = new 
CachedResourceURLStreamHandler(resourceURL, root, webAppPath,
                     usesClassLoaderResources);
-            // Need the custom handler for all protocols. URL.of(...) does not 
allow file to use a custom handler.
-            // Ignore the deprecation for now.
-            @SuppressWarnings("deprecation")
             URL result = new URL(null, resourceURL.toExternalForm(), handler);
             handler.setCacheURL(result);
             return result;
diff --git a/java/org/apache/coyote/Request.java 
b/java/org/apache/coyote/Request.java
index 6d2ab8b640..245251b2e3 100644
--- a/java/org/apache/coyote/Request.java
+++ b/java/org/apache/coyote/Request.java
@@ -738,12 +738,12 @@ public final class Request {
 
     public void setRequestThread() {
         Thread t = Thread.currentThread();
-        threadId = t.threadId();
+        threadId = t.getId();
         getRequestProcessor().setWorkerThreadName(t.getName());
     }
 
     public boolean isRequestThread() {
-        return Thread.currentThread().threadId() == threadId;
+        return Thread.currentThread().getId() == threadId;
     }
 
     // -------------------- Per-Request "notes" --------------------
diff --git a/java/org/apache/jasper/EmbeddedServletOptions.java 
b/java/org/apache/jasper/EmbeddedServletOptions.java
index 81df0af31a..451dfbcfaf 100644
--- a/java/org/apache/jasper/EmbeddedServletOptions.java
+++ b/java/org/apache/jasper/EmbeddedServletOptions.java
@@ -124,12 +124,12 @@ public final class EmbeddedServletOptions implements 
Options {
     /**
      * Compiler target VM.
      */
-    private String compilerTargetVM = "21";
+    private String compilerTargetVM = "17";
 
     /**
      * The compiler source VM.
      */
-    private String compilerSourceVM = "21";
+    private String compilerSourceVM = "17";
 
     /**
      * The compiler class name.
diff --git a/java/org/apache/jasper/JspC.java b/java/org/apache/jasper/JspC.java
index e31918cca5..8656617bef 100644
--- a/java/org/apache/jasper/JspC.java
+++ b/java/org/apache/jasper/JspC.java
@@ -197,8 +197,8 @@ public class JspC extends Task implements Options {
 
     protected String compiler = null;
 
-    protected String compilerTargetVM = "21";
-    protected String compilerSourceVM = "21";
+    protected String compilerTargetVM = "17";
+    protected String compilerSourceVM = "17";
 
     protected boolean classDebugInfo = true;
 
diff --git a/java/org/apache/jasper/compiler/JDTCompiler.java 
b/java/org/apache/jasper/compiler/JDTCompiler.java
index 727f61393a..16bf7060f7 100644
--- a/java/org/apache/jasper/compiler/JDTCompiler.java
+++ b/java/org/apache/jasper/compiler/JDTCompiler.java
@@ -328,11 +328,11 @@ public class JDTCompiler extends 
org.apache.jasper.compiler.Compiler {
                 settings.put(CompilerOptions.OPTION_Source, "22");
             } else {
                 log.warn(Localizer.getMessage("jsp.warning.unknown.sourceVM", 
opt));
-                settings.put(CompilerOptions.OPTION_Source, 
CompilerOptions.VERSION_21);
+                settings.put(CompilerOptions.OPTION_Source, 
CompilerOptions.VERSION_17);
             }
         } else {
-            // Default to 21
-            settings.put(CompilerOptions.OPTION_Source, 
CompilerOptions.VERSION_21);
+            // Default to 17
+            settings.put(CompilerOptions.OPTION_Source, 
CompilerOptions.VERSION_17);
         }
 
         // Target JVM
@@ -407,12 +407,12 @@ public class JDTCompiler extends 
org.apache.jasper.compiler.Compiler {
                 settings.put(CompilerOptions.OPTION_Compliance, "22");
             } else {
                 log.warn(Localizer.getMessage("jsp.warning.unknown.targetVM", 
opt));
-                settings.put(CompilerOptions.OPTION_TargetPlatform, 
CompilerOptions.VERSION_21);
+                settings.put(CompilerOptions.OPTION_TargetPlatform, 
CompilerOptions.VERSION_17);
             }
         } else {
-            // Default to 21
-            settings.put(CompilerOptions.OPTION_TargetPlatform, 
CompilerOptions.VERSION_21);
-            settings.put(CompilerOptions.OPTION_Compliance, 
CompilerOptions.VERSION_21);
+            // Default to 17
+            settings.put(CompilerOptions.OPTION_TargetPlatform, 
CompilerOptions.VERSION_17);
+            settings.put(CompilerOptions.OPTION_Compliance, 
CompilerOptions.VERSION_17);
         }
 
         final IProblemFactory problemFactory = new 
DefaultProblemFactory(Locale.getDefault());
diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java 
b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java
index 9e5853894a..ff5d937fc4 100644
--- a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java
+++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java
@@ -368,7 +368,7 @@ public class DelegatingStatement extends AbandonedTrace 
implements Statement {
         }
     }
 
-    @SuppressWarnings("removal") // Need Commons DBCP to address this
+    @SuppressWarnings("deprecation") // Need Commons DBCP to address this
     @Override
     protected void finalize() throws Throwable {
         // This is required because of statement pooling. The poolable
diff --git a/java/org/apache/tomcat/util/Diagnostics.java 
b/java/org/apache/tomcat/util/Diagnostics.java
index e4fb8631be..77091e5c49 100644
--- a/java/org/apache/tomcat/util/Diagnostics.java
+++ b/java/org/apache/tomcat/util/Diagnostics.java
@@ -487,7 +487,6 @@ public class Diagnostics {
      * @param requestedSm the StringManager to use
      * @return the formatted JVM information text
      */
-    @SuppressWarnings("deprecation")
     public static String getVMInfo(StringManager requestedSm) {
         StringBuilder sb = new StringBuilder();
 
diff --git a/java/org/apache/tomcat/util/compat/Jre19Compat.java 
b/java/org/apache/tomcat/util/compat/Jre19Compat.java
new file mode 100644
index 0000000000..60ee0c2dc1
--- /dev/null
+++ b/java/org/apache/tomcat/util/compat/Jre19Compat.java
@@ -0,0 +1,84 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You 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.tomcat.util.compat;
+
+import java.lang.reflect.Field;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+public class Jre19Compat extends JreCompat {
+
+    private static final Log log = LogFactory.getLog(Jre19Compat.class);
+    private static final StringManager sm = 
StringManager.getManager(Jre19Compat.class);
+
+    private static final boolean supported;
+
+    static {
+        // Don't need any Java 19 specific classes (yet) so just test for one 
of
+        // the new ones for now.
+        Class<?> c1 = null;
+        try {
+            c1 = Class.forName("java.lang.WrongThreadException");
+        } catch (ClassNotFoundException cnfe) {
+            // Must be pre-Java 19
+            log.debug(sm.getString("jre19Compat.javaPre19"), cnfe);
+        }
+
+        supported = (c1 != null);
+    }
+
+    static boolean isSupported() {
+        return supported;
+    }
+
+    @Override
+    public Object getExecutor(Thread thread)
+            throws NoSuchFieldException, SecurityException, 
IllegalArgumentException, IllegalAccessException {
+
+        Object result = super.getExecutor(thread);
+
+        if (result == null) {
+            Object holder = null;
+            Object task = null;
+            try {
+                Field holderField = 
thread.getClass().getDeclaredField("holder");
+                holderField.setAccessible(true);
+                holder = holderField.get(thread);
+
+                Field taskField = holder.getClass().getDeclaredField("task");
+                taskField.setAccessible(true);
+                task = taskField.get(holder);
+            } catch (NoSuchFieldException nfe) {
+                return null;
+            }
+
+            if (task!= null && task.getClass().getCanonicalName() != null &&
+                    (task.getClass().getCanonicalName().equals(
+                            
"org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") ||
+                            task.getClass().getCanonicalName().equals(
+                                    
"java.util.concurrent.ThreadPoolExecutor.Worker"))) {
+                Field executorField = 
task.getClass().getDeclaredField("this$0");
+                executorField.setAccessible(true);
+                result = executorField.get(task);
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/java/org/apache/tomcat/util/compat/Jre21Compat.java 
b/java/org/apache/tomcat/util/compat/Jre21Compat.java
new file mode 100644
index 0000000000..d06c8519a7
--- /dev/null
+++ b/java/org/apache/tomcat/util/compat/Jre21Compat.java
@@ -0,0 +1,82 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You 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.tomcat.util.compat;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+public class Jre21Compat extends Jre19Compat {
+
+    private static final Log log = LogFactory.getLog(Jre21Compat.class);
+    private static final StringManager sm = 
StringManager.getManager(Jre21Compat.class);
+
+    private static final Method nameMethod;
+    private static final Method startMethod;
+    private static final Method ofVirtualMethod;
+
+
+    static {
+        Class<?> c1 = null;
+        Method m1 = null;
+        Method m2 = null;
+        Method m3 = null;
+
+        try {
+            c1 = Class.forName("java.lang.Thread$Builder");
+            m1 = c1.getMethod("name", String.class, long.class);
+            m2 = c1.getMethod("start", Runnable.class);
+            m3 = Thread.class.getMethod("ofVirtual", (Class<?>[]) null);
+        } catch (ClassNotFoundException e) {
+            // Must be pre-Java 21
+            log.debug(sm.getString("jre21Compat.javaPre21"), e);
+        } catch (ReflectiveOperationException e) {
+            // Should never happen
+            log.error(sm.getString("jre21Compat.unexpected"), e);
+        }
+        nameMethod = m1;
+        startMethod = m2;
+        ofVirtualMethod = m3;
+    }
+
+    static boolean isSupported() {
+        return ofVirtualMethod != null;
+    }
+
+    @Override
+    public Object createVirtualThreadBuilder(String name) {
+        try {
+            Object threadBuilder = ofVirtualMethod.invoke(null, (Object[]) 
null);
+            nameMethod.invoke(threadBuilder, name, Long.valueOf(0));
+            return threadBuilder;
+        } catch (IllegalAccessException | IllegalArgumentException | 
InvocationTargetException e) {
+            throw new UnsupportedOperationException(e);
+        }
+    }
+
+    @Override
+    public void threadBuilderStart(Object threadBuilder, Runnable command) {
+        try {
+            startMethod.invoke(threadBuilder, command);
+        } catch (IllegalAccessException | IllegalArgumentException | 
InvocationTargetException e) {
+            throw new UnsupportedOperationException(e);
+        }
+    }
+}
diff --git a/java/org/apache/tomcat/util/compat/Jre22Compat.java 
b/java/org/apache/tomcat/util/compat/Jre22Compat.java
index 6e5416694c..d685cd56ec 100644
--- a/java/org/apache/tomcat/util/compat/Jre22Compat.java
+++ b/java/org/apache/tomcat/util/compat/Jre22Compat.java
@@ -22,7 +22,7 @@ import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.util.res.StringManager;
 
-public class Jre22Compat extends JreCompat {
+public class Jre22Compat extends Jre21Compat {
 
     private static final Log log = LogFactory.getLog(Jre22Compat.class);
     private static final StringManager sm = 
StringManager.getManager(Jre22Compat.class);
diff --git a/java/org/apache/tomcat/util/compat/JreCompat.java 
b/java/org/apache/tomcat/util/compat/JreCompat.java
index 7273b0b233..743f76e64f 100644
--- a/java/org/apache/tomcat/util/compat/JreCompat.java
+++ b/java/org/apache/tomcat/util/compat/JreCompat.java
@@ -16,16 +16,23 @@
  */
 package org.apache.tomcat.util.compat;
 
+import java.lang.reflect.Field;
+
+import org.apache.tomcat.util.res.StringManager;
+
 /**
  * This is the base implementation class for JRE compatibility and provides an
- * implementation based on Java 21. Sub-classes may extend this class and 
provide
+ * implementation based on Java 17. Sub-classes may extend this class and 
provide
  * alternative implementations for later JRE versions
  */
 public class JreCompat {
 
     private static final JreCompat instance;
     private static final boolean graalAvailable;
+    private static final boolean jre19Available;
+    private static final boolean jre21Available;
     private static final boolean jre22Available;
+    private static final StringManager sm = 
StringManager.getManager(JreCompat.class);
 
     static {
         boolean result = false;
@@ -39,13 +46,28 @@ public class JreCompat {
         }
         graalAvailable = result || 
System.getProperty("org.graalvm.nativeimage.imagecode") != null;
 
-        // This is Tomcat 11.0.x with a minimum Java version of Java 21.
+        // This is Tomcat 11.0.x with a minimum Java version of Java 17.
+        // Look for the highest supported JVM first
         if (Jre22Compat.isSupported()) {
             instance = new Jre22Compat();
             jre22Available = true;
+            jre21Available = true;
+            jre19Available = true;
+        } else if (Jre21Compat.isSupported()) {
+            instance = new Jre21Compat();
+            jre22Available = false;
+            jre21Available = true;
+            jre19Available = true;
+        } else if (Jre19Compat.isSupported()) {
+            instance = new Jre19Compat();
+            jre22Available = false;
+            jre21Available = false;
+            jre19Available = true;
         } else {
             instance = new JreCompat();
             jre22Available = false;
+            jre21Available = false;
+            jre19Available = false;
         }
     }
 
@@ -60,9 +82,101 @@ public class JreCompat {
     }
 
 
+    public static boolean isJre19Available() {
+        return jre19Available;
+    }
+
+
+    public static boolean isJre21Available() {
+        return jre21Available;
+    }
+
+
     public static boolean isJre22Available() {
         return jre22Available;
     }
 
 
+    // Java 17 implementations of Java 19 methods
+
+    /**
+     * Obtains the executor, if any, used to create the provided thread.
+     *
+     * @param thread    The thread to examine
+     *
+     * @return  The executor, if any, that created the provided thread
+     *
+     * @throws NoSuchFieldException
+     *              If a field used via reflection to obtain the executor 
cannot
+     *              be found
+     * @throws SecurityException
+     *              If a security exception occurs while trying to identify the
+     *              executor
+     * @throws IllegalArgumentException
+     *              If the instance object does not match the class of the 
field
+     *              when obtaining a field value via reflection
+     * @throws IllegalAccessException
+     *              If a field is not accessible due to access restrictions
+     */
+    public Object getExecutor(Thread thread)
+            throws NoSuchFieldException, SecurityException, 
IllegalArgumentException, IllegalAccessException {
+
+        Object result = null;
+
+        // Runnable wrapped by Thread
+        // "target" in Sun/Oracle JDK
+        // "runnable" in IBM JDK
+        // "action" in Apache Harmony
+        Object target = null;
+        for (String fieldName : new String[] { "target", "runnable", "action" 
}) {
+            try {
+                Field targetField = 
thread.getClass().getDeclaredField(fieldName);
+                targetField.setAccessible(true);
+                target = targetField.get(thread);
+                break;
+            } catch (NoSuchFieldException nfe) {
+                continue;
+            }
+        }
+
+        // "java.util.concurrent" code is in public domain,
+        // so all implementations are similar including our
+        // internal fork.
+        if (target != null && target.getClass().getCanonicalName() != null &&
+                (target.getClass().getCanonicalName().equals(
+                        
"org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") ||
+                        target.getClass().getCanonicalName().equals(
+                                
"java.util.concurrent.ThreadPoolExecutor.Worker"))) {
+            Field executorField = target.getClass().getDeclaredField("this$0");
+            executorField.setAccessible(true);
+            result = executorField.get(target);
+        }
+
+        return result;
+    }
+
+
+    // Java 17 implementations of Java 21 methods
+
+    /**
+     * Create a thread builder for virtual threads using the given name to 
name the threads.
+     *
+     * @param name The base name for the threads
+     *
+     * @return The thread buidler for virtual threads
+     */
+    public Object createVirtualThreadBuilder(String name) {
+        throw new 
UnsupportedOperationException(sm.getString("jreCompat.noVirtualThreads"));
+    }
+
+
+    /**
+     * Create a thread with the given thread builder and use it to execute the 
given runnable.
+     *
+     * @param threadBuilder The thread builder to use to create a thread
+     * @param command       The command to run
+     */
+    public void threadBuilderStart(Object threadBuilder, Runnable command) {
+        throw new 
UnsupportedOperationException(sm.getString("jreCompat.noVirtualThreads"));
+    }
 }
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java 
b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index fb6080eaeb..4c9942a3b4 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -1210,7 +1210,6 @@ public abstract class AbstractEndpoint<S,U> {
             this.executor = null;
             if (executor instanceof ThreadPoolExecutor) {
                 //this is our internal one, so we need to shut it down
-                @SuppressWarnings("resource")
                 ThreadPoolExecutor tpe = (ThreadPoolExecutor) executor;
                 tpe.shutdownNow();
                 long timeout = getExecutorTerminationTimeoutMillis();
diff --git a/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java 
b/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java
index 461d16e05f..54453a9a66 100644
--- a/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java
+++ b/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java
@@ -23,6 +23,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.tomcat.util.compat.JreCompat;
 import org.apache.tomcat.util.res.StringManager;
 
 /**
@@ -34,10 +35,12 @@ public class VirtualThreadExecutor extends 
AbstractExecutorService {
 
     private CountDownLatch shutdown = new CountDownLatch(1);
 
-    private Thread.Builder threadBuilder;
+    private final JreCompat jreCompat = JreCompat.getInstance();
+
+    private Object threadBuilder;
 
     public VirtualThreadExecutor(String namePrefix) {
-        threadBuilder = Thread.ofVirtual().name(namePrefix, 0);
+        threadBuilder = jreCompat.createVirtualThreadBuilder(namePrefix);
     }
 
     @Override
@@ -46,7 +49,7 @@ public class VirtualThreadExecutor extends 
AbstractExecutorService {
             throw new RejectedExecutionException(
                     sm.getString("virtualThreadExecutor.taskRejected", 
command.toString(), this.toString()));
         }
-        threadBuilder.start(command);
+        jreCompat.threadBuilderStart(threadBuilder, command);
     }
 
     @Override
@@ -92,4 +95,4 @@ public class VirtualThreadExecutor extends 
AbstractExecutorService {
     public boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
         return shutdown.await(timeout, unit);
     }
-}
\ No newline at end of file
+}
diff --git 
a/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java 
b/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java
index 23b6d6518a..dfdc33b9d0 100644
--- a/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java
+++ b/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java
@@ -38,6 +38,7 @@ import org.junit.Test;
 
 import org.apache.catalina.Context;
 import org.apache.catalina.Wrapper;
+import org.apache.catalina.startup.SimpleHttpClient;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
 
@@ -79,11 +80,9 @@ public class TestAsyncContextImplListenerOnComplete extends 
TomcatBaseTest {
             socket.connect(new InetSocketAddress("localhost", port));
 
             try (var writer = new 
OutputStreamWriter(socket.getOutputStream())) {
-                writer.write("""
-                    GET /repro
-                    Accept: text/event-stream
-
-                    """);
+                writer.write("GET /repro" + SimpleHttpClient.CRLF +
+                        "Accept: text/event-stream" + SimpleHttpClient.CRLF +
+                        SimpleHttpClient.CRLF);
                 writer.flush();
             }
             Thread.sleep(1_000);
@@ -146,18 +145,18 @@ public class TestAsyncContextImplListenerOnComplete 
extends TomcatBaseTest {
         private final Set<AsyncContext> contexts = new HashSet<>();
 
         private EventSource() {
-            Thread.ofVirtual()
-                .name(getClass().getSimpleName())
-                .start(() -> {
-                    while (true) {
-                        try {
-                            Thread.sleep(2000);
-                            send("PING");
-                        } catch (InterruptedException e) {
-                            System.out.println("Failed to sleep: " + e);
-                        }
+            Runnable r = () -> {
+                while (true) {
+                    try {
+                        Thread.sleep(2000);
+                        send("PING");
+                    } catch (InterruptedException e) {
+                        System.out.println("Failed to sleep: " + e);
                     }
-                });
+                }
+            };
+            Thread t = new Thread(r);
+            t.start();
         }
 
         public void send(String message) {
diff --git a/test/org/apache/juli/TestThreadNameCache.java 
b/test/org/apache/juli/TestThreadNameCache.java
index e67ea01f14..65c7a6e6b2 100644
--- a/test/org/apache/juli/TestThreadNameCache.java
+++ b/test/org/apache/juli/TestThreadNameCache.java
@@ -38,7 +38,7 @@ public class TestThreadNameCache {
             @Override
             public void run() {
                 setName(THREAD_NAME);
-                threadId = Integer.valueOf((int) threadId());
+                threadId = Integer.valueOf((int) getId());
                 threadIdLatch.countDown();
                 try {
                     cacheLatch.await();
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 32de8976c1..5f3a32e564 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -105,6 +105,13 @@
   issues do not "pop up" wrt. others).
 -->
 <section name="Tomcat 11.0.0-M18 (markt)" rtext="in development">
+  <subsection name="General">
+    <changelog>
+      <update>
+        Reduce the minimum supported Java version to Java 17. (markt)
+      </update>
+    </changelog>
+  </subsection>
   <subsection name="Jasper">
     <changelog>
       <add>
@@ -114,6 +121,11 @@
         for the Jakarta EL 6.0 API. The property support has also been refined
         for greater consistency. (markt)
       </add>
+      <update>
+        The defaults for <code>compilerSourceVM</code> and
+        <code>compilerTargetVM</code> have been updated to 17 to align with 
Java
+        17 being the minimum Java version required for Tomcat 11. (markt)
+      </update>
     </changelog>
   </subsection>
 </section>
diff --git a/webapps/docs/jasper-howto.xml b/webapps/docs/jasper-howto.xml
index 31b49ce50a..ac73faafea 100644
--- a/webapps/docs/jasper-howto.xml
+++ b/webapps/docs/jasper-howto.xml
@@ -106,10 +106,10 @@ set then <code>setenv.[sh|bat]</code> should be used to 
add
 to the <code>CLASSPATH</code> environment variable.</li>
 
 <li><strong>compilerSourceVM</strong> - What JDK version are the source files
-compatible with? (Default value: <code>21</code>)</li>
+compatible with? (Default value: <code>17</code>)</li>
 
 <li><strong>compilerTargetVM</strong> - What JDK version are the generated 
files
-compatible with? (Default value: <code>21</code>)</li>
+compatible with? (Default value: <code>17</code>)</li>
 
 <li><strong>development</strong> - Is Jasper used in development mode? If true,
 the frequency at which JSPs are checked for modification may be specified via
diff --git a/webapps/docs/tomcat-docs.xsl b/webapps/docs/tomcat-docs.xsl
index d152384e45..54abf79897 100644
--- a/webapps/docs/tomcat-docs.xsl
+++ b/webapps/docs/tomcat-docs.xsl
@@ -37,8 +37,8 @@
   <xsl:param    name="version"             select="'11.0.x'"/>
   <xsl:param    name="majorversion"        select="'11'"/>
   <xsl:param    name="majorminorversion"   select="'11.0'"/>
-  <xsl:param    name="minjavaversion"      select="'21'"/>
-  <xsl:param    name="buildjavaversion"    select="'21'"/>
+  <xsl:param    name="minjavaversion"      select="'17'"/>
+  <xsl:param    name="buildjavaversion"    select="'22'"/>
   <xsl:param    name="antversionrequired"  select="'1.10.2'"/>
   <xsl:param    name="build-date"          select="'MMM d yyyy'"/>
   <xsl:param    name="build-date-iso-8601" select="'yyyy-MM-dd'"/>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org


Reply via email to