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 <ddek...@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")