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;
     }
 }