You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2023/12/09 14:42:54 UTC

(freemarker) branch 3 updated (931dbfdb -> 31ecce76)

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

ddekany pushed a change to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git


    from 931dbfdb (org.apache.freemarker, not org.freemarker...)
     new 8b0259c3 Switched to Java 17 as the minimum Java version. Forward ported changes from 2.3-gae that was done there to allow tests running with Java 16. Forward ported FreemarkerServlet debug error page fix too.
     new 90a82201 (README is not maintained for this branch yet, so simplified it for now.)
     new a1ad5b72 Deleted Travis file (should switch to Git Actions later)
     new 31ecce76 Updated change log

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .travis.yml                                        |  25 ---
 FM3-CHANGE-LOG.txt                                 |   4 +-
 README.md                                          | 243 +--------------------
 .../build/module/common/FreemarkerJavaPlugin.kt    |   2 +-
 .../org/apache/freemarker/core/DateFormatTest.java |  31 ++-
 .../model/impl/RestrictedObjectWrapperTest.java    |  42 ++--
 .../core/valueformat/NumberFormatTest.java         |   2 +-
 .../core/templatesuite/expected/number-format.txt  |   4 +-
 .../core/model/impl/ClassIntrospector.java         |  53 ++---
 .../core/{_Java8.java => util/_Java21.java}        |  15 +-
 .../apache/freemarker/core/util/_Java21Impl.java   |   9 +-
 .../apache/freemarker/core/util/_JavaVersions.java |  51 +----
 .../java/org/apache/freemarker/dom/NodeModel.java  |  53 +----
 .../dom/SunInternalXalanXPathSupport.java          | 160 --------------
 .../dom/DefaultObjectWrapperExtensionTest.java     |  16 +-
 freemarker-servlet/build.gradle.kts                |  28 ++-
 .../freemarker/servlet/FreemarkerServlet.java      |  83 ++++---
 .../servlet/DummyMockServletContext.java           |  20 +-
 .../freemarker/servlet/test/WebAppTestCase.java    |  51 +++--
 .../src/main/resources/logback-test.xml            |   3 +-
 osgi.bnd                                           |   2 +-
 settings.gradle.kts                                |   2 -
 22 files changed, 188 insertions(+), 711 deletions(-)
 delete mode 100644 .travis.yml
 copy freemarker-core/src/main/java/org/apache/freemarker/core/{_Java8.java => util/_Java21.java} (74%)
 copy freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PublicAll.java => freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21Impl.java (85%)
 delete mode 100644 freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java


(freemarker) 03/04: Deleted Travis file (should switch to Git Actions later)

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit a1ad5b729c2c7d90d91b7a29af4c9f0d295fb311
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Dec 9 15:33:08 2023 +0100

    Deleted Travis file (should switch to Git Actions later)
---
 .travis.yml | 25 -------------------------
 1 file changed, 25 deletions(-)

diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index c00418ff..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# 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.
-
-language: java
-#before_install:
-#  - sudo apt-get -qq update
-#  - sudo apt-get install -y openjdk-8-jre
-jdk:
-- openjdk8
-install:
-- ./gradlew build -xmanualOnline -PsignPublication=false


(freemarker) 04/04: Updated change log

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 31ecce7654bd348481e00578c8435c27494c8788
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Dec 9 15:36:10 2023 +0100

    Updated change log
---
 FM3-CHANGE-LOG.txt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt
index 922b1110..25d66696 100644
--- a/FM3-CHANGE-LOG.txt
+++ b/FM3-CHANGE-LOG.txt
@@ -573,10 +573,10 @@ models by default like FreemarkerServlet does.
 Core / Miscellaneous
 ....................
 
-- Minimum Java version increased to 8
+- Minimum Java version increased to 17
 - Removed support for incompatibleImprovements before 3.0.0. So currently 3.0.0 is the only support value.
 - Removed legacy extensions (so these have no module): rhino, jython, xml (not to be confused with dom), jdom, ant.
-- Servlet 3.0 and JSP 2.2 and is the minimum requirement now (if Serlvet/JSP features are used at all).
+- Servlet 3.1 and JSP 2.3 and is the minimum requirement now (if Serlvet/JSP features are used at all).
 - Log categories are now simply the full qualified class name of the logging classes. There are no predefined log
   categories (such as "freemarker.runtime", etc.) anymore. This is to follow the common practices in logging.
 - #include-d/#import-ed templates don't inherit the charset (encoding) of the #include-ing/#import-ing template.


(freemarker) 02/04: (README is not maintained for this branch yet, so simplified it for now.)

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 90a82201d93703328a73c10eefb62129c5d49089
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Dec 9 15:31:52 2023 +0100

    (README is not maintained for this branch yet, so simplified it for now.)
---
 README.md | 243 ++------------------------------------------------------------
 1 file changed, 4 insertions(+), 239 deletions(-)

diff --git a/README.md b/README.md
index 0ac3547b..7533d408 100644
--- a/README.md
+++ b/README.md
@@ -1,241 +1,6 @@
-Apache FreeMarker {version}
-===========================
+Apache FreeMarker 3
+===================
 
