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 <[email protected]> Authored: Thu Oct 26 19:40:27 2017 +0200 Committer: ddekany <[email protected]> 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; } }
