Merge remote-tracking branch 'origin/2.3-gae' into 2.3
diff --git a/ivy.xml b/ivy.xml
index 0d54d11..251ec1b 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -138,8 +138,8 @@
<dependency org="ch.qos.logback" name="logback-classic" rev="1.1.2" conf="test->default" />
- <dependency org="commons-io" name="commons-io" rev="2.2" conf="test->default" />
- <dependency org="com.google.guava" name="guava-jdk5" rev="17.0" conf="test->default" />
+ <dependency org="commons-io" name="commons-io" rev="2.7" conf="test->default" />
+ <dependency org="com.google.guava" name="guava" rev="29.0-jre" conf="test->default" />
<dependency org="org.eclipse.jetty" name="jetty-server" rev="&jetty.version;" conf="test->default" />
<dependency org="org.eclipse.jetty" name="jetty-webapp" rev="&jetty.version;" conf="test->default" />
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index e0c5135..36b2511 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -775,25 +775,27 @@
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
- ExecutableMemberSignature sig = new ExecutableMemberSignature(method);
- // Contrary to intuition, a class can actually have several
- // different methods with same signature *but* different
- // return types. These can't be constructed using Java the
- // language, as this is illegal on source code level, but
- // the compiler can emit synthetic methods as part of
- // generic type reification that will have same signature
- // yet different return type than an existing explicitly
- // declared method. Consider:
- // public interface I<T> { T m(); }
- // public class C implements I<Integer> { Integer m() { return 42; } }
- // C.class will have both "Object m()" and "Integer m()" methods.
- List<Method> methodList = accessibles.get(sig);
- if (methodList == null) {
- // TODO Collection.singletonList is more efficient, though read only.
- methodList = new LinkedList<>();
- accessibles.put(sig, methodList);
+ if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
+ ExecutableMemberSignature sig = new ExecutableMemberSignature(method);
+ // Contrary to intuition, a class can actually have several
+ // different methods with same signature *but* different
+ // return types. These can't be constructed using Java the
+ // language, as this is illegal on source code level, but
+ // the compiler can emit synthetic methods as part of
+ // generic type reification that will have same signature
+ // yet different return type than an existing explicitly
+ // declared method. Consider:
+ // public interface I<T> { T m(); }
+ // public class C implements I<Integer> { Integer m() { return 42; } }
+ // C.class will have both "Object m()" and "Integer m()" methods.
+ List<Method> methodList = accessibles.get(sig);
+ if (methodList == null) {
+ // TODO Collection.singletonList is more efficient, though read only.
+ methodList = new LinkedList<>();
+ accessibles.put(sig, methodList);
+ }
+ methodList.add(method);
}
- methodList.add(method);
}
return;
} catch (SecurityException e) {
@@ -814,21 +816,26 @@
}
}
+ // This is needed as java.bean.Introspector sometimes gives back a method that's actually not accessible,
+ // as it's an override of an accessible method in a non-public subclass. While that's still a public method, calling
+ // it directly via reflection will throw java.lang.IllegalAccessException, and we are supposed to call the overidden
+ // accessible method instead. Like, we migth get two PropertyDescriptor-s for the same property name, and only one
+ // will have a reader method that we can actually call. So we have to find that method here.
+ // Furthermore, the return type of the inaccisable method is possibly different (more specific) than the return type
+ // of the overidden accessible method. Also Introspector behavior changed with Java 9, as earlier in such case the
+ // Introspector returned all variants of the method (so the accessible one was amongst them at least), while in
+ // Java 9 it apparently always returns one variant only, but that's sometimes (not sure if it's predictable) the
+ // inaccessbile one.
private static Method getMatchingAccessibleMethod(Method m, Map<ExecutableMemberSignature, List<Method>> accessibles) {
if (m == null) {
return null;
}
- ExecutableMemberSignature sig = new ExecutableMemberSignature(m);
- List<Method> ams = accessibles.get(sig);
- if (ams == null) {
- return null;
- }
- for (Method am : ams) {
- if (am.getReturnType() == m.getReturnType()) {
- return am;
- }
- }
- return null;
+ List<Method> ams = accessibles.get(new ExecutableMemberSignature(m));
+ // Certainly we could return any of the accessbiles, as Java reflection will call the correct override of the
+ // method anyway. There's an ambiguity when the return type is "overloaded", but in practice it probably doesn't
+ // matter which variant we call. Though, technically, they could do totaly different things. So, to avoid any
+ // corner cases that cause problems after an upgrade, we make an effort to give same result as before 2.3.31.
+ return ams != null ? _MethodUtil.getMethodWithClosestNonSubReturnType(m.getReturnType(), ams) : null;
}
private static Method getFirstAccessibleMethod(
diff --git a/src/main/java/freemarker/ext/beans/_MethodUtil.java b/src/main/java/freemarker/ext/beans/_MethodUtil.java
index 1540853..99a733e 100644
--- a/src/main/java/freemarker/ext/beans/_MethodUtil.java
+++ b/src/main/java/freemarker/ext/beans/_MethodUtil.java
@@ -25,6 +25,7 @@
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -458,4 +459,85 @@
return getInheritableFieldAnnotation(superClass, fieldName, false, annotationClass);
}
+ public static Method getMethodWithClosestNonSubReturnType(
+ Class<?> returnType, Collection<Method> methods) {
+ // Exact match:
+ for (Method method : methods) {
+ if (method.getReturnType() == returnType) {
+ return method;
+ }
+ }
+
+ if (returnType == Object.class || returnType.isPrimitive()) {
+ // We can't go wider than these types, so we give up. Note that void is primitive.
+ return null;
+ }
+
+ // Super-class match, which we prefer over Interface match, except if the match is just Object:
+ Class<?> superClass = returnType.getSuperclass();
+ while (superClass != null && superClass != Object.class) {
+ for (Method method : methods) {
+ if (method.getReturnType() == superClass) {
+ return method;
+ }
+ }
+ superClass = superClass.getSuperclass();
+ }
+
+ // Interface match:
+ Method result = getMethodWithClosestNonSubInterfaceReturnType(returnType, methods);
+ if (result != null) {
+ return result;
+ }
+
+ // As the returnType is non-primitive, Object return type will do, as a last resort:
+ for (Method method : methods) {
+ if (method.getReturnType() == Object.class) {
+ return method;
+ }
+ }
+
+ return null;
+ }
+
+ private static Method getMethodWithClosestNonSubInterfaceReturnType(
+ Class<?> returnType, Collection<Method> methods) {
+ HashSet<Class<?>> nullResultReturnTypeInterfaces = new HashSet<>();
+ do {
+ Method result
+ = getMethodWithClosestNonSubInterfaceReturnType(returnType, methods, nullResultReturnTypeInterfaces);
+ if (result != null) {
+ return result;
+ }
+ // As Class.getInterfaces() doesn't return the inherited interfaces, we have to do this.
+ returnType = returnType.getSuperclass();
+ } while (returnType != null);
+ return null;
+ }
+
+ private static Method getMethodWithClosestNonSubInterfaceReturnType(
+ Class<?> returnType, Collection<Method> methods, Set<Class<?>> nullResultReturnTypeInterfaces) {
+ boolean returnTypeIsInterface = returnType.isInterface();
+ if (returnTypeIsInterface) {
+ if (nullResultReturnTypeInterfaces.contains(returnType)) {
+ return null;
+ }
+ for (Method method : methods) {
+ if (method.getReturnType() == returnType) {
+ return method;
+ }
+ }
+ }
+ for (Class<?> subInterface : returnType.getInterfaces()) {
+ Method result = getMethodWithClosestNonSubInterfaceReturnType(subInterface, methods, nullResultReturnTypeInterfaces);
+ if (result != null) {
+ return result;
+ }
+ }
+ if (returnTypeIsInterface) {
+ nullResultReturnTypeInterfaces.add(returnType);
+ }
+ return null;
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 93139ae..bd7585a 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -472,6 +472,9 @@
/** FreeMarker version 2.3.30 (an {@link #Configuration(Version) incompatible improvements break-point}) */
public static final Version VERSION_2_3_30 = new Version(2, 3, 30);
+ /** FreeMarker version 2.3.31 (an {@link #Configuration(Version) incompatible improvements break-point}) */
+ public static final Version VERSION_2_3_31 = new Version(2, 3, 31);
+
/** The default of {@link #getIncompatibleImprovements()}, currently {@link #VERSION_2_3_0}. */
public static final Version DEFAULT_INCOMPATIBLE_IMPROVEMENTS = Configuration.VERSION_2_3_0;
/** @deprecated Use {@link #DEFAULT_INCOMPATIBLE_IMPROVEMENTS} instead. */
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 18c16e4..71feecb 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -54,6 +54,7 @@
public static final int VERSION_INT_2_3_28 = Configuration.VERSION_2_3_28.intValue();
public static final int VERSION_INT_2_3_29 = Configuration.VERSION_2_3_29.intValue();
public static final int VERSION_INT_2_3_30 = Configuration.VERSION_2_3_30.intValue();
+ public static final int VERSION_INT_2_3_31 = Configuration.VERSION_2_3_31.intValue();
public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0);
/**
diff --git a/src/main/resources/freemarker/version.properties b/src/main/resources/freemarker/version.properties
index 641b6a6..13d378b 100644
--- a/src/main/resources/freemarker/version.properties
+++ b/src/main/resources/freemarker/version.properties
@@ -56,11 +56,11 @@
# continue working without modification or recompilation.
# - When the major version number is increased, major backward
# compatibility violations are allowed, but still should be avoided.
-version=2.3.30
+version=2.3.31-nightly_@timestampInVersion@
# This exists as for Maven we use "-SNAPSHOT" for nightly releases,
# and no _nightly_@timestampInVersion@. For final releases it's the
# same as "version".
-mavenVersion=2.3.30
+mavenVersion=2.3.31-SNAPSHOT
# Version string that conforms to OSGi
# ------------------------------------
@@ -73,7 +73,7 @@
# 2.4.0.rc01
# 2.4.0.pre01
# 2.4.0.nightly_@timestampInVersion@
-versionForOSGi=2.3.30.stable
+versionForOSGi=2.3.31.nightly_@timestampInVersion@
# Version string that conforms to legacy MF
# -----------------------------------------
@@ -92,7 +92,7 @@
# "97 denotes "nightly", 98 denotes "pre", 99 denotes "rc" build.
# In general, for the nightly/preview/rc Y of version 2.X, the versionForMf is
# 2.X-1.(99|98).Y. Note the X-1.
-versionForMf=2.3.30
+versionForMf=2.3.30.97
# The date of the build.
# This should be automatically filled by the building tool (Ant).
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 89e0fb3..29dce43 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -29,7 +29,7 @@
<titleabbrev>Manual</titleabbrev>
- <productname>Freemarker 2.3.30</productname>
+ <productname>Freemarker 2.3.31</productname>
</info>
<preface role="index.html" xml:id="preface">
@@ -21602,37 +21602,59 @@
<section>
<title>Description</title>
- <para>Imports a library. That is, it creates a new empty namespace,
- and then executes the template given with
- <literal><replaceable>path</replaceable></literal> parameter in that
- namespace so the template populates the namespace with variables
- (macros, functions, ...etc.). Then it makes the newly created
- namespace available to the caller with a hash variable. The hash
- variable will be created as a plain variable in the namespace used
- by the caller of <literal>import</literal> (as if you would create
- it with <literal>assign</literal> directive), with the name given
- with the <literal><replaceable>hash</replaceable></literal>
- parameter. If the import happens in the namespace of the main
+ <para>Used for making a collection of macros, functions, and other
+ variables available for the importing template, which were defined
+ in the imported template. For example, let's say you have written
+ macros to generate some commonly needed pieces output, and you have
+ put them into <literal>/libs/commons.ftl</literal>. Then, in the
+ template where you want to use them, do this (near the top of the
+ the template by convention, next to any other
+ <literal>import</literal>-s):</para>
+
+ <programlisting role="template"><#import "/libs/commons.ftl" as com>
+</programlisting>
+
+ <para>Later in same template, let's say you want to use the
+ <literal>copyright</literal> macro defined in
+ <literal>/libs/commons.ftl</literal>. Then you can call that macro
+ like this:</para>
+
+ <programlisting role="template"><@com.copyright date="1999-2002"/></programlisting>
+
+ <para>Note the <literal>com.</literal> before the macro name above.
+ All that was defined in <literal>/libs/commons.ftl</literal> will be
+ inside <literal>com</literal>.</para>
+
+ <para>Described more technically, <literal>import</literal> first
+ creates a new empty <link
+ linkend="dgui_misc_namespace">namespace</link>, and then executes
+ the template given with
+ <literal><replaceable>path</replaceable></literal> parameter inside
+ that namespace, so the template populates the namespace with
+ variables (macros, functions, ...etc.). Then the namespace is
+ assigned to the variable specified with the
+ <literal><replaceable>hash</replaceable></literal> parameter, and
+ you can access its contents through that. A namespace is <link
+ linkend="dgui_datamodel_container">a hash</link>, hence <link
+ linkend="dgui_template_exp_var_hash">the dot operator</link> worked
+ above. The assignment is like the <literal>assign</literal>
+ directive, that is, it sets the variable in the current namespace.
+ Except, if the import happens in the namespace of the main (topmost)
template, the hash variable is also created in the global
namespace.</para>
<para>If you call <literal>import</literal> with the same
<literal><replaceable>path</replaceable></literal> for multiple
times, it will create the namespace and run the template for the
- very first call of <literal>import</literal> only. The later calls
- will just create a hash by which you can access the
- <emphasis>same</emphasis> namespace.</para>
+ first call of <literal>import</literal> only. The later calls will
+ just give back the namespace that was created and initialized when
+ the template was imported for the first time, and will not execute
+ the imported template.</para>
- <para>The output printed by the imported template will be ignored
- (will not be inserted at the place of importing). The template is
- executed to populate the namespace with variables, and not to write
- to the output.</para>
-
- <para>Example:</para>
-
- <programlisting role="template"><#import "/libs/mylib.ftl" as my>
-
-<@my.copyright date="1999-2002"/></programlisting>
+ <para>Any output printed by the imported template will be ignored
+ (will not be inserted at the place of <literal>import</literal>
+ directive invocation). An imported template is executed to populate
+ its namespace with variables, and not to write to the output.</para>
<para>The <literal><replaceable>path</replaceable></literal>
parameter can be a relative path like <literal>"foo.ftl"</literal>
@@ -21641,26 +21663,34 @@
directory of the template that uses the <literal>import</literal>
directive. Absolute paths are relative to a base (often referred as
the ''root directory of the templates'') that the programmer defines
- when he configures FreeMarker.</para>
+ when configuring FreeMarker.</para>
<para>Always use <literal>/</literal> (slash) to separate path
components, never <literal>\</literal> (backslash). If you are
loading templates from your local file system and it uses
- backslashes (like under. Windows), FreeMarker will convert them
- automatically.</para>
+ backslashes (like under Windows), FreeMarker will do the necessary
+ conversions automatically.</para>
<para>Like with the <literal>include</literal> directive, <link
linkend="ref_directive_include_acquisition">acquisition</link> and
<link linkend="ref_directive_include_localized">localized
lookup</link> may be used for resolving the path.</para>
- <para><phrase role="forProgrammers">Note, that it is possible to
+ <para><phrase role="forProgrammers">Note that it's possible to
automatically do the commonly used imports for all templates, with
- the "auto imports" setting of
+ the "auto imports" setting of <literal>Configuration</literal>.
+ Because templates may not use all the automatically imported
+ namespaces, it's also possible to make imports lazy (on demand),
+ with the <quote>lazy auto imports</quote> setting of
<literal>Configuration</literal>.</phrase></para>
<para>If you are new to namespaces, you should read: <xref
linkend="dgui_misc_namespace"/></para>
+
+ <para>In case you are not sure if you should use the
+ <literal>import</literal>, or the somewhat similar
+ <literal>include</literal> directive, <link
+ linkend="topic.import_vs_include">then see this</link>.</para>
</section>
</section>
@@ -21723,6 +21753,16 @@
<section>
<title>Description</title>
+ <note>
+ <para><emphasis>Using <literal>include</literal> directive is
+ almost always a bad practice</emphasis>, and you should consider
+ using <link linkend="ref.directive.import">the
+ <literal>import</literal> directive</link> instead! Even if using
+ <literal>import</literal> adds some verbosity, on the long run it
+ can pay off. See <link linkend="topic.import_vs_include">the
+ reasons here...</link></para>
+ </note>
+
<para>You can use it to insert another FreeMarker template file
(specified by the <literal><replaceable>path</replaceable></literal>
parameter) into your template. The output from the included template
@@ -22055,6 +22095,73 @@
</listitem>
</itemizedlist>
</section>
+
+ <section xml:id="topic.import_vs_include">
+ <title>Why <literal>import</literal> should be used instead of
+ <literal>include</literal></title>
+
+ <para>Generally, using <link linkend="ref.directive.import">the
+ <literal>import</literal> directive</link> is a better practice
+ than using <literal>include</literal>.</para>
+
+ <para>At first glance, import is only fitting if you have a
+ collection of commonly used macros, functions, and other
+ variables, which you put into a template for reuse. But, people
+ often use <literal>include</literal> to insert a common fragment
+ of output (e.g. page footer) into multiple templates. The
+ <literal>import</literal> directive has no output, so it's clearly
+ not a direct replacement. But, it's usually a better practice to
+ <link linkend="dgui_misc_userdefdir">put those output fragments
+ into macros</link>, as macros can have parameters, or even nested
+ content. If you do that, you have a collection of macros in a
+ template, that you can <literal>import</literal>.</para>
+
+ <para>So if you have collection of macros, functions and other
+ variables in a template, this is why <literal>import</literal> is
+ generally a better choice:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>Imported templates are processed only when first
+ requested. To compare with <literal>include</literal>, let's
+ say template <literal>top.ftl</literal> includes
+ <literal>commons.ftl</literal>, and
+ <literal>tables.ftl</literal>. If
+ <literal>tables.ftl</literal> also includes
+ <literal>commons.ftl</literal>, then now
+ <literal>commons.ftl</literal> will be processed twice. On the
+ other hand, importing <literal>commons.ftl</literal> for the
+ second time just gives back the namespace that was already
+ initialized during the first import.</para>
+ </listitem>
+
+ <listitem>
+ <para>With imports, each imported template has its own
+ namespace. As they don't just drop everything into a common
+ shared namespace, it's easier to see in templates where a
+ referred variable, or macro/function is coming from.
+ Accidental name clashes are also avoided.</para>
+ </listitem>
+
+ <listitem>
+ <para>If you have several collections of useful
+ macros/functions/constants (say,
+ <literal>commons.ftl</literal>, <literal>form.ftl</literal>,
+ <literal>report.ftl</literal>, etc.), and you decide to
+ auto-import them, but a top-level template usually only uses
+ some of them, you can configure auto-imports to be lazy (i.e.,
+ they on happen when something in their namespace is actually
+ accessed). This is not possible with auto-includes.</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>import</literal> never prints to the output,
+ while <literal>include</literal> might prints unwanted output,
+ like some whitespace that wasn't removed by the automatic
+ whitespace removal.</para>
+ </listitem>
+ </itemizedlist>
+ </section>
</section>
</section>
@@ -28903,7 +29010,7 @@
application developers, system administrators, or other highly
trusted personnel. Consider templates as part of the source code
just like <literal>*.java</literal> files are. If you still want
- to allow users to upload templates, here's what to
+ to allow untrusted users to upload templates, here's what to
consider:</para>
<itemizedlist>
@@ -28912,7 +29019,7 @@
(<literal>Configuration.setObjectWrapper</literal>): The
data-model might gives access to the public Java API of some
objects that you have put into the data-model. By default, for
- objects that aren't instances of a the bunch of specially
+ objects that aren't instances of the bunch of specially
handled types (<literal>String</literal>,
<literal>Number</literal>, <literal>Boolean</literal>,
<literal>Date</literal>, <literal>Map</literal>,
@@ -28929,7 +29036,7 @@
<literal>List</literal>-s, <literal>Array</literal>-s,
<literal>String</literal>-s, <literal>Number</literal>-s,
<literal>Boolean</literal>-s and <literal>Date</literal>-s.
- But for many application that's too restrictive, and instead
+ But for many applications that's too restrictive, and instead
you have to create a
<literal>WhitelistMemberAccessPolicy</literal>, and create a
<literal>DefaultObjectWrapper</literal> (or other
@@ -28941,10 +29048,24 @@
<literal>ObjectWrapper</literal> implementation of
course.)</para>
+ <para>Always expect that templates may get some objects that
+ you haven't put into the data-model yourself. Notably,
+ templates can always get a <literal>Locale</literal> object
+ with the <literal>.locale_object</literal> expression. Or the
+ web application framework you are using may exposes some
+ objects, like attributes from the Servlet scopes. Such objects
+ will be still wrapped with the
+ <literal>ObjectWrapper</literal> that you set in the
+ <literal>Configuration</literal>, and this is why it's
+ important to ensure safety on that level. Controlling what
+ objects the template will have access to is hard, but you can
+ control centrally what members of any object they have access
+ to.</para>
+
<para>If you are creating <literal>TemplateModel</literal>-s
in custom code (instead of the
- <literal>ObjectWrapper</literal> creating those), be sure you
- avoid deprecated container constructors like <literal>new
+ <literal>ObjectWrapper</literal> creating those), be sure that
+ you avoid deprecated container constructors like <literal>new
SimpleSequence()</literal>, as those will use the also
deprecated default object wrapper instance, which doesn't have
the same restrictions than the
@@ -28969,18 +29090,39 @@
by the <literal>MemberAccessPolicy</literal> also won't be
visible with <literal>?api</literal> (assuming you are using a
well behaving <literal>ObjectWrapper</literal>, like
- <literal>DefaultObjectWrapper</literal> is, hopefully.)</para>
+ <literal>DefaultObjectWrapper</literal>.)</para>
- <para>Last not least, some maybe aware of that the standard
- object wrappers filters out some well known
- <quote>unsafe</quote> methods, like
+ <para>If you are using the default object wrapper class
+ (<literal>freemarker.template.DefaultObjectWrapper</literal>),
+ or a subclass of it, you should disable the XML (DOM) wrapping
+ feature of it, by overriding <literal>wrapDomNode(Object
+ obj)</literal> so that it does this: <literal>return
+ getModelFactory(obj.getClass()).create(obj, this);</literal>.
+ The problem with the XML wrapping feature, which wraps
+ <literal>org.w3c.dom.Node</literal> objects on special way to
+ make them easier to work with in templates, is that this
+ facility by design lets template authors evaluate arbitrary
+ XPath expressions, and XPath can do too much in certain
+ setups. If you really need the XML wrapping facility, review
+ carefully what XPath expressions are possible in your setup.
+ Also, be sure you don't use the long deprecated, and more
+ dangerous <literal>freemarker.ext.xml</literal> package, only
+ <literal>freemarker.ext.dom</literal>. Also, note that when
+ using the XML wrapping feature, not allowing
+ <literal>org.w3c.dom.Node</literal> methods in the
+ <literal>MemberAccessPolicy</literal> has no effect, since it
+ doesn't expose Java <literal>Node</literal> members to
+ templates directly.</para>
+
+ <para>Last not least, some maybe aware of the historical
+ legacy that standard object wrappers filter out some well
+ known <quote>unsafe</quote> methods, like
<literal>System.exit</literal>. Do not ever rely on that,
- since it only blocks the methods that's in a small predefined
- list (for some historical reasons). The standard Java API is
- huge and ever growing, and then there are the 3rd party
- libraries, and the API-s of your own application. Clearly it's
- impossible to blacklist all the problematic members in
- those.</para>
+ since it only blocks the methods from a small predefined list.
+ The standard Java API is huge and ever growing, and then there
+ are the 3rd party libraries, and the API-s of your own
+ application. Clearly it's impossible to blacklist all the
+ problematic members in those.</para>
</listitem>
<listitem>
@@ -29020,11 +29162,13 @@
<literal>TemplateModel</literal>, its static initialization
will be run. To avoid these, you should use a
<literal>TemplateClassResolver</literal> that restricts the
- accessible classes (possibly based on which template asks for
- them), such as
+ accessible classes to the absolute minimum (possibly based on
+ which template asks for them), such as
<literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.
- Note that if, and only if your
- <literal>ObjectWrapper</literal> is a
+ Do <emphasis>not</emphasis> use
+ <literal>TemplateClassResolver.SAFER_RESOLVER</literal>, it's
+ not restrictive enough for this purpose! Note that if, and
+ only if your <literal>ObjectWrapper</literal> is a
<literal>BeansWrapper</literal> or a subclass of it (typically
<literal>DefaultObjectWrapper</literal>), constructors not
allowed by the <literal>MemberAccessPolicy</literal> also
@@ -29167,10 +29311,49 @@
<appendix xml:id="app_versions">
<title>Version history</title>
+ <section xml:id="versions_2_3_31">
+ <title>2.3.31</title>
+
+ <para>Release date: FIXME</para>
+
+ <section>
+ <title>Changes on the Java side</title>
+
+ <itemizedlist>
+ <listitem>
+ <para><link
+ xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-145">FREEMARKER-145</link>:
+ Fixed bug where methods with "overloaded" return type may become
+ inaccessible on Java 9+, if some overriding subclasses are not
+ public. (This is because
+ <literal>java.beans.Introspector</literal> behavior has changed
+ with Java 9.)</para>
+ </listitem>
+
+ <listitem>
+ <para><link
+ xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-133">FREEMARKER-133</link>:
+ Fixed bug where FreeMarker sometimes tries to expose public
+ methods that are defined or overridden in a non-public class, if
+ the non-public class was then extended by a public class.
+ Calling such method just ends up with
+ <literal>IllegalAccessException</literal>, but they shouldn't be
+ exposed on the first place. Furthermore, such a wrong method
+ sometimes replaces the good version of the method, which would
+ be callable. When this glitch happens is somewhat unpredictable,
+ as it also depends on what methods
+ <literal>java.beans.Introspector</literal> exposes (which at
+ very least can depend on the Java version), and in what
+ order.</para>
+ </listitem>
+ </itemizedlist>
+ </section>
+ </section>
+
<section xml:id="versions_2_3_30">
<title>2.3.30</title>
- <para>Release date: 2020-02-16 + release process</para>
+ <para>Release date: 2020-03-05</para>
<para>Please note that with this version the minimum required Java
version was increased from Java 5 to Java 7.</para>
diff --git a/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java b/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java
new file mode 100644
index 0000000..05fa695
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java
@@ -0,0 +1,38 @@
+/*
+ * 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 freemarker.ext.beans;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+
+import org.junit.Test;
+
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class Java9InstrospectorBugWorkaround extends TemplateTest {
+
+ @Test
+ public void test() throws IOException, TemplateException {
+ addToDataModel("path", Paths.get("foo", "bar"));
+ assertOutput("<#assign _ = path.parent>", "");
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MethodUtilTest2.java b/src/test/java/freemarker/ext/beans/MethodUtilTest2.java
new file mode 100644
index 0000000..9723613
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodUtilTest2.java
@@ -0,0 +1,164 @@
+/*
+ * 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 freemarker.ext.beans;
+
+import static freemarker.ext.beans._MethodUtil.*;
+import static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Test;
+
+public class MethodUtilTest2 {
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType1() {
+ List<Method> methods = getMethods(ObjectM.class, ListM.class, CollectionM.class);
+ assertEquals(getMethod(ObjectM.class), getMethodWithClosestNonSubReturnType(Object.class, methods));
+ assertEquals(getMethod(CollectionM.class), getMethodWithClosestNonSubReturnType(Collection.class, methods));
+ assertEquals(getMethod(ListM.class), getMethodWithClosestNonSubReturnType(List.class, methods));
+ assertEquals(getMethod(ListM.class), getMethodWithClosestNonSubReturnType(ArrayList.class, methods));
+ assertEquals(getMethod(ObjectM.class), getMethodWithClosestNonSubReturnType(String.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(int.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(void.class, methods));
+ }
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType2() {
+ List<Method> methods = getMethods(ListM.class, CollectionM.class);
+ assertNull(getMethodWithClosestNonSubReturnType(Object.class, methods));
+ assertEquals(getMethod(CollectionM.class), getMethodWithClosestNonSubReturnType(Collection.class, methods));
+ assertEquals(getMethod(ListM.class), getMethodWithClosestNonSubReturnType(List.class, methods));
+ assertEquals(getMethod(ListM.class), getMethodWithClosestNonSubReturnType(ArrayList.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(String.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(int.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(void.class, methods));
+ }
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType3() {
+ List<Method> methods = getMethods(ObjectM.class, SerializableM.class);
+ assertEquals(getMethod(SerializableM.class), getMethodWithClosestNonSubReturnType(String.class, methods));
+ assertEquals(getMethod(SerializableM.class), getMethodWithClosestNonSubReturnType(Serializable.class, methods));
+ assertEquals(getMethod(ObjectM.class), getMethodWithClosestNonSubReturnType(List.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(int.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(void.class, methods));
+ }
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType4() {
+ List<Method> methods = getMethods(ReturnType1M.class, ReturnType2M.class, ObjectM.class);
+ assertEquals(getMethod(ReturnType2M.class), getMethodWithClosestNonSubReturnType(ReturnType3.class, methods));
+ assertEquals(getMethod(ReturnType2M.class), getMethodWithClosestNonSubReturnType(ReturnType2.class, methods));
+ assertEquals(getMethod(ReturnType1M.class), getMethodWithClosestNonSubReturnType(ReturnType1.class, methods));
+ assertEquals(getMethod(ObjectM.class), getMethodWithClosestNonSubReturnType(Serializable.class, methods));
+ }
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType5() {
+ List<Method> methods = getMethods(SerializableM.class, ReturnType1M.class);
+ assertEquals(getMethod(ReturnType1M.class), getMethodWithClosestNonSubReturnType(ReturnType3.class, methods));
+ }
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType6() {
+ List<Method> methods = getMethods(SerializableM.class);
+ assertEquals(getMethod(SerializableM.class), getMethodWithClosestNonSubReturnType(ReturnType3.class, methods));
+ }
+
+ @Test
+ public void testGetMethodWithClosestNonSubReturnType7() {
+ List<Method> methods = getMethods(IntM.class, VoidM.class, ObjectM.class, CollectionM.class);
+ assertEquals(getMethod(IntM.class), getMethodWithClosestNonSubReturnType(int.class, methods));
+ assertEquals(getMethod(VoidM.class), getMethodWithClosestNonSubReturnType(void.class, methods));
+ assertNull(getMethodWithClosestNonSubReturnType(long.class, methods));
+ assertEquals(getMethod(ObjectM.class), getMethodWithClosestNonSubReturnType(Long.class, methods));
+ assertEquals(getMethod(CollectionM.class), getMethodWithClosestNonSubReturnType(List.class, methods));
+ }
+
+ private static List<Method> getMethods(Class<?>... methodHolders) {
+ List<Method> result = new ArrayList<>();
+ for (Class<?> methodHolder : methodHolders) {
+ result.add(getMethod(methodHolder));
+ }
+ return result;
+ }
+
+ private static Method getMethod(Class<?> methodHolder) {
+ try {
+ return methodHolder.getMethod("m");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static class ObjectM {
+ public Object m() { return null; }
+ }
+
+ public static class CollectionM {
+ public Collection<?> m() { return null; }
+ }
+
+ public static class ListM {
+ public List<?> m() { return null; }
+ }
+
+ public static class SerializableM {
+ public Serializable m() { return null; }
+ }
+
+ public static class StringM {
+ public String m() { return null; }
+ }
+
+ public static class ReturnType1M {
+ public ReturnType1 m() { return null; }
+ }
+
+ public static class ReturnType2M {
+ public ReturnType2 m() { return null; }
+ }
+
+ public static class ReturnType3M {
+ public ReturnType3 m() { return null; }
+ }
+
+ public static class IntM {
+ public int m() { return 0; }
+ }
+
+ public static class LongM {
+ public long m() { return 0L; }
+ }
+
+ public static class VoidM {
+ public void m() { }
+ }
+
+ public static class ReturnType1 { }
+ public static class ReturnType2 extends ReturnType1 implements Serializable { }
+ public static class ReturnType3 extends ReturnType2 { }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index 586bd98..9bf0609 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -339,12 +339,14 @@
try {
new Configuration(new Version(999, 1, 2));
+ fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("upgrade"));
}
try {
new Configuration(new Version(2, 2, 2));
+ fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("2.3.0"));
}
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index 3334301..b973358 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -99,6 +99,7 @@
expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.28
expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.29
expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.30
+ expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.31
List<Version> actual = new ArrayList<>();
for (int i = _TemplateAPI.VERSION_INT_2_3_0; i <= Configuration.getVersion().intValue(); i++) {