You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/10/26 17:41:11 UTC
incubator-freemarker git commit: - Refined `exp!` so that things like
`exp!?upperCase` mean `(exp!)?upperCase` instead of giving a syntax error.
Repository: incubator-freemarker
Updated Branches:
refs/heads/3 3fa49dd71 -> 1e1493549
- Refined `exp!` so that things like `exp!?upperCase` mean `(exp!)?upperCase` instead of giving a syntax error.
- Added missing hint in case someone tries to use the removed `?default` built-in.
- Removed `?if_exists`. The converter converts `foo?if_exists` to `foo!`
- The value of `missing!` now can be used as boolean `false`, and as a function that returns `null` and accepts
any arguments, and as a directive that does nothing and allows any arguments (not only as "", empty sequence,
and empty hash).
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/1e149354
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/1e149354
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/1e149354
Branch: refs/heads/3
Commit: 1e149354946e1fc158ead915606d387ebd23929d
Parents: 3fa49dd
Author: ddekany <dd...@apache.org>
Authored: Thu Oct 26 19:40:27 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Thu Oct 26 19:40:27 2017 +0200
----------------------------------------------------------------------
FM3-CHANGE-LOG.txt | 18 ++-
.../core/FM2ASTToFM3SourceConverter.java | 80 +++++++++---
.../converter/FM2ToFM3ConverterTest.java | 15 +++
.../freemarker/core/DefaultExpressionTest.java | 13 ++
.../core/ParsingErrorMessagesTest.java | 2 +
.../templates/existence-operators.ftl | 20 +--
.../templatesuite/templates/hashliteral.ftl | 4 +-
.../apache/freemarker/core/ASTExpBuiltIn.java | 8 +-
.../apache/freemarker/core/ASTExpDefault.java | 51 +++++++-
.../core/BuiltInsForExistenceHandling.java | 9 --
.../org/apache/freemarker/core/_EvalUtils.java | 6 +-
.../core/model/GeneralPurposeNothing.java | 115 -----------------
.../freemarker/core/model/TemplateModel.java | 8 +-
.../core/model/impl/DefaultObjectWrapper.java | 9 +-
freemarker-core/src/main/javacc/FTL.jj | 124 ++++++++++---------
15 files changed, 250 insertions(+), 232 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt
index 584ff7a..3d61c35 100644
--- a/FM3-CHANGE-LOG.txt
+++ b/FM3-CHANGE-LOG.txt
@@ -65,9 +65,12 @@ Major template language changes / features
by name. In FM3 that won't work anymore, as now a parameter is either strictly positional or strictly named.
- Operator for handing null/missing values were reworked:
- The right-side operator precedence of the `exp!defaultExp` (and `exp!`) operator is now the same precedence on
- both sides, which is lower as of `.`, but higher as of `+`. The converter takes care of cases where this would
- change the meaning of the expression (like `x!y+1` is converted to `x!(x+1)`.)
- - [TODO] Much more will happen here
+ both sides, which is lower as the precedence of `.`, but higher as the precedence of `+`. The converter takes
+ care of cases where this would change the meaning of the expression (like `x!y+1` is converted to `x!(x+1)`.)
+ - The value of `missing!` now can be used as boolean `false`, and as a function that returns `null` and accepts
+ any arguments, and as a directive that does nothing and allows any arguments (not only as "", empty sequence,
+ and empty hash).
+ - [TODO] Deeper changes are supposed to happen here later. (Some of the above changes will be meaningless then.)
Smaller template language changes
---------------------------------
@@ -98,7 +101,9 @@ Node: Changes already mentioned above aren't repeated here!
- Removed some long deprecated built-ins:
- `webSafe` (converted to `html`)
- `exists` (`foo?exists` is converted `foo??`)
- - `default` (`foo?default(bar)` is converted to `foo!bar`)
+ - `default` (`foo?default(bar)` is converted to `foo!bar`).
+ - `if_exists` and `ifExists` (`foo?if_exists` is converted to `foo!`). (There's a slight difference though that
+ the return value can be called as directive (which does nothing), while with `?ifExists` that wasn't possible.)
- Comma is now required between sequence literal items (such as `[a, b, c]`). It's not well known, but in FM2 the comma
could be omitted.
- #include has no "encoding" parameter anymore (as now only the Configuration is responsible ofr deciding the encoding)
@@ -447,7 +452,12 @@ Core / Models and Object wrapping
it's not able to list its keys, almost all `isEmpty()` implementations in FM2 were just dummies returning `false`.)
Note that `?hashContent` now returns `true` for `TemplateHashModel` that aren't also `TemplateHashModelEx2`-s,
based on the idea that for some `key` (which you may don't know) `get(key)` might returns something.
+ - `TemplateModel.NOTHING` was removed without replacement.
- BeanModel.keys() and values() are now final methods. Override BeanModel.keySet() and/or get(String) instead.
+- Methods that return void now return an empty string instead of `TemplateModel.NOTHING` (which was removed).
+ Thus, `${obj.voidReturningMethod()}` still works (it prints nothing, just as in FM2), but things like
+ `x + obj.voidReturningMethod()` now fail (unlike in FM2), as they are probably oversights. This all applies to
+ Java methods wrapped by the DefaultObjectWrapper (which, in FM3, is also used in place of the FM2 BeansWrapper).
Core / Template loading and caching
...................................
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
index 0861be0..f883a80 100644
--- a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
+++ b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
@@ -1825,7 +1825,7 @@ public class FM2ASTToFM3SourceConverter {
}
private void printExpMethodCall(MethodCall node) throws ConverterException {
- Expression callee = getParam(node, 0, ParameterRole.CALLEE, Expression.class);
+ Expression callee = getMethodCallCalleeExp(node);
printExp(callee);
if (callee instanceof BuiltIn
@@ -1845,6 +1845,10 @@ public class FM2ASTToFM3SourceConverter {
printWithParamsTrailingSkippedTokens(")", node, argCnt);
}
+ private Expression getMethodCallCalleeExp(MethodCall node) throws UnexpectedNodeContentException {
+ return getParam(node, 0, ParameterRole.CALLEE, Expression.class);
+ }
+
private void printExpBuiltIn(BuiltIn node) throws ConverterException {
Expression lho = getParam(node, 0, ParameterRole.LEFT_HAND_OPERAND, Expression.class);
String rho = getParam(node, 1, ParameterRole.RIGHT_HAND_OPERAND, String.class);
@@ -1862,22 +1866,50 @@ public class FM2ASTToFM3SourceConverter {
pos = printWSAndExpCommentsIfContainsComment(pos); // lho?< >exists
pos = getPositionAfterIdentifier(pos); // lho?<exists>
assertParamCount(node, 2);
+ } else if (rho.equals("if_exists") || rho.equals("ifExists")) {
+ // lho?if_exists -> lho!
+
+ ParentNodeRelation parentNodeRel = getParentNodeRelation(node);
+ boolean wholeExpNeedsParenthesis =
+ parentNodeRel.is(MethodCall.class, ParameterRole.CALLEE)
+ || parentNodeRel.is(DynamicKeyName.class, ParameterRole.LEFT_HAND_OPERAND)
+ || parentNodeRel.is(Dot.class, ParameterRole.LEFT_HAND_OPERAND);
+
+ if (wholeExpNeedsParenthesis) {
+ print("(");
+ }
+
+ // <lho>?if_exists
+ printExp(lho);
+ int pos = getEndPositionExclusive(lho);
+
+ pos = printWSAndExpCommentsIfContainsComment(pos); // lho< >?if_exists
+ pos = skipRequiredString(pos, "?"); // lho<?>if_exists
+ pos = printWSAndExpCommentsIfContainsComment(pos); // lho?< >if_exists
+ pos = getPositionAfterIdentifier(pos); // lho?<if_exists>
+ assertParamCount(node, 2);
+
+ print("!");
+
+ if (wholeExpNeedsParenthesis) {
+ print(")");
+ }
} else if (rho.equals("default")) {
// lho?default(exp) -> lho!exp
- TemplateObject parentNode = getParentNode(node);
- if (!(parentNode instanceof MethodCall)) {
+ ParentNodeRelation parentNodeRel = getParentNodeRelation(node);
+ if (!(parentNodeRel.is(MethodCall.class, ParameterRole.CALLEE))) {
throw new UnconvertableLegacyFeatureException(
"?default must be followed by a paramter list, like in ?default(1), "
+ "otherwise it has no equivalent in FreeMarker 3.",
node.getBeginLine(), node.getBeginColumn());
}
- MethodCall parentCall = (MethodCall) parentNode;
+ MethodCall parentCall = (MethodCall) parentNodeRel.parentNode;
// Sometimes parentheses must be added, e.g.:
// - Needed: `a?default(b).x` -> `(a!b).x`
// - Not needed: `a?default(b) + x` -> `a!b + x`
- TemplateObject grandParentNode = getParentNode(parentCall);
+ TemplateObject grandParentNode = getParentNodeRelation(parentCall).parentNode;
boolean wholeExpNeedsParenthesis = grandParentNode instanceof Expression
&& !needsParenthesisAsDefaultValue((Expression) grandParentNode)
&& !(grandParentNode instanceof ParentheticalExpression);
@@ -2684,20 +2716,39 @@ public class FM2ASTToFM3SourceConverter {
return src.substring(startPos, pos);
}
- private IdentityHashMap<TemplateObject, TemplateObject> parentsByChildrenNode = null;
+ private IdentityHashMap<TemplateObject, ParentNodeRelation> parentRelationsByChildrenNode = null;
private IdentityHashMap<TemplateObject, Object> parentsProcessed = null;
- private TemplateObject getParentNode(TemplateObject node) throws ConverterException {
- if (parentsByChildrenNode == null) {
- parentsByChildrenNode = new IdentityHashMap<>();
+ private static class ParentNodeRelation {
+ private final TemplateObject parentNode;
+ /** {@code null} if the node is not a child, but a parameter of the parent. */
+ private final Integer relationChildIndex;
+ /** {@code null} if the node is a parameter, but a child of the parent. */
+ private final ParameterRole relationParameterRole;
+
+ ParentNodeRelation(TemplateObject parentNode, Integer relationChildIndex,
+ ParameterRole relationParameterRole) {
+ this.parentNode = parentNode;
+ this.relationChildIndex = relationChildIndex;
+ this.relationParameterRole = relationParameterRole;
+ }
+
+ boolean is(Class<? extends TemplateObject> parentClass, ParameterRole paramRole) {
+ return parentClass.isInstance(parentNode) && relationParameterRole == paramRole;
+ }
+ }
+
+ private ParentNodeRelation getParentNodeRelation(TemplateObject node) throws ConverterException {
+ if (parentRelationsByChildrenNode == null) {
+ parentRelationsByChildrenNode = new IdentityHashMap<>();
parentsProcessed = new IdentityHashMap<>();
collectParentNodesOfChildren(template.getRootTreeNode());
}
- TemplateObject parent = parentsByChildrenNode.get(node);
- if (parent == null) {
+ ParentNodeRelation parentRelation = parentRelationsByChildrenNode.get(node);
+ if (parentRelation == null) {
throw new ConverterException("Can't find the parent node of a(n) " + node.getClass().getName() + " node.");
}
- return parent;
+ return parentRelation;
}
private void collectParentNodesOfChildren(TemplateObject parentNode) {
@@ -2712,7 +2763,7 @@ public class FM2ASTToFM3SourceConverter {
int childCnt = parentElement.getChildCount();
for (int i = 0; i < childCnt; i++) {
TemplateElement child = parentElement.getChild(i);
- parentsByChildrenNode.put(child, parentNode);
+ parentRelationsByChildrenNode.put(child, new ParentNodeRelation(parentNode, i, null));
collectParentNodesOfChildren(child);
}
}
@@ -2722,7 +2773,8 @@ public class FM2ASTToFM3SourceConverter {
Object paramValue = parentNode.getParameterValue(i);
if (paramValue instanceof TemplateObject) {
TemplateObject paramValueNode = (TemplateObject) paramValue;
- parentsByChildrenNode.put(paramValueNode, parentNode);
+ parentRelationsByChildrenNode.put(
+ paramValueNode, new ParentNodeRelation(parentNode, null, parentNode.getParameterRole(i)));
collectParentNodesOfChildren(paramValueNode);
}
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
index 4a2d0dd..048d534 100644
--- a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
+++ b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
@@ -520,9 +520,23 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
assertEquals(1, (Object) e.getRow());
assertEquals(14, (Object) e.getColumn());
}
+ try {
+ convert("${f(x?default)}");
+ fail();
+ } catch (UnconvertableLegacyFeatureException e) {
+ assertEquals(1, (Object) e.getRow());
+ assertEquals(5, (Object) e.getColumn());
+ }
assertConverted("${(s!d).a}", "${s?default(d).a}");
assertConverted("${(s!d1!d2)!a}", "${s?default(d1, d2)!a}");
assertConverted("${s!d + a}", "${s?default(d) + a}");
+
+ assertConverted("${s!}", "${s?if_exists}");
+ assertConverted("${s <#-- c1 --> <#-- c2 --> !}", "${s <#-- c1 --> ? <#-- c2 --> if_exists}");
+ assertConverted("${s!?c}", "${s?ifExists?c}");
+ assertConverted("${(s!).c}", "${s?ifExists.c}"); // Change to `s!.c` when the built-in variable syntax changes
+ assertConverted("${(s!)[c]}", "${s?ifExists[c]}");
+ assertConverted("${(s!)(c)}", "${s?ifExists(c)}");
}
@Test
@@ -536,6 +550,7 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
assertConvertedSame("${v!d?upperCase}");
assertConvertedSame("${v!}");
assertConvertedSame("${v!(d + 1)}");
+ assertConvertedSame("${v!?upperCase}");
assertConvertedSame("${(v!) + 'x'}");
assertConverted("${v!(+1)}", "${v!+1}");
assertConverted("${v!(-1)}", "${v!-1}");
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core-test/src/test/java/org/apache/freemarker/core/DefaultExpressionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/DefaultExpressionTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DefaultExpressionTest.java
index 7955869..e27d761 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/DefaultExpressionTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DefaultExpressionTest.java
@@ -109,5 +109,18 @@ public class DefaultExpressionTest extends TemplateTest {
+ "<@m a=x! b=y! c=z! />",
"[][][] [x][y][z] [][Y][]");
}
+
+ @Test
+ public void testDefaultNothing() throws Exception {
+ assertOutput("${missing!}", "");
+ assertOutput("<#if missing!>t<#else>f</#if>", "f");
+ assertOutput("${(missing!)(1, x=2)!'null'}", "null");
+ assertOutput("<@missing! 1 x=2>x</@>", "");
+ assertOutput("<#list xs! as x>x</#list>", "");
+ assertOutput("<#list xs! as k, v>x</#list>", "");
+ assertOutput("${xs!?length}", "0");
+ assertOutput("${(xs!)?length}", "0"); // same
+ assertOutput("${xs!?size}", "0");
+ }
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
index e788dd8..bf729ea 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
@@ -87,6 +87,8 @@ public class ParsingErrorMessagesTest {
assertErrorContains("${x?datetimeIfUnknown}", "The correct name is: dateTimeIfUnknown");
assertErrorContains("${x?datetime_if_unknown}", "The correct name is: dateTimeIfUnknown");
assertErrorContains("${x?exists}", "someExpression??");
+ assertErrorContains("${x?if_exists}", "someExpression!");
+ assertErrorContains("${x?default(1)}", "someExpression!defaultExpression");
}
@Test
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/existence-operators.ftl
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/existence-operators.ftl b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/existence-operators.ftl
index e1db7cf..5265e34 100644
--- a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/existence-operators.ftl
+++ b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/existence-operators.ftl
@@ -32,8 +32,8 @@
<@isNonFastIRE>${v}</@> <#-- To check that it isn't an IRE.FAST_INSTANCE -->
<@assertEquals actual=v?? expected=false />
<@assertEquals actual=(v)?? expected=false />
-<@assertEquals actual=v?ifExists expected='' />
-<@assertEquals actual=(v)?ifExists expected='' />
+<@assertEquals actual=v! expected='' />
+<@assertEquals actual=(v)! expected='' />
<@assertEquals actual=v?hasContent expected=false />
<@assertEquals actual=(v)?hasContent expected=false />
@@ -52,8 +52,8 @@
<@assertEquals actual=(v)!'-' expected=v />
<@assert v?? />
<@assert (v)?? />
- <@assertEquals actual=v?ifExists expected=v />
- <@assertEquals actual=(v)?ifExists expected=v />
+ <@assertEquals actual=v! expected=v />
+ <@assertEquals actual=(v)! expected=v />
<@assert v?hasContent />
<@assert (v)?hasContent />
</#list>
@@ -69,8 +69,8 @@
<@assertEquals actual=(u.v)!'-' expected='-' />
<@isIRE>${u.v??}</@>
<@assertEquals actual=(u.v)?? expected=false />
-<@isIRE>${u.v?ifExists}</@>
-<@assertEquals actual=(u.v)?ifExists expected='' />
+<@isIRE>${u.v!}</@>
+<@assertEquals actual=(u.v)! expected='' />
<@isIRE>${u.v?hasContent}</@>
<@assertEquals actual=(u.v)?hasContent expected=false />
@@ -83,8 +83,8 @@
<@assertEquals actual=(u.v)!'-' expected='-' />
<@assertEquals actual=u.v?? expected=false />
<@assertEquals actual=(u.v)?? expected=false />
-<@assertEquals actual=u.v?ifExists expected='' />
-<@assertEquals actual=(u.v)?ifExists expected='' />
+<@assertEquals actual=u.v! expected='' />
+<@assertEquals actual=(u.v)! expected='' />
<@assertEquals actual=u.v?hasContent expected=false />
<@assertEquals actual=(u.v)?hasContent expected=false />
@@ -97,8 +97,8 @@
<@assertEquals actual=(u.v)!'-' expected='V' />
<@assert u.v?? />
<@assert (u.v)?? />
-<@assertEquals actual=u.v?ifExists expected='V' />
-<@assertEquals actual=(u.v)?ifExists expected='V' />
+<@assertEquals actual=u.v! expected='V' />
+<@assertEquals actual=(u.v)! expected='V' />
<@assert u.v?hasContent />
<@assert (u.v)?hasContent />
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/hashliteral.ftl
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/hashliteral.ftl b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/hashliteral.ftl
index 463e41b..3bfa079 100644
--- a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/hashliteral.ftl
+++ b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/hashliteral.ftl
@@ -62,7 +62,7 @@ ${test.bar}
${test.test1}
${test.test45}
-${test.hello?ifExists}
+${test.hello!}
${test.bar}
${test.hash}
@@ -72,7 +72,7 @@ ${test.newhash.temp}
<p>Pathological case: zero item hash:</p>
<#assign test = {}>
-${test.test1?ifExists}
+${test.test1!}
<p>Hash of number literals:</p>
<#assign test = {"1" : 2}>
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
index 817dff5..8151568 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
@@ -113,7 +113,6 @@ abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
putBI("hasContent", new BuiltInsForExistenceHandling.has_contentBI());
putBI("hasNext", new BuiltInsForNestedContentParameters.has_nextBI());
putBI("html", new BuiltInsForStringsEncoding.htmlBI());
- putBI("ifExists", new BuiltInsForExistenceHandling.if_existsBI());
putBI("index", new BuiltInsForNestedContentParameters.indexBI());
putBI("indexOf", new BuiltInsForStringsBasic.index_ofBI(false));
putBI("int", new intBI());
@@ -329,6 +328,13 @@ abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
sb.append("\nThe correct name is: ").append(correctedKey);
} else if (key.equals("exists")) {
sb.append("\nUse someExpression?? instead of someExpression?exists.");
+ } else if (key.equals("ifExists") || key.equals("if_exists")) {
+ sb.append("\nUse someExpression! instead of someExpression?" + key + ".");
+ } else if (key.equals("default")) {
+ sb.append("\nUse someExpression!defaultExpression instead of "
+ + "someExpression?default(defaultExpression), or someExpression!(defaultExpression) if "
+ + "defaultExpression contains operators that have lower precedence than the default value "
+ + "operator (!). Also note that instead of x?default(y, z), you can write x!y!z.");
} else {
sb.append(
"\nHelp (latest version): http://freemarker.org/docs/ref_builtins.html; "
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
index e0aee94..04c1afc 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
@@ -20,7 +20,14 @@
package org.apache.freemarker.core;
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.ArgumentArrayLayout;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateFunctionModel;
import org.apache.freemarker.core.model.TemplateHashModelEx;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateModelIterator;
@@ -30,14 +37,25 @@ import org.apache.freemarker.core.model.TemplateStringModel;
/** {@code exp!defExp}, {@code (exp)!defExp} and the same two with {@code (exp)!}. */
class ASTExpDefault extends ASTExpression {
- static private class EmptyStringAndSequenceAndHash
- implements TemplateStringModel, TemplateSequenceModel, TemplateHashModelEx {
+ static private class EmptyStringAndSequenceAndHashAndFalse
+ implements TemplateStringModel, TemplateBooleanModel,
+ TemplateFunctionModel, TemplateDirectiveModel,
+ TemplateSequenceModel, TemplateHashModelEx {
+
+ private static final ArgumentArrayLayout ALLOW_ALL_ARG_LAYOUT
+ = ArgumentArrayLayout.create(0, true, null, true);
+
@Override
public String getAsString() {
return "";
}
@Override
+ public boolean getAsBoolean() throws TemplateException {
+ return false;
+ }
+
+ @Override
public TemplateModel get(int i) {
return null;
}
@@ -86,9 +104,36 @@ class ASTExpDefault extends ASTExpression {
public TemplateHashModelEx.KeyValuePairIterator keyValuePairIterator() throws TemplateException {
return TemplateHashModelEx.KeyValuePairIterator.EMPTY_KEY_VALUE_PAIR_ITERATOR;
}
+
+ @Override
+ public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env)
+ throws TemplateException {
+ return null;
+ }
+
+ @Override
+ public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
+ return ALLOW_ALL_ARG_LAYOUT;
+ }
+
+ @Override
+ public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env)
+ throws TemplateException, IOException {
+ // Do nothing
+ }
+
+ @Override
+ public boolean isNestedContentSupported() {
+ return true;
+ }
+
+ @Override
+ public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
+ return ALLOW_ALL_ARG_LAYOUT;
+ }
}
- private static final TemplateModel EMPTY_STRING_AND_SEQUENCE_AND_HASH = new EmptyStringAndSequenceAndHash();
+ private static final TemplateModel EMPTY_STRING_AND_SEQUENCE_AND_HASH = new EmptyStringAndSequenceAndHashAndFalse();
private final ASTExpression lho, rho;
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
index 2e6a816..1ba7ffc 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
@@ -64,14 +64,5 @@ class BuiltInsForExistenceHandling {
return _eval(env) == TemplateBooleanModel.TRUE;
}
}
-
- static class if_existsBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
- @Override
- TemplateModel _eval(Environment env)
- throws TemplateException {
- TemplateModel model = evalMaybeNonexistentTarget(env);
- return model == null ? TemplateModel.NOTHING : model;
- }
- }
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtils.java
index 1b59d1e..525ed82 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtils.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtils.java
@@ -455,8 +455,10 @@ public class _EvalUtils {
env);
}
} else if (tm instanceof TemplateBooleanModel) {
- // [FM3] This should be before TemplateStringModel, but automatic boolean-to-string is only non-error since
- // 2.3.20, so to keep backward compatibility we couldn't insert this before TemplateStringModel.
+ // TODO [FM3] It would be more logical if it's before TemplateStringModel (number etc. are before it as
+ // well). But currently, in FM3, `exp!` returns a multi-typed value that's also a boolean `false`, and so
+ // `${missing!}` wouldn't print `""` anymore if we reorder these. But, if and when `null` handling is
+ // reworked ("checked nulls"), this problem should go away, and so we should move this.
boolean booleanValue = ((TemplateBooleanModel) tm).getAsBoolean();
return env.formatBoolean(booleanValue, false);
} else {
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java
deleted file mode 100644
index 4fbf496..0000000
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java
+++ /dev/null
@@ -1,115 +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.core.model;
-
-import org.apache.freemarker.core.CallPlace;
-import org.apache.freemarker.core.Environment;
-import org.apache.freemarker.core.TemplateException;
-
-/**
- * Singleton object representing nothing, used by ?if_exists built-in.
- * It is meant to be interpreted in the most sensible way possible in various contexts.
- * This can be returned to avoid exceptions.
- */
-// TODO [FM3] As `exp!` doesn't use this, are the other use cases necessary and correct?
-final class GeneralPurposeNothing
-implements TemplateBooleanModel, TemplateStringModel, TemplateSequenceModel, TemplateHashModelEx,
- TemplateFunctionModel {
-
- static final TemplateModel INSTANCE = new GeneralPurposeNothing();
-
- private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(
- 0, true,
- null, true);
-
- private GeneralPurposeNothing() {
- }
-
- @Override
- public String getAsString() {
- return "";
- }
-
- @Override
- public boolean getAsBoolean() {
- return false;
- }
-
- @Override
- public int getHashSize() throws TemplateException {
- return 0;
- }
-
- @Override
- public boolean isEmptyHash() {
- return true;
- }
-
- @Override
- public int getCollectionSize() {
- return 0;
- }
-
- @Override
- public boolean isEmptyCollection() throws TemplateException {
- return true;
- }
-
- @Override
- public TemplateModel get(int i) throws TemplateException {
- return null;
- }
-
- @Override
- public TemplateModel get(String key) {
- return null;
- }
-
- @Override
- public TemplateModelIterator iterator() throws TemplateException {
- return TemplateModelIterator.EMPTY_ITERATOR;
- }
-
- @Override
- public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws TemplateException {
- return null;
- }
-
- @Override
- public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
- return ARGS_LAYOUT;
- }
-
- @Override
- public TemplateCollectionModel keys() {
- return TemplateCollectionModel.EMPTY_COLLECTION;
- }
-
- @Override
- public TemplateCollectionModel values() {
- return TemplateCollectionModel.EMPTY_COLLECTION;
- }
-
- @Override
- public TemplateHashModelEx.KeyValuePairIterator keyValuePairIterator() throws TemplateException {
- return EmptyKeyValuePairIterator.EMPTY_KEY_VALUE_PAIR_ITERATOR;
- }
-
-}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
index b4247c7..f939f96 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
@@ -45,11 +45,5 @@ import org.apache.freemarker.core.util.TemplateLanguageUtils;
* @see TemplateLanguageUtils#getTypeDescription(TemplateModel)
*/
public interface TemplateModel {
-
- /**
- * A general-purpose object to represent nothing. It acts as
- * an empty string, false, empty sequence, empty hash, and
- * null-returning method model.
- */
- TemplateModel NOTHING = GeneralPurposeNothing.INSTANCE;
+ // Empty
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
index 4527e1a..45d00c2 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
@@ -57,16 +57,16 @@ import org.apache.freemarker.core.model.ObjectWrappingException;
import org.apache.freemarker.core.model.RichObjectWrapper;
import org.apache.freemarker.core.model.TemplateBooleanModel;
import org.apache.freemarker.core.model.TemplateCollectionModel;
-import org.apache.freemarker.core.model.TemplateIterableModel;
import org.apache.freemarker.core.model.TemplateDateModel;
import org.apache.freemarker.core.model.TemplateFunctionModel;
import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateIterableModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateModelAdapter;
import org.apache.freemarker.core.model.TemplateModelIterator;
import org.apache.freemarker.core.model.TemplateNumberModel;
-import org.apache.freemarker.core.model.TemplateStringModel;
import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
import org.apache.freemarker.core.model.WrapperTemplateModel;
import org.apache.freemarker.core.util.BugException;
import org.apache.freemarker.core.util.CommonBuilder;
@@ -992,7 +992,7 @@ public class DefaultObjectWrapper implements RichObjectWrapper {
/**
* Invokes the specified method, wrapping the return value. The specialty
* of this method is that if the return value is null, and the return type
- * of the invoked method is void, {@link TemplateModel#NOTHING} is returned.
+ * of the invoked method is void, an empty string is returned.
* @param object the object to invoke the method on
* @param method the method to invoke
* @param args the arguments to the method
@@ -1012,7 +1012,7 @@ public class DefaultObjectWrapper implements RichObjectWrapper {
Object retval = method.invoke(object, args);
return
method.getReturnType() == void.class
- ? TemplateModel.NOTHING
+ ? TemplateStringModel.EMPTY_STRING
: getOuterIdentity().wrap(retval);
}
@@ -1487,6 +1487,7 @@ public class DefaultObjectWrapper implements RichObjectWrapper {
* @see #hashCode()
* @see #cloneForCacheKey()
*/
+ @Override
public boolean equals(Object thatObj) {
if (this == thatObj) return true;
if (thatObj == null) return false;
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1e149354/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj b/freemarker-core/src/main/javacc/FTL.jj
index dedf283..0479dbc 100644
--- a/freemarker-core/src/main/javacc/FTL.jj
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -1382,41 +1382,30 @@ ASTExpression ASTExpression() :
}
/**
- * PrimaryExpression followed by optional `!defaultExp` or `!`.
- * Note: Because x!y!z means x!(y!z), this consumes a whole chain of !defaultExp-s.
+ * Deals with the operators that have the highest precedence. Also deals with `exp!default` and `exp!`, due to parser
+ * tricks needed because of the last.
*/
-ASTExpression PrimaryWithDefaultExpression() :
+ASTExpression HighestPrecedenceExpression() :
{
- Token exclamTk;
- ASTExpression priExp, defaultExp;
+ ASTExpression exp;
}
{
- priExp = PrimaryExpression()
- [
- exclamTk = <EXCLAM>
- (
- LOOKAHEAD(<ID><ASSIGNMENT_EQUALS>) { /* Do not consume */ }
- |
- [
- LOOKAHEAD(PrimaryWithDefaultExpression())
- defaultExp = PrimaryWithDefaultExpression()
- {
- ASTExpDefault result = new ASTExpDefault(priExp, defaultExp);
- result.setLocation(template, priExp, defaultExp);
- return result;
- }
- ]
- )
- // If we reach this, we had no defaultExp.
- {
- ASTExpDefault result = new ASTExpDefault(priExp, null);
- result.setLocation(template, priExp, exclamTk);
- return result;
- }
- ]
- // If we reach this, we had no <EXCALM>.
+ exp = AtomicExpression()
+ (
+ exp = DotVariable(exp)
+ |
+ exp = DynamicKey(exp)
+ |
+ exp = FunctionCall(exp)
+ |
+ exp = ASTExpBuiltIn(exp)
+ |
+ exp = ASTExpDefault(exp) // See precedence notes at the product
+ |
+ exp = Exists(exp)
+ )*
{
- return priExp;
+ return exp;
}
}
@@ -1425,7 +1414,7 @@ ASTExpression PrimaryWithDefaultExpression() :
* or a possibly more complex expression bounded
* by parentheses.
*/
-ASTExpression PrimaryExpression() :
+ASTExpression AtomicExpression() :
{
ASTExpression exp;
}
@@ -1447,10 +1436,6 @@ ASTExpression PrimaryExpression() :
|
exp = ASTExpBuiltInVariable()
)
- (
- LOOKAHEAD(<DOT> | <OPEN_BRACKET> |<OPEN_PAREN> | <BUILT_IN> | <EXISTS>)
- exp = AddSubExpression(exp)
- )*
{
return exp;
}
@@ -1487,7 +1472,7 @@ ASTExpression UnaryPrefixExpression() :
|
result = ASTExpNot()
|
- result = PrimaryWithDefaultExpression()
+ result = HighestPrecedenceExpression()
)
{
return result;
@@ -1504,7 +1489,7 @@ ASTExpression ASTExpNot() :
(
t = <EXCLAM> { nots.add(t); }
)+
- exp = PrimaryWithDefaultExpression()
+ exp = HighestPrecedenceExpression()
{
for (int i = 0; i < nots.size(); i++) {
result = new ASTExpNot(exp);
@@ -1528,7 +1513,7 @@ ASTExpression ASTExpNegateOrPlus() :
|
t = <MINUS> { isMinus = true; }
)
- exp = PrimaryWithDefaultExpression()
+ exp = HighestPrecedenceExpression()
{
result = new ASTExpNegateOrPlus(exp, isMinus);
result.setLocation(template, t, exp);
@@ -1866,41 +1851,58 @@ ASTExpBuiltInVariable ASTExpBuiltInVariable() :
}
}
-/**
- * Production that builds up an expression
- * using the dot or dynamic key name
- * or the args list if this is a method invocation.
- */
-ASTExpression AddSubExpression(ASTExpression exp) :
+ASTExpression Exists(ASTExpression exp) :
{
- ASTExpression result = null;
+ Token t;
}
{
- (
- result = DotVariable(exp)
- |
- result = DynamicKey(exp)
- |
- result = FunctionCall(exp)
- |
- result = ASTExpBuiltIn(exp)
- |
- result = Exists(exp)
- )
+ t = <EXISTS>
{
+ ASTExpExists result = new ASTExpExists(exp);
+ result.setLocation(template, exp, t);
return result;
}
}
-ASTExpression Exists(ASTExpression exp) :
+
+/**
+ * This stands for `exp!defaultExp` and `exp!`. Note `!` has lower precedence than `.`, `?` etc, i.e., it's not a
+ * HighestPrecedenceExpression. So `a.b!c.d` means `a.b!(c.d)`, not `(a.b!c).d`. But, parsing is tricky because
+ * the right operand is optional, so `exp!?foo` should mean `(exp!)?foo`, but if we "split" the expression at the `!`
+ * before descending into HighestPrecedenceExpression (the normal way in a recursive descent parsers), then later we end
+ * up with a `?foo` expression, which is invalid in itself. Thus, we process `!` as if it had the as high precedence as
+ * `.` etc have, utilizing that those operators are left-associative (so the result is the same with the wrong
+ * precedence). Then, next trick, we consume the right-hand operand as HighestPrecedenceExpression. Thus, if `!`
+ * is not followed by a HighestPrecedenceExpression, but something like `?foo`, the parser will ascend back into the
+ * loop inside HighestPrecedenceExpression, which can consume it as it only expects an operator (like `?`) at this
+ * point, not a left hand operand. If, on the other hand, `!` is followed by a HighestPrecedenceExpression, then we
+ * consume it, stealing it from the HighestPrecedenceExpression that we will ascend back into, thus, the `!` behaves
+ * according its lower precedence on the right side (and also, note that `!` is right-associative).
+ */
+ASTExpression ASTExpDefault(ASTExpression exp) :
{
- Token t;
+ Token exclamTk;
+ ASTExpression defaultExp;
}
{
- t = <EXISTS>
+ exclamTk = <EXCLAM>
+ (
+ LOOKAHEAD(<ID><ASSIGNMENT_EQUALS>) { /* Do not consume */ }
+ |
+ [
+ LOOKAHEAD(HighestPrecedenceExpression())
+ defaultExp = HighestPrecedenceExpression()
+ {
+ ASTExpDefault result = new ASTExpDefault(exp, defaultExp);
+ result.setLocation(template, exp, defaultExp);
+ return result;
+ }
+ ]
+ )
+ // If we reach this, we had no defaultExp.
{
- ASTExpExists result = new ASTExpExists(exp);
- result.setLocation(template, exp, t);
+ ASTExpDefault result = new ASTExpDefault(exp, null);
+ result.setLocation(template, exp, exclamTk);
return result;
}
}