In the mini-language used for configuring FreeMarker from java.util.Properties or other string-only sources (but not used inside templates): public static fields can be referred like com.example.MyClass.MY_CONSTANT or Configuration.AUTO_DETECT_TAG_SYNTAX.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/fe799f5d Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/fe799f5d Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/fe799f5d Branch: refs/heads/master Commit: fe799f5d382fad7e35be634f660e73f62d6ec9e8 Parents: 731be2c Author: ddekany <[email protected]> Authored: Mon Dec 21 02:05:03 2015 +0100 Committer: ddekany <[email protected]> Committed: Mon Dec 21 02:05:16 2015 +0100 ---------------------------------------------------------------------- src/main/java/freemarker/core/Configurable.java | 6 +- .../core/_ObjectBuilderSettingEvaluator.java | 123 ++++++++++++++++--- src/manual/en_US/book.xml | 18 ++- .../core/ObjectBuilderSettingsTest.java | 43 +++++++ 4 files changed, 167 insertions(+), 23 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe799f5d/src/main/java/freemarker/core/Configurable.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java index ba5d8c8..9c82428 100644 --- a/src/main/java/freemarker/core/Configurable.java +++ b/src/main/java/freemarker/core/Configurable.java @@ -1887,6 +1887,8 @@ public class Configurable { * The keys and values can be any kind of expression, like even object builder expressions. * The resulting Java object will be a {@link Map} that keeps the item order ({@link LinkedHashMap} as * of this writing). + * <li>A reference to a public static filed, like {@code Configuration.AUTO_DETECT_TAG_SYNTAX} or + * {@code com.example.MyClass.MY_CONSTANT}. * <li>An object builder expression. That is, object builder expressions can be nested into each other. * </ul> * </li> @@ -1901,13 +1903,13 @@ public class Configurable { * can't be omitted for nested expressions. * </li> * <li> - * <p>The following classes can be referred to with short class name instead of full qualified name: + * <p>The following classes can be referred to with simple (unqualified) name instead of fully qualified name: * {@link DefaultObjectWrapper}, {@link BeansWrapper}, {@link SimpleObjectWrapper}, {@link Locale}, * {@link TemplateConfiguration}, {@link PathGlobMatcher}, {@link FileNameGlobMatcher}, {@link PathRegexMatcher}, * {@link AndMatcher}, {@link OrMatcher}, {@link NotMatcher}, {@link ConditionalTemplateConfigurationFactory}, * {@link MergingTemplateConfigurationFactory}, {@link FirstMatchTemplateConfigurationFactory}, * {@link HTMLOutputFormat}, {@link XMLOutputFormat}, {@link RTFOutputFormat}, {@link PlainTextOutputFormat}, - * {@link UndefinedOutputFormat}. + * {@link UndefinedOutputFormat}, {@link Configuration}. * </li> * <li> * <p>{@link TimeZone} objects can be created like {@code TimeZone("UTC")}, despite that there's no a such http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe799f5d/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java b/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java index a1e1c22..afbb436 100644 --- a/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java +++ b/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java @@ -46,6 +46,7 @@ import freemarker.cache.OrMatcher; import freemarker.cache.PathGlobMatcher; import freemarker.cache.PathRegexMatcher; import freemarker.ext.beans.BeansWrapper; +import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; import freemarker.template.SimpleObjectWrapper; import freemarker.template.TemplateHashModel; @@ -75,7 +76,7 @@ public class _ObjectBuilderSettingEvaluator { private static final String BUILDER_CLASS_POSTFIX = "Builder"; - private static Map/*<String,String>*/ SHORTHANDS; + private static Map<String,String> SHORTHANDS; private static final Object VOID = new Object(); @@ -165,9 +166,12 @@ public class _ObjectBuilderSettingEvaluator { int startPos = pos; BuilderCallExpression exp = new BuilderCallExpression(); + // We need the canBeStaticField/mustBeStaticFiled complication to deal with legacy syntax where parentheses + // weren't required for constructor calls. + exp.canBeStaticField = true; + final String fetchedClassName = fetchClassName(optional); { - final String fetchedClassName = fetchClassName(optional); if (fetchedClassName == null) { if (!optional) { throw new _ObjectBuilderSettingEvaluationException("class name", src, pos); @@ -178,6 +182,7 @@ public class _ObjectBuilderSettingEvaluator { if (!fetchedClassName.equals(exp.className)) { // Before 2.3.21 only full-qualified class names were allowed modernMode = true; + exp.canBeStaticField = false; } } @@ -186,15 +191,17 @@ public class _ObjectBuilderSettingEvaluator { char openParen = fetchOptionalChar("("); // Only the top-level expression can omit the "(...)" if (openParen == 0 && !topLevel) { - if (!optional) { - throw new _ObjectBuilderSettingEvaluationException("(", src, pos); + if (fetchedClassName.indexOf('.') != -1) { + exp.mustBeStaticField = true; + } else { + pos = startPos; + return VOID; } - pos = startPos; - return VOID; } if (openParen != 0) { fetchParameterListInto(exp); + exp.canBeStaticField = false; } return exp; @@ -683,8 +690,11 @@ public class _ObjectBuilderSettingEvaluator { addWithSimpleName(SHORTHANDS, Locale.class); SHORTHANDS.put("TimeZone", "freemarker.core._TimeZone"); + + // For accessing static fields: + addWithSimpleName(SHORTHANDS, Configuration.class); } - String fullClassName = (String) SHORTHANDS.get(className); + String fullClassName = SHORTHANDS.get(className); return fullClassName == null ? className : fullClassName; } @@ -842,20 +852,41 @@ public class _ObjectBuilderSettingEvaluator { private class BuilderCallExpression extends ExpressionWithParameters { private String className; + private boolean canBeStaticField; + private boolean mustBeStaticField; @Override Object eval() throws _ObjectBuilderSettingEvaluationException { + if (mustBeStaticField) { + if (!canBeStaticField) { + throw new BugException(); + } + return getStaticFieldValue(className); + } + Class cl; if (!modernMode) { try { - return ClassUtil.forName(className).newInstance(); - } catch (InstantiationException e) { - throw new LegacyExceptionWrapperSettingEvaluationExpression(e); - } catch (IllegalAccessException e) { - throw new LegacyExceptionWrapperSettingEvaluationExpression(e); - } catch (ClassNotFoundException e) { - throw new LegacyExceptionWrapperSettingEvaluationExpression(e); + try { + return ClassUtil.forName(className).newInstance(); + } catch (InstantiationException e) { + throw new LegacyExceptionWrapperSettingEvaluationExpression(e); + } catch (IllegalAccessException e) { + throw new LegacyExceptionWrapperSettingEvaluationExpression(e); + } catch (ClassNotFoundException e) { + throw new LegacyExceptionWrapperSettingEvaluationExpression(e); + } + } catch (LegacyExceptionWrapperSettingEvaluationExpression e) { + if (!canBeStaticField) { + throw e; + } + // Silently try to interpret className as static filed, throw the original exception if that fails. + try { + return getStaticFieldValue(className); + } catch (_ObjectBuilderSettingEvaluationException e2) { + throw e; + } } } @@ -868,8 +899,23 @@ public class _ObjectBuilderSettingEvaluator { try { cl = ClassUtil.forName(className); } catch (Exception e2) { + boolean failedToGetAsStaticField; + if (canBeStaticField) { + // Try to interpret className as static filed: + try { + return getStaticFieldValue(className); + } catch (_ObjectBuilderSettingEvaluationException e3) { + // Suppress it + failedToGetAsStaticField = true; + } + } else { + failedToGetAsStaticField = false; + } throw new _ObjectBuilderSettingEvaluationException( - "Failed to get class " + StringUtil.jQuote(className) + ".", e2); + "Failed to get class " + StringUtil.jQuote(className) + + (failedToGetAsStaticField ? " (also failed to resolve name as static field)" : "") + + ".", + e2); } } @@ -908,6 +954,53 @@ public class _ObjectBuilderSettingEvaluator { return result; } + private Object getStaticFieldValue(String dottedName) throws _ObjectBuilderSettingEvaluationException { + int lastDotIdx = dottedName.lastIndexOf('.'); + if (lastDotIdx == -1) { + throw new IllegalArgumentException(); + } + String className = shorthandToFullQualified(dottedName.substring(0, lastDotIdx)); + String fieldName = dottedName.substring(lastDotIdx + 1); + + Class<?> cl; + try { + cl = ClassUtil.forName(className); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to get field's parent class, " + StringUtil.jQuote(className) + ".", + e); + } + + Field field; + try { + field = cl.getField(fieldName); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to get field " + StringUtil.jQuote(fieldName) + " from class " + + StringUtil.jQuote(className) + ".", + e); + } + + if ((field.getModifiers() & Modifier.STATIC) == 0) { + throw new _ObjectBuilderSettingEvaluationException("Referred field isn't static: " + field); + } + if ((field.getModifiers() & Modifier.PUBLIC) == 0) { + throw new _ObjectBuilderSettingEvaluationException("Referred field isn't public: " + field); + } + + if (field.getName().equals(INSTANCE_FIELD_NAME)) { + throw new _ObjectBuilderSettingEvaluationException( + "The " + INSTANCE_FIELD_NAME + " field is only accessible through pseudo-constructor call: " + + className + "()"); + } + + try { + return field.get(null); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException("Failed to get field value: " + field, e); + } + } + private Object callConstructor(Class cl) throws _ObjectBuilderSettingEvaluationException { if (hasNoParameters()) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe799f5d/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index c561322..ce8137e 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -26131,12 +26131,12 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <listitem> <para>Fixes and improvements in the <quote>object - builder</quote> syntax used for configuring FreeMarker from - <literal>java.util.Properties</literal> (or other string-only - sources). This is not to be confused with the template language - syntax, which has nothing to do with the <quote>object - builder</quote> syntax we are writing about here. The - improvements are:</para> + builder</quote> mini-language used for configuring FreeMarker + from <literal>java.util.Properties</literal> or other + string-only sources (not used in templates). This is not to be + confused with the template language syntax, which has nothing to + do with the <quote>object builder</quote> syntax we are writing + about here. The improvements are:</para> <itemizedlist> <listitem> @@ -26196,6 +26196,12 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <literal>BigDecimal</literal> and <literal>bi</literal> for <literal>BigInteger</literal>.</para> </listitem> + + <listitem> + <para>Public static fields can be referred, like + <literal>com.example.MyClass.MY_CONSTANT</literal> or + <literal>Configuration.AUTO_DETECT_TAG_SYNTAX</literal>.</para> + </listitem> </itemizedlist> </listitem> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe799f5d/src/test/java/freemarker/core/ObjectBuilderSettingsTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/ObjectBuilderSettingsTest.java b/src/test/java/freemarker/core/ObjectBuilderSettingsTest.java index b708314..4276c7a 100644 --- a/src/test/java/freemarker/core/ObjectBuilderSettingsTest.java +++ b/src/test/java/freemarker/core/ObjectBuilderSettingsTest.java @@ -913,6 +913,46 @@ public class ObjectBuilderSettingsTest { "{ -1: -11, -0.5: -0.55, -0.1bd: -0.11bd }"); } + @Test + public void testStaticFields() throws Exception { + { + TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval( + "freemarker.core.ObjectBuilderSettingsTest$TestBean1(" + + "freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST, true)", + Object.class, false, _SettingEvaluationEnvironment.getCurrent()); + assertEquals(TestStaticFields.CONST, (int) res.i); + } + { + TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval( + "freemarker.core.ObjectBuilderSettingsTest$TestBean1(" + + "p2 = freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST)", + Object.class, false, _SettingEvaluationEnvironment.getCurrent()); + assertEquals(TestStaticFields.CONST, res.getP2()); + } + assertEqualsEvaled(123, "freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST"); + + // With shorthand class name: + assertEqualsEvaled(Configuration.AUTO_DETECT_TAG_SYNTAX, "Configuration.AUTO_DETECT_TAG_SYNTAX"); + + try { + _ObjectBuilderSettingEvaluator.eval( + "freemarker.core.ObjectBuilderSettingsTest$TestBean1(" + + "p2 = freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST())", + Object.class, false, _SettingEvaluationEnvironment.getCurrent()); + fail(); + } catch (_ObjectBuilderSettingEvaluationException e) { + assertThat(e.getMessage(), + containsString("freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST")); + } + try { + assertEqualsEvaled(123, "freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST()"); + fail(); + } catch (_ObjectBuilderSettingEvaluationException e) { + assertThat(e.getMessage(), + containsString("freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST")); + } + } + private void assertEqualsEvaled(Object expectedValue, String s) throws _ObjectBuilderSettingEvaluationException, ClassNotFoundException, InstantiationException, IllegalAccessException { @@ -1352,6 +1392,9 @@ public class ObjectBuilderSettingsTest { } + public static class TestStaticFields { + public static final int CONST = 123; + } public static class DummyArithmeticEngine extends ArithmeticEngine {