-[![Build Status](https://travis-ci.org/apache/freemarker.svg?branch=3)](https://travis-ci.org/apache/freemarker)
+This is a placeholder README for the "experimental" FreeMarker 3 branch.
 
-For the latest version or to report bugs visit:
-https://freemarker.apache.org/
-
-
-Regarding pull requests on Github
----------------------------------
-
-By sending a pull request you grant the Apache Software Foundation
-sufficient rights to use and release the submitted work under the
-Apache license. You grant the same rights (copyright license, patent
-license, etc.) to the Apache Software Foundation as if you have signed
-a [Contributor License Agreement](https://www.apache.org/dev/new-committers-guide.html#cla).
-For contributions that are judged to be non-trivial, you will be asked
-to actually signing a Contributor License Agreement.
-
-
-What is Apache FreeMarker?
---------------------------
-
-FreeMarker is a "template engine"; a generic tool to generate text
-output (anything from HTML to auto generated source code) based on
-templates. It's a Java package, a class library for Java programmers.
-It's not an application for end-users in itself, but something that
-programmers can embed into their products. FreeMarker is designed to
-be practical for the generation of HTML Web pages, particularly by
-servlet-based applications following the MVC (Model View Controller)
-pattern.
-
-
-Licensing
----------
-
-FreeMarker is licensed under the Apache License, Version 2.0.
-
-See the LICENSE file for more details!
-
-
-Documentation
--------------
-
-Online: https://freemarker.apache.org/docs/
-
-Offline: The full documentation is available in the binary distribution
-in the documentation/index.html directory.
-
-
-Installing
-----------
-
-If you are using Maven, just add this dependency:
-
-```xml
-  <dependency>
-    <groupId>org.apache.freemarker</groupId>
-    <artifactId>freemarker-core</artifactId>
-    <version>{version}</version>
-  </dependency>
-```
-
-Otherwise simply copy freemarker-core-<version>.jar to a location where your
-Java application's ClassLoader will find it. For example, if you are using
-FreeMarker in a web application, you probably want to put the jar into the
-WEB-INF/lib directory of your web application.
-
-FreeMarker 3 has only one required dependency, `org.slf4j:slf4j-api`. (Of 
-course, it will be automatically downloaded by Maven, Gradle, and the like, 
-and is already there in almost all projects anyway. If it wasn't there, note 
-that adding slf4j-api is not enough, as it needs an implementation, which is 
-not downloaded automatically by Maven, Gradle, etc. The most popular is 
-`ch.qos.logback:logback-classic`. FreeMarker has several optional dependencies,
-but usually you don't have to deal with them, because if you are using an
-optional feature that's certainly because your application already uses the
-related library.
-
-The minimum required Java version is currently Java SE 7. (The presence
-of a later version may be detected on runtime and utilized by
-FreeMarker.)
-
-
-Change log
-----------
-
-Online (for stable releases only):
-http://freemarker.org/docs/app_versions.html
-
-Offline:
-In the binary release, open documentation/index.html, and you will find the
-link.
-
-
-Building FreeMarker
--------------------
-
-If you haven't yet, download the source release, or checkout FreeMarker from
-the source code repository. See repository locations here:
-https://freemarker.apache.org/sourcecode.html
-
-You need JDK 8 to be installed.
-
-You must copy `gradle.properties.sample` into `gradle.properties`, and edit
-its content to fit your system.
-
-To build the jar-s of all modules (freemarker-core, freemarker-servlet, etc.),
-issue `./gradlew jar` in the project root directory (Windows users see the note
-below though). It will automatically download all dependencies on first run too
-(including the proper version of Gradle itself). The built jar-s will be in the
-build/libs subdirectory of each module (freemarker-core, freemarker-servlet,
-etc.). You can also install the jar-s into your local Maven repository with
-`./gradlew install`.
-
-Note for Windows users: If you are using an Apache source release (as opposed
-to checking the project out from the Git repository), `./gradlew` will fail as
-`gradle\wrapper\gradle-wrapper.jar` is missing. Due to Apache policy we can't
-include that file in distributions, so you have to download that very common
-artifact from somewhere manually (like from the Git repository of FreeMarker).
-(On UN*X-like systems you don't need that jar, as our custom `gradlew` shell
-script does everything itself.)
-
-To test your build, issue `./gradlew test`. Issued from the top directory,
-this will run the tests of all modules.
-
-To generate the aggregated API documentation (contains the API of several modules
-that are deemed to be used commonly enough), issue `./gradlew aggregateJavadoc`
-from the root module; the output will appear in the `build/docs/javadoc`
-subdirectory. To generate API documentation per module, issue
-`./gradlew javadoc`; the output will appear in the `build/docs/javadoc`
-subdirectory of the module.
-
-To generate the FreeMarker Manual, issue `./gradlew manualOffline`; the output
-will appear under `freemarker-manual/build/docgen`.
-
-
-IDE setup
----------
-
-### Eclipse
-
-Last tested Eclipse Oxygen (4.7.0)
-
-- First, build the project with Gradle if you haven't yet (see earlier how).
-- Start Eclipse
-- Start a new workspace (File -> "Switch workspace")
-- Window -> Preferences
-  - General -> Workspace, set the text file encoding
-    to "UTF-8". (Or, you can set the same later on project level instead.)
-  - General -> Editors -> Text Editors, set:
-    - Insert space for tabs
-    - Show print margin, 120 columns
-  - Java -> Code Style -> Formatter -> Import...
-    Select `src\ide-settings\Eclipse\Formatter-profile-FreeMarker.xml`
-    inside the FreeMarker project directory. That select "FreeMarker" as the
-    "Active profile".
-    (This profile uses space-only indentation policy and 120 character line
-    width, and formatting rules that are pretty much standard in modern Java.)
-  - Java -> Code Style -> Organize imports
-    Order: java, javax, org, com.
-    Number of imports required for .*: 99
-    Number of static imports needed for .*: 1
-  - Java -> Installed JRE-s:
-    Ensure that you have JDK 8 installed, and that it was added to Eclipse.
-    Note that it's not JRE, but JDK.
-  - Java -> Compiler -> Javadoc:
-    "Malformed Javadoc comments": Error
-    "Only consider members as visible": Private
-    "Validate tag arguments": true
-    "Missing tag descriptions": Validate @return tags
-    "Missing Javadoc tags": Ignore
-    "Missing Javadoc comments": Ignore
-  - Java -> Compiler -> Errors/Warnings:
-    Set "Forbidden reference (access rules)" from "Error" to "Warning".
-  - Java -> Editor -> Save Actions:
-    Check "Perform the selected actions on save", and under that have only
-    "Organize imports" and "Additional actions" checked. The list for the
-    last should contain
-    "Add missing @Override annotations",
-    "Add missing @Override annotations to implementations of interface methods",
-    "Add missing @Deprecated annotations", and
-    "Remove unnecessary cast".
-- File -> Import... -> Gradle / Existing Gradle Project
-  Import the FreeMarker project directory. Everything can remain at its default.
-- Now Eclipse will automatically build the broject in the backround. There shouldn't
-  be any errors.
-- You may right-click on a all the project in the project tree, and chose
-  "Run as..." -> "JUnit test". Not all projects contains tests though (notably,
-  freemarker-core tests are in separate projects).
-- To run all the tests at once, you must use Gradle. To do that from under Eclipse,
-  go to "Run" -> "Run Configurations...", then right click on "Gradle Project" in the
-  tree, then "New". The "Gradle task" (and the "Name") should be `test`, and the
-  "Working directory" `${workspace_loc:/freemarker}`.
-- It's highly recommended to use the Eclipse FindBugs plugin.
-  - Install it from Eclipse Marketplace (3.0.2 as of this writing)
-  - Window -> Preferences -> Java -> FindBugs:
-    Set all bug marker ranks from Warning to Error. (For false alarms we add
-    @SuppressFBWarnings(value = "...", justification = "...") annotations.)
-  - Fro each project: Project -> Properties -> FindBugs -> [x] Run Automatically
-  - There should 0 errors. But sometimes the plugin fails to take the
-    @SuppressFBWarnings annotations into account; then use Project -> Clean. 
-- Setting type filters (not required, but convenient): "Window" -> "Preferences" ->
-  "Java" -> "Appearance" -> "Type filter", "Add..." these:
-  `javax.swing.*`, `freemarker.*`, `com.sun.*`
-
-### IntelliJ IDEA
-
-Last tested on IntelliJ IDEA Community 2017.1.5.
-
-- First, build the project with Gradle if you haven't yet (see earlier how).
-- "New..." -> "Project from existing source" (or if you start on the Welcome screen: "Import project")
-  - Point to the project root directory
-  - On the next screen, select "Import project from external model" and "Gradle"
-  - On the next screen, ensure that at least Java 8 is selected for Gradle. Other defaults
-    ("Use default gradle wrapper", etc.) should be fine.
-  - On the next window (if there's one), all modules will be selected, that's fine, go on
-  - On the next window (if there's one), it will prompt to remove `freemarker` from the project.
-    Let it do it (as it's an incorrect duplication of the `freemarker` root project).
-  - At the end of this process you should have all modules in the Project tree view.
-- "File" -> "Settings"
-  - Under "Build, Execution, Deployment" / "Build Tools" / "Gradle" find the "Build and test using", and
-    "Run tests using" dropdowns, and set all of them to "Gradle".
-  - Under "Editor" / "Code style", import and use
-    freemarker/src/ide-settings/IntelliJ-IDEA/Java-code-style-FreeMarker.xml
-  - Under "Editor" / "Inspections", import and use
-    freemarker/src/ide-settings/IntelliJ-IDEA/Editor-Inspections-FreeMarker.xml
-  - Copy the copyright header comment from some of the java files, then
-    under "Editor" / "Copyright" / "Copyright Profiles" click "+", enter "ASL2" as name,
-    then paste the copyright header. Delete the `/*` and ` */` lines, and the ` *`
-    prefixes (to select columns of text, hold Alt while selecting with the mouse.) Then
-    go back to "Copyright" in the tree, and set "Default project copyright" to "ASL2".
-- Testing your setup:
-  - You may do "Build"/"Build project" (Ctrl+F9) to see if everything compiles now.
-  - You may run the `test` task of the root project with Gradle to see that everything works as
-    expected. To do that from IntelliJ, create a run configuration:
-    "Run" \ "Edit Configurations..." \ "+" \ "Gradle" then:
-    - Set "Name" to `All tests` for example
-    - Set "Gradle project" to the root project (`freemarker`)
-    - Set "Tasks" to `test`
-- TODO Setting up the FindBugs plugin
+See more here: https://cwiki.apache.org/confluence/display/FREEMARKER/FreeMarker+3


(freemarker) 01/04: Switched to Java 17 as the minimum Java version. Forward ported changes from 2.3-gae that was done there to allow tests running with Java 16. Forward ported FreemarkerServlet debug error page fix too.

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 8b0259c31cad672e227db14dd4bcc47f2d238845
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Dec 9 15:30:23 2023 +0100

    Switched to Java 17 as the minimum Java version. Forward ported changes from 2.3-gae that was done there to allow tests running with Java 16. Forward ported FreemarkerServlet debug error page fix too.
---
 .../build/module/common/FreemarkerJavaPlugin.kt    |   2 +-
 .../org/apache/freemarker/core/DateFormatTest.java |  31 ++--
 .../model/impl/RestrictedObjectWrapperTest.java    |  42 ++----
 .../core/valueformat/NumberFormatTest.java         |   2 +-
 .../core/templatesuite/expected/number-format.txt  |   4 +-
 .../core/model/impl/ClassIntrospector.java         |  53 ++-----
 .../org/apache/freemarker/core/util/_Java21.java   |  28 ++--
 .../apache/freemarker/core/util/_Java21Impl.java   |  26 ++--
 .../apache/freemarker/core/util/_JavaVersions.java |  51 ++-----
 .../java/org/apache/freemarker/dom/NodeModel.java  |  53 +------
 .../dom/SunInternalXalanXPathSupport.java          | 160 ---------------------
 .../dom/DefaultObjectWrapperExtensionTest.java     |  16 ++-
 freemarker-servlet/build.gradle.kts                |  28 ++--
 .../freemarker/servlet/FreemarkerServlet.java      |  83 ++++++-----
 .../servlet/DummyMockServletContext.java           |  20 ++-
 .../freemarker/servlet/test/WebAppTestCase.java    |  51 +++++--
 .../src/main/resources/logback-test.xml            |   3 +-
 osgi.bnd                                           |   2 +-
 settings.gradle.kts                                |   2 -
 19 files changed, 191 insertions(+), 466 deletions(-)

diff --git a/buildSrc/module-common/src/main/kotlin/org/apache/freemarker/build/module/common/FreemarkerJavaPlugin.kt b/buildSrc/module-common/src/main/kotlin/org/apache/freemarker/build/module/common/FreemarkerJavaPlugin.kt
index 1d44d5f2..e71c8fb3 100644
--- a/buildSrc/module-common/src/main/kotlin/org/apache/freemarker/build/module/common/FreemarkerJavaPlugin.kt
+++ b/buildSrc/module-common/src/main/kotlin/org/apache/freemarker/build/module/common/FreemarkerJavaPlugin.kt
@@ -34,7 +34,7 @@ import org.gradle.kotlin.dsl.withType
 fun getDefaultJavaVersion(project: Project): JavaLanguageVersion {
     return project.providers
         .gradleProperty("freemarkerDefaultJavaVersion")
-        .getOrElse("8")
+        .getOrElse("17")
         .let { JavaLanguageVersion.of(it) }
 }
 
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java
index ae9fc027..2c7eab9f 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java
@@ -18,26 +18,12 @@
  */
 package org.apache.freemarker.core;
 
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.*;
-
-import java.io.IOException;
-import java.sql.Time;
-import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
+import com.google.common.collect.ImmutableMap;
 import org.apache.freemarker.core.model.TemplateDateModel;
 import org.apache.freemarker.core.model.impl.SimpleDate;
 import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
 import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
-import org.apache.freemarker.core.userpkg.AppMetaTemplateDateFormatFactory;
-import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactory;
-import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory;
-import org.apache.freemarker.core.userpkg.HTMLISOTemplateDateFormatFactory;
-import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.*;
 import org.apache.freemarker.core.valueformat.TemplateDateFormat;
 import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
 import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException;
@@ -45,7 +31,16 @@ import org.apache.freemarker.core.valueformat.impl.AliasTemplateDateFormatFactor
 import org.apache.freemarker.test.TemplateTest;
 import org.junit.Test;
 
-import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.*;
 
 public class DateFormatTest extends TemplateTest {
     
@@ -372,7 +367,7 @@ public class DateFormatTest extends TemplateTest {
                 + "<#setting locale='en_GB_Win'>${d} "
                 + "<#setting locale='fr_FR'>${d} "
                 + "<#setting locale='hu_HU'>${d}",
-                "2015-Sep_en 2015-Sep_en_GB 2015-Sep_en_GB 2015-sept._fr_FR 2015-szept.");
+                "2015-Sep_en 2015-Sept_en_GB 2015-Sept_en_GB 2015-sept._fr_FR 2015-szept.");
     }
     
     /**
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java
index 6247fba9..b7ae632b 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java
@@ -19,47 +19,31 @@
 
 package org.apache.freemarker.core.model.impl;
 
-import static org.apache.freemarker.test.hamcerst.Matchers.*;
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.*;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-
-import javax.annotation.PostConstruct;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.TemplateException;
-import org.apache.freemarker.core.model.ObjectWrappingException;
-import org.apache.freemarker.core.model.TemplateBooleanModel;
-import org.apache.freemarker.core.model.TemplateCollectionModel;
-import org.apache.freemarker.core.model.TemplateDateModel;
-import org.apache.freemarker.core.model.TemplateHashModelEx;
-import org.apache.freemarker.core.model.TemplateIterableModel;
-import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
-import org.apache.freemarker.core.model.TemplateNumberModel;
-import org.apache.freemarker.core.model.TemplateSequenceModel;
-import org.apache.freemarker.core.model.TemplateStringModel;
+import org.apache.freemarker.core.model.*;
 import org.apache.freemarker.core.model.impl.DefaultObjectWrapperTest.TestBean;
 import org.junit.Test;
 import org.w3c.dom.Document;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.*;
+
+import static org.apache.freemarker.test.hamcerst.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.*;
+
 public class RestrictedObjectWrapperTest {
 
     @Test
     public void testBasics() throws TemplateException {
-        PostConstruct.class.toString();
         RestrictedObjectWrapper ow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
         testCustomizationCommonPart(ow);
         assertTrue(ow.wrap(Collections.emptyMap()) instanceof DefaultMapAdapter);
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java
index 167f4275..08856ba2 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java
@@ -372,7 +372,7 @@ public class NumberFormatTest extends TemplateTest {
         addToDataModel("nInf", Double.NEGATIVE_INFINITY);
         addToDataModel("nan", Double.NaN);
 
-        String humanAudienceOutput = "\u221e -\u221e \ufffd";
+        String humanAudienceOutput = "\u221e -\u221e NaN";
         String computerAudienceOutput = "Infinity -Infinity NaN";
 
         assertOutput("${pInf?c} ${nInf?c} ${nan?c}", computerAudienceOutput);
diff --git a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
index 7929be58..82420029 100644
--- a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
+++ b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
@@ -18,7 +18,7 @@
  */
 1
 1
-1 234 567,89
+1 234 567,89
 1234567.886
 1,00
 1
@@ -32,4 +32,4 @@
 1
 1E-16
 100000.5
-100 000,5
+100 000,5
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
index 330caed2..20571f2e 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -19,12 +19,16 @@
 
 package org.apache.freemarker.core.model.impl;
 
-import java.beans.BeanInfo;
-import java.beans.IndexedPropertyDescriptor;
-import java.beans.IntrospectionException;
-import java.beans.Introspector;
-import java.beans.MethodDescriptor;
-import java.beans.PropertyDescriptor;
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreAPI;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.CommonBuilder;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.beans.*;
 import java.lang.ref.Reference;
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.WeakReference;
@@ -32,31 +36,10 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.Map.Entry;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-import org.apache.freemarker.core.Configuration;
-import org.apache.freemarker.core.Version;
-import org.apache.freemarker.core._CoreAPI;
-import org.apache.freemarker.core.util.BugException;
-import org.apache.freemarker.core.util.CommonBuilder;
-import org.apache.freemarker.core.util._JavaVersions;
-import org.apache.freemarker.core.util._NullArgumentException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 /**
  * Returns information about a {@link Class} that's useful for FreeMarker. Encapsulates a cache for this. Thread-safe,
  * doesn't even require "proper publishing" starting from 2.3.24 or Java 5. Immutable, with the exception of the
@@ -394,11 +377,6 @@ class ClassIntrospector {
         List<PropertyDescriptor> introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray)
                 : Collections.emptyList();
 
-        if (_JavaVersions.JAVA_8 == null) {
-            // java.beans.Introspector was good enough then.
-            return introspectorPDs;
-        }
-
         // introspectorPDs contains each property exactly once. But as now we will search them manually too, it can
         // happen that we find the same property for multiple times. Worse, because of indexed properties, it's possible
         // that we have to merge entries (like one has the normal reader method, the other has the indexed reader
@@ -415,7 +393,7 @@ class ClassIntrospector {
         // (Note that java.beans.Introspector discovers non-accessible public methods, and to emulate that behavior
         // here, we don't utilize the accessibleMethods Map, which we might already have at this point.)
         for (Method method : clazz.getMethods()) {
-            if (_JavaVersions.JAVA_8.isDefaultMethod(method) && method.getReturnType() != void.class
+            if (method.isDefault() && method.getReturnType() != void.class
                     && !method.isBridge()) {
                 Class<?>[] paramTypes = method.getParameterTypes();
                 if (paramTypes.length == 0
@@ -588,14 +566,9 @@ class ClassIntrospector {
         List<MethodDescriptor> introspectionMDs = introspectorMDArray != null && introspectorMDArray.length != 0
                 ? Arrays.asList(introspectorMDArray) : Collections.emptyList();
 
-        if (_JavaVersions.JAVA_8 == null) {
-            // java.beans.Introspector was good enough then.
-            return introspectionMDs;
-        }
-
         Map<String, List<Method>> defaultMethodsToAddByName = null;
         for (Method method : clazz.getMethods()) {
-            if (_JavaVersions.JAVA_8.isDefaultMethod(method) && !method.isBridge()) {
+            if (method.isDefault() && !method.isBridge()) {
                 if (defaultMethodsToAddByName == null) {
                     defaultMethodsToAddByName = new HashMap<>();
                 }
diff --git a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21.java
similarity index 69%
copy from freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
copy to freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21.java
index 7929be58..2bb6f913 100644
--- a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21.java
@@ -16,20 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-1
-1
-1 234 567,89
-1234567.886
-1,00
-1
-1234567,89
-1234567.886
-1
-1
-1.000000000000001
-1E-16
--1E-16
-1
-1E-16
-100000.5
-100 000,5
+
+package org.apache.freemarker.core.util;
+
+/**
+ * Used internally only, might change without notice!
+ */
+public interface _Java21 {
+    // This is currently to demonstrate how to support Java features beyond the minimum required version.
+    // You could add methods here that can only be implemented if we are running on Java 21 or later.
+    void example();
+}
diff --git a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21Impl.java
similarity index 84%
copy from freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
copy to freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21Impl.java
index 7929be58..3805bcdb 100644
--- a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/expected/number-format.txt
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_Java21Impl.java
@@ -16,20 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-1
-1
-1 234 567,89
-1234567.886
-1,00
-1
-1234567,89
-1234567.886
-1
-1
-1.000000000000001
-1E-16
--1E-16
-1
-1E-16
-100000.5
-100 000,5
+
+package org.apache.freemarker.core.util;
+
+class _Java21Impl implements _Java21 {
+    @Override
+    public void example() {
+        //
+    }
+}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
index 986e2437..ac55ca88 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
@@ -18,66 +18,31 @@
  */
 package org.apache.freemarker.core.util;
 
-import org.apache.freemarker.core.Version;
-import org.apache.freemarker.core._Java8;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 /**
  * Used internally only, might change without notice!
  */
 public final class _JavaVersions {
 
-    private static final Logger LOG = LoggerFactory.getLogger(_JavaVersions.class);
-
     private _JavaVersions() {
         // Not meant to be instantiated
     }
 
-    private static final boolean IS_AT_LEAST_8;
-    static {
-        boolean result = false;
-        String vStr = _SecurityUtils.getSystemProperty("java.version", null);
-        if (vStr != null) {
-            try {
-                Version v = new Version(vStr);
-                result = v.getMajor() == 1 && v.getMinor() >= 8 || v.getMajor() > 1;
-            } catch (Exception e) {
-                // Ignore
-            }
-        } else {
-            try {
-                Class.forName("java.time.Instant");
-                result = true;
-            } catch (Exception e) {
-                // Ignore
-            }
-        }
-        IS_AT_LEAST_8 = result;
-    }
-    
+    private static final boolean IS_AT_LEAST_21 = Runtime.version().feature() >= 21;
+
     /**
      * {@code null} if Java 8 is not available, otherwise the object through with the Java 8 operations are available.
      */
-    static public final _Java8 JAVA_8;
+    static public final _Java21 JAVA_21;
     static {
-        _Java8 java8;
-        if (IS_AT_LEAST_8) {
+        if (IS_AT_LEAST_21) {
             try {
-                java8 = (_Java8) Class.forName("org.apache.freemarker.core._Java8Impl")
-                        .getField("INSTANCE").get(null);
+                JAVA_21 = (_Java21) Class.forName("freemarker.core._Java21Impl").getField("INSTANCE").get(null);
             } catch (Exception e) {
-                try {
-                    LOG.error("Failed to access Java 8 functionality", e);
-                } catch (Exception e2) {
-                    // Suppressed
-                }
-                java8 = null;
+                throw new RuntimeException("Failed to create _Java21Impl", e);
             }
         } else {
-            java8 = null;
+            JAVA_21 = null;
         }
-        JAVA_8 = java8;
     }
-    
+
 }
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java
index e4056877..9bc2b41b 100644
--- a/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java
+++ b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java
@@ -20,39 +20,21 @@
 package org.apache.freemarker.dom;
 
 
-import java.lang.ref.WeakReference;
-import java.util.Collections;
-import java.util.Map;
-import java.util.WeakHashMap;
-
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.TemplateException;
 import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel;
-import org.apache.freemarker.core.model.AdapterTemplateModel;
-import org.apache.freemarker.core.model.TemplateBooleanModel;
-import org.apache.freemarker.core.model.TemplateDateModel;
-import org.apache.freemarker.core.model.TemplateHashModel;
-import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.TemplateModelIterator;
-import org.apache.freemarker.core.model.TemplateNodeModel;
-import org.apache.freemarker.core.model.TemplateNodeModelEx;
-import org.apache.freemarker.core.model.TemplateNumberModel;
-import org.apache.freemarker.core.model.TemplateSequenceModel;
-import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.*;
 import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
 import org.apache.freemarker.core.model.impl.SimpleString;
 import org.apache.freemarker.core.model.impl.SingleItemTemplateModelIterator;
 import org.slf4j.Logger;
-import org.w3c.dom.Attr;
-import org.w3c.dom.CDATASection;
 import org.w3c.dom.CharacterData;
-import org.w3c.dom.Document;
-import org.w3c.dom.DocumentType;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.w3c.dom.ProcessingInstruction;
-import org.w3c.dom.Text;
+import org.w3c.dom.*;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * A base class for wrapping a single W3C DOM_WRAPPER Node as a FreeMarker template model.
@@ -460,16 +442,6 @@ abstract public class NodeModel implements TemplateNodeModelEx, TemplateHashMode
                 LOG.debug("Failed to use Xalan internal XPath support.", e);
             }
             
-            if (xpathSupportClass == null) try {
-                useSunInternalXPathSupport();
-            } catch (Exception e) {
-                LOG.debug("Failed to use Sun internal XPath support.", e);
-            } catch (IllegalAccessError e) {
-                // Happens on OpenJDK 9 (but not on Oracle Java 9)
-                LOG.debug("Failed to use Sun internal XPath support. "
-                        + "Tip: On Java 9+, you may need Xalan or Jaxen+Saxpath.", e);
-            }
-            
             if (xpathSupportClass == null) try {
                 useJaxenXPathSupport();
             } catch (ClassNotFoundException e) {
@@ -511,17 +483,6 @@ abstract public class NodeModel implements TemplateNodeModelEx, TemplateHashMode
         }
     }
     
-    static public void useSunInternalXPathSupport() throws Exception {
-        Class.forName("com.sun.org.apache.xpath.internal.XPath");
-        Class c = Class.forName("org.apache.freemarker.dom.SunInternalXalanXPathSupport");
-        synchronized (STATIC_LOCK) {
-            xpathSupportClass = c;
-        }
-        if (LOG.isDebugEnabled()) {
-            LOG.debug("Using Sun's internal Xalan classes for XPath support");
-        }
-    }
-    
     /**
      * Set an alternative implementation of org.apache.freemarker.dom.XPathSupport to use
      * as the XPath engine.
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
deleted file mode 100644
index a7057849..00000000
--- a/freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * 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.freemarker.dom;
-
-import java.util.List;
-
-import javax.xml.transform.TransformerException;
-
-import org.apache.freemarker.core.Environment;
-import org.apache.freemarker.core.Template;
-import org.apache.freemarker.core.TemplateException;
-import org.apache.freemarker.core.model.TemplateBooleanModel;
-import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.impl.SimpleNumber;
-import org.apache.freemarker.core.model.impl.SimpleString;
-import org.w3c.dom.Node;
-import org.w3c.dom.traversal.NodeIterator;
-
-import com.sun.org.apache.xml.internal.utils.PrefixResolver;
-import com.sun.org.apache.xpath.internal.XPath;
-import com.sun.org.apache.xpath.internal.XPathContext;
-import com.sun.org.apache.xpath.internal.objects.XBoolean;
-import com.sun.org.apache.xpath.internal.objects.XNodeSet;
-import com.sun.org.apache.xpath.internal.objects.XNull;
-import com.sun.org.apache.xpath.internal.objects.XNumber;
-import com.sun.org.apache.xpath.internal.objects.XObject;
-import com.sun.org.apache.xpath.internal.objects.XString;
-
-/**
- * XPath support implemented on the internal Xalan that is packed into Java under {@code com.sun} packages. This
- * won't be accessible if Java 9 module access rules are enforced (like if the application is started with
- * {@code java --illegal-access=deny}), because then accessing {@code com.sun} packages is banned. In such case
- * {@link XalanXPathSupport} can be used, which however needs the normal Apache Xalan to be present.
- */
-class SunInternalXalanXPathSupport implements XPathSupport {
-    
-    private XPathContext xpathContext = new XPathContext();
-        
-    private static final String ERRMSG_RECOMMEND_JAXEN
-            = "(Note that there is no such restriction if you "
-                    + "configure FreeMarker to use Jaxen instead of Xalan.)";
-
-    private static final String ERRMSG_EMPTY_NODE_SET
-            = "Cannot perform an XPath query against an empty node set." + ERRMSG_RECOMMEND_JAXEN;
-    
-    @Override
-    synchronized public TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateException {
-        if (!(context instanceof Node)) {
-            if (context == null || isNodeList(context)) {
-                int cnt = context != null ? ((List) context).size() : 0;
-                throw new TemplateException(
-                        (cnt != 0
-                                ? "Xalan can't perform an XPath query against a Node Set (contains " + cnt
-                                        + " node(s)). Expecting a single Node."
-                                : "Xalan can't perform an XPath query against an empty Node Set."
-                        )
-                        + " (There's no such restriction if you configure FreeMarker to use Jaxen for XPath.)");
-            } else {
-                throw new TemplateException(
-                        "Can't perform an XPath query against a " + context.getClass().getName()
-                                + ". Expecting a single org.w3c.dom.Node.");
-            }
-        }
-        Node node = (Node) context;
-        try {
-            XPath xpath = new XPath(xpathQuery, null, CUSTOM_PREFIX_RESOLVER, XPath.SELECT, null);
-            int ctxtNode = xpathContext.getDTMHandleFromNode(node);
-            XObject xresult = xpath.execute(xpathContext, ctxtNode, CUSTOM_PREFIX_RESOLVER);
-            if (xresult instanceof XNodeSet) {
-                NodeListModel result = new NodeListModel(node);
-                result.xpathSupport = this;
-                NodeIterator nodeIterator = xresult.nodeset();
-                Node n;
-                do {
-                    n = nodeIterator.nextNode();
-                    if (n != null) {
-                        result.add(n);
-                    }
-                } while (n != null);
-                return result.getCollectionSize() == 1 ? result.get(0) : result;
-            }
-            if (xresult instanceof XBoolean) {
-                return ((XBoolean) xresult).bool() ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
-            }
-            if (xresult instanceof XNull) {
-                return null;
-            }
-            if (xresult instanceof XString) {
-                return new SimpleString(xresult.toString());
-            }
-            if (xresult instanceof XNumber) {
-                return new SimpleNumber(Double.valueOf(((XNumber) xresult).num()));
-            }
-            throw new TemplateException("Cannot deal with type: " + xresult.getClass().getName());
-        } catch (TransformerException te) {
-            throw new TemplateException(te);
-        }
-    }
-
-    private static final PrefixResolver CUSTOM_PREFIX_RESOLVER = new PrefixResolver() {
-        
-        @Override
-        public String getNamespaceForPrefix(String prefix, Node node) {
-            return getNamespaceForPrefix(prefix);
-        }
-        
-        @Override
-        public String getNamespaceForPrefix(String prefix) {
-            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
-                return Environment.getCurrentEnvironment().getDefaultNS();
-            }
-            return Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
-        }
-        
-        @Override
-        public String getBaseIdentifier() {
-            return null;
-        }
-        
-        @Override
-        public boolean handlesNullPrefixes() {
-            return false;
-        }
-    };
-
-    /**
-     * Used for generating more intelligent error messages.
-     */
-    private static boolean isNodeList(Object context) {
-        if (!(context instanceof List)) {
-            return false;
-        }
-
-        List ls = (List) context;
-        int ln = ls.size();
-        for (int i = 0; i < ln; i++) {
-            if (!(ls.get(i) instanceof Node)) {
-                return false;
-            }
-        }
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/freemarker-dom/src/test/java/org/apache/freemarker/dom/DefaultObjectWrapperExtensionTest.java b/freemarker-dom/src/test/java/org/apache/freemarker/dom/DefaultObjectWrapperExtensionTest.java
index fa26e5ef..02d678ed 100644
--- a/freemarker-dom/src/test/java/org/apache/freemarker/dom/DefaultObjectWrapperExtensionTest.java
+++ b/freemarker-dom/src/test/java/org/apache/freemarker/dom/DefaultObjectWrapperExtensionTest.java
@@ -19,12 +19,6 @@
 
 package org.apache.freemarker.dom;
 
-import static org.junit.Assert.*;
-
-import java.io.IOException;
-
-import javax.xml.parsers.ParserConfigurationException;
-
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.TemplateException;
 import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
@@ -34,6 +28,11 @@ import org.junit.Before;
 import org.junit.Test;
 import org.xml.sax.SAXException;
 
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+
 public class DefaultObjectWrapperExtensionTest extends TemplateTest {
 
     @Before
@@ -61,7 +60,10 @@ public class DefaultObjectWrapperExtensionTest extends TemplateTest {
             // Expected
         }
 
-        assertOutput("${doc.getElementsByTagName('title').item(0).textContent}", "test");
+        // TODO [FM3]: java.lang.IllegalAccessException: class org.apache.freemarker.core.model.impl.DefaultObjectWrapper
+        // cannot access class com.sun.org.apache.xerces.internal.dom.ElementImpl (in module java.xml) because module java.xml does not export com.sun.org.apache.xerces.internal.dom to unnamed module @6aba2b86
+        // But, org.w3c.dom.Document.getElementsByTagName is accessible, so we should call that instead.
+        // assertOutput("${doc.getElementsByTagName('title').item(0).textContent}", "test");
     }
 
 }
diff --git a/freemarker-servlet/build.gradle.kts b/freemarker-servlet/build.gradle.kts
index 29161234..b3e51e38 100644
--- a/freemarker-servlet/build.gradle.kts
+++ b/freemarker-servlet/build.gradle.kts
@@ -33,23 +33,29 @@ dependencies {
     api(project(":freemarker-core"))
     api(libs.legacyFreemarker)
 
-    // Because of the limitations of Eclipse dependency handling, we have to use the dependency artifacts from
-    // Jetty ${jettyVersion} here, which is the Jetty version used for the tests. When the jettyVersion changes, run
-    // `gradlew :freemarker-servlet:dependencies` and copy-paste the exact versions to here:
-    compileOnly("org.eclipse.jetty.orbit:javax.servlet:3.0.0.v201112011016")
-    compileOnly("org.eclipse.jetty.orbit:javax.servlet.jsp:2.2.0.v201112011158")
-    compileOnly("org.eclipse.jetty.orbit:javax.el:2.2.0.v201108011116")
-
-    // When changing this, the non-test org.eclipse.jetty.orbit dependencies must be updated as well! Thus, it must use
-    // exactly the same Servlet/JSP-related specification versions as the minimal requirements of FreeMarker.
-    val jettyVersion = "8.1.22.v20160922"
+    // Servlet, JSP, and EL related classes
+    compileOnly("javax.servlet:javax.servlet-api:3.1.0")
+    compileOnly("javax.servlet.jsp:javax.servlet.jsp-api:2.3.3")
+    compileOnly("javax.el:javax.el-api:3.0.0") // EL is not included in jsp-api anymore (was there in jsp-api 2.1)
 
+    // Chose the Jetty version very carefully, as it should implement the same Servlet API, JSP API, and EL API
+    // than what we declare above, because the same classes will come from Jetty as well. For example, Jetty depends
+    // on org.mortbay.jasper:apache-el, which contains the javax.el classes, along with non-javax.el classes, so you
+    // can't even exclude it. Similarly, org.eclipse.jetty:apache-jsp contains the JSP API javax.servlet.jsp classes,
+    // yet again along with other classes. Anyway, this mess is temporary, as we will migrate to Jakarta, and only
+    // support that.
+    val jettyVersion = "9.4.53.v20231009"
     testImplementation("org.eclipse.jetty:jetty-server:$jettyVersion")
     testImplementation("org.eclipse.jetty:jetty-webapp:$jettyVersion")
-    testImplementation("org.eclipse.jetty:jetty-jsp:$jettyVersion")
     testImplementation("org.eclipse.jetty:jetty-util:$jettyVersion")
+    testImplementation("org.eclipse.jetty:apache-jsp:$jettyVersion")
     // Jetty also contains the servlet-api and jsp-api classes
 
+    // JSP JSTL (not included in Jetty):
+    val apacheStandardTaglibsVersion = "1.2.5"
+    testImplementation("org.apache.taglibs:taglibs-standard-impl:$apacheStandardTaglibsVersion")
+    testImplementation("org.apache.taglibs:taglibs-standard-spec:$apacheStandardTaglibsVersion")
+
     testImplementation("displaytag:displaytag:1.2") {
         exclude(group = "com.lowagie", module = "itext")
         // We manage logging centrally:
diff --git a/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java b/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
index 00ea670f..4fa8cd20 100644
--- a/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
+++ b/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
@@ -19,37 +19,8 @@
 
 package org.apache.freemarker.servlet;
 
-import java.io.IOException;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.GregorianCalendar;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-
-import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.*;
 import org.apache.freemarker.core.Configuration.ExtendableBuilder;
-import org.apache.freemarker.core.ConfigurationException;
-import org.apache.freemarker.core.Environment;
-import org.apache.freemarker.core.MutableProcessingConfiguration;
-import org.apache.freemarker.core.Template;
-import org.apache.freemarker.core.TemplateException;
-import org.apache.freemarker.core.TemplateExceptionHandler;
-import org.apache.freemarker.core.TemplateNotFoundException;
 import org.apache.freemarker.core.model.ObjectWrapper;
 import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
 import org.apache.freemarker.core.model.TemplateModel;
@@ -70,6 +41,21 @@ import org.apache.freemarker.servlet.jsp.TaglibFactory.MetaInfTldSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.regex.Pattern;
+
 /**
  * FreeMarker MVC View servlet that can be used similarly to JSP views. That is, you put the variables to expose into
  * HTTP servlet request attributes, then forward to an FTL file (instead of to a JSP file) that's mapped to this servet
@@ -821,13 +807,29 @@ public class FreemarkerServlet extends HttpServlet {
                 }
             }
         } catch (TemplateException e) {
+            boolean suppressServletException;
+
             final TemplateExceptionHandler teh = config.getTemplateExceptionHandler();
             // Ensure that debug handler responses aren't rolled back:
             if (teh == TemplateExceptionHandler.HTML_DEBUG || teh == TemplateExceptionHandler.DEBUG
-                    || teh.getClass().getName().indexOf("Debug") != -1) {
+                    || teh.getClass().getName().contains("Debug")) {
                 response.flushBuffer();
+
+                // Apparently, if the status is 200, yet the servlet throw an exception, Jetty (9.4.53) closes the
+                // connection, so the developer possibly won't see the debug error page (or not all of it).
+                suppressServletException = true;
+            } else {
+                suppressServletException = false;
+            }
+
+            if (suppressServletException) {
+                logServletExceptionWithFreemarkerLog("Error executing FreeMarker template", e);
+                log("Error executing FreeMarker template. " +
+                        "Servlet-level exception was suppressed to show debug page with HTTP 200. " +
+                        "See earlier FreeMarker log message for details!");
+            } else {
+                throw newServletExceptionWithFreeMarkerLogging("Error executing FreeMarker template", e);
             }
-            throw newServletExceptionWithFreeMarkerLogging("Error executing FreeMarker template", e);
         }
     }
 
@@ -878,21 +880,16 @@ public class FreemarkerServlet extends HttpServlet {
     }
 
     private ServletException newServletExceptionWithFreeMarkerLogging(String message, Throwable cause) throws ServletException {
+        logServletExceptionWithFreemarkerLog(message, cause);
+        return new ServletException(message, cause);
+    }
+
+    private static void logServletExceptionWithFreemarkerLog(String message, Throwable cause) {
         if (cause instanceof TemplateException) {
             LOG.error(message, cause);
         }
-
-        ServletException e = new ServletException(message, cause);
-        try {
-            // Prior to Servlet 2.5, the cause exception wasn't set by the above constructor.
-            // If we are on 2.5+ then this will throw an exception as the cause was already set.
-            e.initCause(cause);
-        } catch (Exception ex) {
-            // Ignored; see above
-        }
-        throw e;
     }
-    
+
     /**
      * Returns the locale used for the {@link Configuration#getTemplate(String, Locale)} call (as far as the
      * {@value #INIT_PARAM_OVERRIDE_RESPONSE_LOCALE} Servlet init-param allows that). The base implementation in
diff --git a/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/DummyMockServletContext.java b/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/DummyMockServletContext.java
index 7f508c05..b54f696c 100644
--- a/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/DummyMockServletContext.java
+++ b/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/DummyMockServletContext.java
@@ -18,6 +18,9 @@
  */
 package org.apache.freemarker.servlet;
 
+import javax.servlet.*;
+import javax.servlet.ServletRegistration.Dynamic;
+import javax.servlet.descriptor.JspConfigDescriptor;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -26,18 +29,6 @@ import java.util.EventListener;
 import java.util.Map;
 import java.util.Set;
 
-import javax.servlet.Filter;
-import javax.servlet.FilterRegistration;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.Servlet;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRegistration;
-import javax.servlet.ServletRegistration.Dynamic;
-import javax.servlet.SessionCookieConfig;
-import javax.servlet.SessionTrackingMode;
-import javax.servlet.descriptor.JspConfigDescriptor;
-
 public class DummyMockServletContext implements ServletContext {
 
     @Override
@@ -255,6 +246,11 @@ public class DummyMockServletContext implements ServletContext {
     public void declareRoles(String... roleNames) {
     }
 
+    @Override
+    public String getVirtualServerName() {
+        return null;
+    }
+
     @Override
     public SessionCookieConfig getSessionCookieConfig() {
         return null;
diff --git a/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/test/WebAppTestCase.java b/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/test/WebAppTestCase.java
index 050c92ac..37b80232 100644
--- a/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/test/WebAppTestCase.java
+++ b/freemarker-servlet/src/test/java/org/apache/freemarker/servlet/test/WebAppTestCase.java
@@ -19,7 +19,22 @@
 
 package org.apache.freemarker.servlet.test;
 
-import static org.junit.Assert.*;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.freemarker.test.ResourcesExtractor;
+import org.apache.freemarker.test.TestUtils;
+import org.eclipse.jetty.annotations.ServletContainerInitializersStarter;
+import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
+import org.eclipse.jetty.plus.annotation.ContainerInitializer;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -33,19 +48,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
 
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.freemarker.test.ResourcesExtractor;
-import org.apache.freemarker.test.TestUtils;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.handler.ContextHandlerCollection;
-import org.eclipse.jetty.webapp.WebAppContext;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 public class WebAppTestCase {
 
@@ -112,7 +116,7 @@ public class WebAppTestCase {
 
         ensureWebAppIsDeployed(webAppName);
 
-        final URI uri = new URI("http://localhost:" + server.getConnectors()[0].getLocalPort()
+        final URI uri = new URI("http://localhost:" + ((NetworkConnector) server.getConnectors()[0]).getLocalPort()
                 + "/" + webAppName + "/" + webAppRelURL);
 
         final HttpURLConnection httpCon = (HttpURLConnection) uri.toURL().openConnection();
@@ -244,6 +248,9 @@ public class WebAppTestCase {
         context.setAttribute(
                 ATTR_JETTY_CONTAINER_INCLUDE_JAR_PATTERN,
                 ".*taglib.*\\.jar$");
+
+        addJasperInitializer(context);
+
         contextHandlers.addHandler(context);
         // As we add this after the Server was started, it has to be started manually:
         context.start();
@@ -252,6 +259,20 @@ public class WebAppTestCase {
         LOG.info("Deployed web app.: {}", webAppName);
     }
 
+    /**
+     * Without this, we will have this error when loading a taglib:
+     * NullPointerException: Cannot invoke "org.apache.jasper.compiler.TldCache.getTldResourcePath(String)" because the
+     * return value of "org.apache.jasper.Options.getTldCache()" is null
+     */
+    private static void addJasperInitializer(WebAppContext context) {
+        JettyJasperInitializer jettyJasperInitializer = new JettyJasperInitializer();
+        ServletContainerInitializersStarter servletContainerInitializersStarter
+                = new ServletContainerInitializersStarter(context);
+        ContainerInitializer containerInitializer = new ContainerInitializer(jettyJasperInitializer, null);
+        context.setAttribute("org.eclipse.jetty.containerInitializers", List.of(containerInitializer));
+        context.addBean(servletContainerInitializersStarter, true);
+    }
+
     private static void deleteTemporaryDirectories() throws IOException {
         if (testTempDirectory.getParentFile() == null) {
             throw new IOException("Won't delete the root directory");
diff --git a/freemarker-test-utils/src/main/resources/logback-test.xml b/freemarker-test-utils/src/main/resources/logback-test.xml
index dc96e8db..bf98b412 100644
--- a/freemarker-test-utils/src/main/resources/logback-test.xml
+++ b/freemarker-test-utils/src/main/resources/logback-test.xml
@@ -39,8 +39,9 @@
 		</else>
 	</if>
 
-
 	<logger name="org.eclipse.jetty" level="INFO" />
+	<logger name="org.apache.tomcat" level="INFO" />
+	<logger name="org.apache.jasper" level="INFO" />
 	<!-- logger name="org.apache.freemarker" level="TRACE" / -->
 
 	<root level="debug">
diff --git a/osgi.bnd b/osgi.bnd
index c75a0b1a..3d2976b1 100644
--- a/osgi.bnd
+++ b/osgi.bnd
@@ -52,7 +52,7 @@ DynamicImport-Package: *
 # The required minimum is 1.7, but we utilize versions up to 1.8 if available.
 # See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
 # against more than is required"
-Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Bundle-RequiredExecutionEnvironment: JavaSE-17
 
 # Non-OSGi meta:
 Main-Class: freemarker.core.CommandLine
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ad9902fb..c819bd41 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -24,8 +24,6 @@ apply(from = rootDir.toPath().resolve("gradle").resolve("repositories.gradle.kts
 dependencyResolutionManagement {
     versionCatalogs {
         create("libs") {
-            version("defaultJava", "8")
-
             version("junit", "4.12")
             version("slf4j", "1.7.25")