Repository: incubator-freemarker
Updated Branches:
  refs/heads/3 d357910a5 -> f725d36fa


Getting rid of `?exists` (`foo?exists` is converted `foo??`) and `?default` 
(`foo?default(bar)` is converted to `foo!bar`). Fixing the right-side 
precedence of the `exp!defaultExp` (and `exp!`) operator: now it has 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)`.)


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/f725d36f
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/f725d36f
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/f725d36f

Branch: refs/heads/3
Commit: f725d36faae50f8efaef6335efaa4bbf7e002061
Parents: d357910
Author: ddekany <[email protected]>
Authored: Thu Oct 26 00:32:12 2017 +0200
Committer: ddekany <[email protected]>
Committed: Thu Oct 26 00:32:12 2017 +0200

----------------------------------------------------------------------
 FM3-CHANGE-LOG.txt                              |   3 +-
 .../core/FM2ASTToFM3SourceConverter.java        | 185 ++++++++++++++++++-
 .../freemarker/converter/FM2ToFM3Converter.java |  36 +++-
 .../converter/FM2ToFM3ConverterCLI.java         |   9 +
 .../freemarker/converter/_ConverterUtils.java   |  10 +
 .../converter/FM2ToFM3ConverterTest.java        |  47 ++++-
 .../freemarker/core/DefaultExpressionTest.java  | 113 +++++++++++
 .../src/test/resources/__conversion-markers.txt |   0
 .../templates/existence-operators.ftl           |  24 +--
 .../core/templatesuite/templates/nested.ftl     |   4 +-
 .../templates/output-encoding1.ftl              |   4 +-
 .../templates/output-encoding2.ftl              |   4 +-
 .../templates/output-encoding3.ftl              |   4 +-
 .../core/templatesuite/templates/var-layers.ftl |   2 +-
 .../templatesuite/templates/varlayers_lib.ftl   |   2 +-
 .../apache/freemarker/core/ASTExpBuiltIn.java   |   3 +-
 .../core/BuiltInsForExistenceHandling.java      |  56 ------
 .../core/model/impl/DefaultObjectWrapper.java   |   6 +-
 .../freemarker/core/model/impl/EnumModels.java  |   2 +-
 freemarker-core/src/main/javacc/FTL.jj          |  86 +++++----
 .../templatesuite/templates/default-xmlns.ftl   |   8 +-
 .../dom/templatesuite/templates/xmlns5.ftl      |  16 +-
 22 files changed, 478 insertions(+), 146 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt
index d0f3821..4ff7b9c 100644
--- a/FM3-CHANGE-LOG.txt
+++ b/FM3-CHANGE-LOG.txt
@@ -92,7 +92,8 @@ Node: Changes already mentioned above aren't repeated here!
   invocation of the function or macro.
 - Removed some long deprecated built-ins:
   - `webSafe` (converted to `html`)
-  - `exists` (converted to the `??` operator)
+  - `exists` (`foo?exists` is converted `foo??`)
+  - `default` (`foo?default(bar)` is converted to `foo!bar`) 
 - 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)

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 cb2ae18..0861be0 100644
--- 
a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
+++ 
b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -1501,12 +1502,38 @@ public class FM2ASTToFM3SourceConverter {
             Expression lho = getParam(node, 0, 
ParameterRole.LEFT_HAND_OPERAND, Expression.class);
             printExp(lho);
             printParameterSeparatorSource(lho, rho);
+            boolean needParentheses = needsParenthesisAsDefaultValue(rho);
+            if (needParentheses) {
+                print('(');
+            }
             printNode(rho);
+            if (needParentheses) {
+                print(')');
+            }
         } else {
             printPostfixOperator(node, "!");
         }
     }
 
+    /**
+     * Tells if in `exp!defaultExp` we need parentheses around `defultExp`.
+     */
+    private boolean needsParenthesisAsDefaultValue(Expression rho) {
+        return !(rho instanceof NumberLiteral)
+        && !(rho instanceof StringLiteral)
+        && !(rho instanceof BooleanLiteral)
+        && !(rho instanceof ListLiteral)
+        && !(rho instanceof HashLiteral)
+        && !(rho instanceof Identifier)
+        && !(rho instanceof Dot)
+        && !(rho instanceof DynamicKeyName)
+        && !(rho instanceof MethodCall)
+        && !(rho instanceof BuiltIn)
+        && !(rho instanceof DefaultToExpression)
+        && !(rho instanceof ExistsExpression)
+        && !(rho instanceof ParentheticalExpression);
+    }
+
     private void printExpNot(NotExpression node) throws ConverterException {
         printWithParamsLeadingSkippedTokens("!", node);
         printExp(getOnlyParam(node, ParameterRole.RIGHT_HAND_OPERAND, 
Expression.class));
@@ -1800,11 +1827,17 @@ public class FM2ASTToFM3SourceConverter {
     private void printExpMethodCall(MethodCall node) throws ConverterException 
{
         Expression callee = getParam(node, 0, ParameterRole.CALLEE, 
Expression.class);
         printExp(callee);
-
+        
+        if (callee instanceof BuiltIn
+                && getParam(callee, 1, ParameterRole.RIGHT_HAND_OPERAND, 
String.class).equals("default")) {
+            // ?default(defExp) handles printing its own parameter lists, as 
it will be converted to exp!defExp.
+            return;
+        }
+        
         Expression prevParam = callee;
         int argCnt = node.getParameterCount() - 1;
         for (int argIdx = 0; argIdx < argCnt; argIdx++) {
-            Expression argExp = getParam(node, argIdx + 1, 
ParameterRole.ARGUMENT_VALUE, Expression.class);
+            Expression argExp = getParam(node, 1 + argIdx, 
ParameterRole.ARGUMENT_VALUE, Expression.class);
             printParameterSeparatorSource(prevParam, argExp);
             printExp(argExp);
             prevParam = argExp;
@@ -1816,20 +1849,112 @@ public class FM2ASTToFM3SourceConverter {
         Expression lho = getParam(node, 0, ParameterRole.LEFT_HAND_OPERAND, 
Expression.class);
         String rho = getParam(node, 1, ParameterRole.RIGHT_HAND_OPERAND, 
String.class);
 
-        // <lho>?biName
-        printExp(lho);
-        int pos = getEndPositionExclusive(lho);
-        
         if (rho.equals("exists")) {
             // lho?exists -> lho??
 
+            // <lho>?exists
+            printExp(lho);
+            int pos = getEndPositionExclusive(lho);
+            
             pos = printWSAndExpCommentsIfContainsComment(pos); // lho< >?exists
             pos = skipRequiredString(pos, "?"); // lho<?>exists
             print("??");
             pos = printWSAndExpCommentsIfContainsComment(pos); // lho?< >exists
             pos = getPositionAfterIdentifier(pos); // lho?<exists>
             assertParamCount(node, 2);
+        } else if (rho.equals("default")) {
+            // lho?default(exp) -> lho!exp
+            
+            TemplateObject parentNode = getParentNode(node);
+            if (!(parentNode instanceof MethodCall)) {
+                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;
+
+            // 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);
+            boolean wholeExpNeedsParenthesis = grandParentNode instanceof 
Expression
+                    && !needsParenthesisAsDefaultValue((Expression) 
grandParentNode)
+                    && !(grandParentNode instanceof ParentheticalExpression);
+            if (wholeExpNeedsParenthesis) {
+                print("(");
+            }
+            
+            // <lho>?default(exp)
+            printExp(lho);
+            int pos = getEndPositionExclusive(lho);
+            
+            pos = printWSAndExpCommentsIfContainsComment(pos); // lho< 
>?default(exp)
+            pos = skipRequiredString(pos, "?"); // lho<?>default(exp)
+            pos = printWSAndExpCommentsIfContainsComment(pos); // lho?< 
>default(exp)
+            pos = getPositionAfterIdentifier(pos); // lho?<default>(exp)
+            // The parameter list is handled elsewhere, as that call is the 
parent expression.
+            assertParamCount(node, 2);
+            
+            pos = printWSAndExpCommentsIfContainsComment(pos); // lho?default< 
>(exp1, expN)
+            pos = skipRequiredString(pos, "("); // lho?default<(>exp1, exp2, 
expN)
+            
+            int argCnt = parentCall.getParameterCount() - 1;
+            String sepBeforeLastComma = "";
+            for (int argIdx = 0; argIdx < argCnt; argIdx++) {
+                String sep = readWSAndExpComments(pos); // lho?default(< 
>exp1,< >expN)
+                if (!_ConverterUtils.isWhitespaceOnly(sep)) {
+                    // exp?def(<#-- c -->d) -> exp!<#-- c -->d
+                    print('!');
+                    printWithConvertedExpComments(sep);
+                } else if (_ConverterUtils.containsLineBreak(sep)) {
+                    // exp?def(\n\td) -> exp\n\t!d
+                    print(sep);
+                    print('!');
+                } else {
+                    if (!sep.isEmpty() && sepBeforeLastComma.length() > 0
+                            && 
!Character.isWhitespace(sepBeforeLastComma.charAt(sepBeforeLastComma.length() - 
1))) {
+                        // exp?def(d1 <#-- c -->, d2) -> exp!d1 <#-- c --> !d2 
                   
+                        print(' ');
+                    }
+                    print('!');
+                }
+                
+                // lho?default(<exp1>, <expN>)
+                Expression argExp = getParam(parentCall, 1 + argIdx, 
ParameterRole.ARGUMENT_VALUE, Expression.class);
+                boolean argValueNeedsParenthesis = 
needsParenthesisAsDefaultValue(argExp);
+                if (argValueNeedsParenthesis) {
+                    print('(');
+                }
+                printExp(argExp);
+                if (argValueNeedsParenthesis) {
+                    print(')');
+                }
+                
+                // lho?default(exp1< >, expN< >)
+                pos = getEndPositionExclusive(argExp);
+                sep = readWSAndExpComments(pos);
+                pos += sep.length();
+                sepBeforeLastComma = sep;
+                if (!_ConverterUtils.isWhitespaceOnly(sep) || 
_ConverterUtils.containsLineBreak(sep)) {
+                    printWithConvertedExpComments(sep);
+                }
+                
+                // lho?default(exp1<,> expN< >)
+                if (argIdx != argCnt - 1) {
+                    pos = skipRequiredString(pos, ",");
+                } else {
+                    printWSAndExpCommentsIfContainsComment(pos);
+                }
+            }
+            if (wholeExpNeedsParenthesis) {
+                print(")");
+            }
         } else {
+            // <lho>?biName
+            printExp(lho);
+            int pos = getEndPositionExclusive(lho);
+            
             // lho<?>biName
             pos = printSeparatorAndWSAndExpComments(pos, "?");
     
@@ -2470,6 +2595,10 @@ public class FM2ASTToFM3SourceConverter {
         return pos + s.length();
     }
 
+    private int skipOptionalString(int pos, String s) throws 
ConverterException {
+        return src.startsWith(s, pos) ? pos + s.length() : pos;
+    }
+    
     private int getPositionAfterIdentifier(int startPos) throws 
ConverterException {
         return getPositionAfterIdentifier(startPos, false);
     }
@@ -2554,6 +2683,50 @@ public class FM2ASTToFM3SourceConverter {
         }
         return src.substring(startPos, pos);
     }
+    
+    private IdentityHashMap<TemplateObject, TemplateObject> 
parentsByChildrenNode = null;
+    private IdentityHashMap<TemplateObject, Object> parentsProcessed = null;
+    
+    private TemplateObject getParentNode(TemplateObject node) throws 
ConverterException {
+        if (parentsByChildrenNode == null) {
+            parentsByChildrenNode = new IdentityHashMap<>();
+            parentsProcessed = new IdentityHashMap<>();
+            collectParentNodesOfChildren(template.getRootTreeNode());
+        }
+        TemplateObject parent = parentsByChildrenNode.get(node);
+        if (parent == null) {
+            throw new ConverterException("Can't find the parent node of a(n) " 
+ node.getClass().getName() + " node.");
+        }
+        return parent;
+    }
+
+    private void collectParentNodesOfChildren(TemplateObject parentNode) {
+        // I don't think there can be dependency loops, but to be sure we 
handle them:
+        if (parentsProcessed.containsKey(parentNode)) {
+            return;
+        }
+        parentsProcessed.put(parentNode, null);
+        
+        if (parentNode instanceof TemplateElement) {
+            TemplateElement parentElement = (TemplateElement) parentNode; 
+            int childCnt = parentElement.getChildCount();
+            for (int i = 0; i < childCnt; i++) {
+                TemplateElement child = parentElement.getChild(i);
+                parentsByChildrenNode.put(child, parentNode);
+                collectParentNodesOfChildren(child);
+            }
+        }
+        
+        int paramCnt = parentNode.getParameterCount();
+        for (int i = 0; i < paramCnt; i++) {
+            Object paramValue = parentNode.getParameterValue(i);
+            if (paramValue instanceof TemplateObject) {
+                TemplateObject paramValueNode = (TemplateObject) paramValue;
+                parentsByChildrenNode.put(paramValueNode, parentNode);
+                collectParentNodesOfChildren(paramValueNode);
+            }
+        }
+    }
 
     /**
      * Because FM2 has this glitch where tags starting wit {@code <} can be 
closed with an unparied {@code ]}, we

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
----------------------------------------------------------------------
diff --git 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
index 3ba7a02..982ff63 100644
--- 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
+++ 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
@@ -24,7 +24,10 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.regex.Pattern;
 
+import org.apache.freemarker.converter.ConversionMarkers.Type;
 import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.ImmutableMap;
 
@@ -41,6 +44,7 @@ import freemarker.core.JSONOutputFormat;
 import freemarker.core.JavaScriptOutputFormat;
 import freemarker.core.MarkupOutputFormat;
 import freemarker.core.OutputFormat;
+import freemarker.core.ParseException;
 import freemarker.core.PlainTextOutputFormat;
 import freemarker.core.RTFOutputFormat;
 import freemarker.core.UndefinedOutputFormat;
@@ -80,12 +84,15 @@ public class FM2ToFM3Converter extends Converter {
                     .put("fm", "fm3")
                     .build();
 
+    private static final Logger LOG = LoggerFactory.getLogger(Converter.class);
+    
     private boolean predefinedFileExtensionSubstitutionsEnabled;
     private Map<String, String> fileExtensionSubstitutions = 
PREDEFINED_FILE_EXTENSION_SUBSTITUTIONS;
     private Properties freeMarker2Settings;
     private Configuration fm2Cfg;
     private StringTemplateLoader stringTemplateLoader;
     private boolean validateOutput = true;
+    private boolean skipUnparsableFiles;
 
     @Override
     protected Pattern getDefaultInclude() {
@@ -162,10 +169,19 @@ public class FM2ToFM3Converter extends Converter {
         Template template = null;
         try {
             template = 
fm2Cfg.getTemplate(fileTransCtx.getRelativeSourcePathWithSlashes());
+            fm2Cfg.clearTemplateCache();
         } catch (Exception e) {
-            throw new ConverterException("Failed to load FreeMarker 2.3.x 
template", e);
+            if (getSkipUnparsableFiles() && e instanceof ParseException) {
+                ParseException pe = (ParseException) e;
+                fileTransCtx.getConversionMarkers().markInSource(
+                        pe.getLineNumber(), pe.getColumnNumber(), Type.WARN, 
"Skipped file due to parse error: "
+                        + pe.getEditorMessage());
+                LOG.debug("Skipped file due to parsing error: {}", 
fileTransCtx.getRelativeSourcePathWithSlashes());
+                return; //!
+            } else {
+                throw new ConverterException("Failed to load FreeMarker 2.3.x 
template", e);
+            }
         }
-        fm2Cfg.clearTemplateCache();
 
         FM2ASTToFM3SourceConverter.Result result = 
FM2ASTToFM3SourceConverter.convert(
                 template, fm2Cfg, stringTemplateLoader, 
fileTransCtx.getConversionMarkers()
@@ -275,5 +291,21 @@ public class FM2ToFM3Converter extends Converter {
     public void setValidateOutput(boolean validateOutput) {
         this.validateOutput = validateOutput;
     }
+    
+    /**
+     * Getter pair of {@link #setSkipUnparsableFiles(boolean)}.
+     */
+    public boolean getSkipUnparsableFiles() {
+        return skipUnparsableFiles;
+    }
+
+    /**
+     * Sets whether source files that syntactically aren't valid FreeMarker 2 
templates should be ignored.
+     * The problem will be logged as a warning into to the conversion markers 
file.
+     * Defaults to {@code false}. 
+     */
+    public void setSkipUnparsableFiles(boolean skipUnparsableFiles) {
+        this.skipUnparsableFiles = skipUnparsableFiles;
+    }
 
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3ConverterCLI.java
----------------------------------------------------------------------
diff --git 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3ConverterCLI.java
 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3ConverterCLI.java
index b4b5dbd..71bdc2d 100644
--- 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3ConverterCLI.java
+++ 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3ConverterCLI.java
@@ -49,6 +49,7 @@ public class FM2ToFM3ConverterCLI {
     private static final String EXCLUDE_OPTION = "exclude";
     private static final String FILE_EXTENSION_SUBSTITUTION = "file-ext-subst";
     private static final String NO_PREDEFINED_FILE_EXTENSION_SUBSTITUTIONS = 
"no-predef-file-ext-substs";
+    private static final String SKIP_UNPARSEABLE_FILES = 
"skip-unparsable-files";
     private static final String FREEMARKER_2_SETTING_OPTION = "fm2-setting";
     private static final String HELP_OPTION = "help";
     private static final String HELP_OPTION_SHORT = "h";
@@ -94,6 +95,10 @@ public class FM2ToFM3ConverterCLI {
                     .desc("Disables the predefined file extension 
substitutions (i.e, \"ftl\", \"ftlh\", "
                             + "\"ftlx\" and \"fm\" are replaced with the 
corresponding FreeMarker 3 file extensions).")
                     .build())
+            .addOption(Option.builder(null).longOpt(SKIP_UNPARSEABLE_FILES)
+                    .desc("Ignore source files that aren't syntactically vaild 
FreeMarker 2.x templates. The problem "
+                            + "will be logged as a warning into to the 
conversion markers file.")
+                    .build())
             .addOption(Option.builder(HELP_OPTION_SHORT).longOpt(HELP_OPTION)
                     .desc("Prints command-line help.")
                     .build());
@@ -160,6 +165,10 @@ public class FM2ToFM3ConverterCLI {
 
                 converter.setFileExtensionSubstitutions((Map) 
Collections.unmodifiableMap(
                         cl.getOptionProperties(FILE_EXTENSION_SUBSTITUTION)));
+                
+                if (cl.hasOption(SKIP_UNPARSEABLE_FILES)) {
+                    converter.setSkipUnparsableFiles(true);
+                }
 
                 
converter.setFreeMarker2Settings(cl.getOptionProperties(FREEMARKER_2_SETTING_OPTION));
                 try {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-converter/src/main/java/org/apache/freemarker/converter/_ConverterUtils.java
----------------------------------------------------------------------
diff --git 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/_ConverterUtils.java
 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/_ConverterUtils.java
index f8851c9..5b26b58 100644
--- 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/_ConverterUtils.java
+++ 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/_ConverterUtils.java
@@ -54,4 +54,14 @@ public final class _ConverterUtils {
         }
         return true;
     }
+
+    public static boolean containsLineBreak(String s) {
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c == '\n' || c == '\r') {
+                return true;
+            }
+        }
+        return false;
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 2fc387f..4a2d0dd 100644
--- 
a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
+++ 
b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
@@ -150,8 +150,7 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
         assertConvertedSame("${a <#--1--> !} ${a <#--2--> ! <#--3--> 0}");
         assertConvertedSame("${a!b.c(x!0, y!0)}");
         assertConvertedSame("${(a.b)!x}");
-        // [FM3] Will be: a!(x+1)
-        assertConvertedSame("${a!x+1}");
+        assertConverted("${a!(x+1)}", "${a!x+1}");
 
         assertConvertedSame("${a??} ${a <#--1--> ??}");
     }
@@ -501,6 +500,50 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
         assertConverted("${s <#-- c --> ??}", "${s <#-- c --> ?exists}");
         assertConverted("${s?? <#-- c --> }", "${s? <#-- c --> exists}");
         assertConverted("${s?? <#-- c --> }", "${s?exists <#-- c --> }");
+        
+        assertConverted("${s!1}", "${s?default(1)}");
+        assertConverted("${s!(1 + x)}", "${s?default(1 + x)}");
+        assertConverted("${s!(-x)}", "${s?default(-x)}");
+        assertConverted("${s!a.b}", "${s?default(a.b)}");
+        assertConverted("${s!a!b}", "${s?default(a!b)}");
+        assertConverted("${s\n\t!1}", "${s?default(\n\t1)}");
+        assertConverted("${s!1}", "${s?default( 1)}");
+        assertConverted("${s! <#-- c1 --> d1}", "${s?default( <#-- c1 --> 
d1)}");
+        assertConverted("${s!d1!d2!d3}", "${s?default(d1, d2, d3)}");
+        assertConverted("${s!d1!d2!d3}", "${s?default( d1,d2,d3 )}");
+        assertConverted("${s!a!b!c!d}", "${s?default(a!b, c!d)}");
+        assertConverted("${s!d1 <#-- c1 --> !d2 <#-- c2 -->}", "${s?default(d1 
<#-- c1 -->, d2 <#-- c2 -->)}");
+        try {
+            convert("<#assign d = x?default>");
+            fail();
+        } catch (UnconvertableLegacyFeatureException e) {
+            assertEquals(1, (Object) e.getRow());
+            assertEquals(14, (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}");
+    }
+
+    @Test
+    public void testDefaultValueExpressionPrecedenceChange() throws 
IOException, ConverterException {
+        assertConvertedSame("${v!d}");
+        assertConvertedSame("${v!}");
+        assertConvertedSame("${v!d.e}");
+        assertConvertedSame("${v!d[e]}");
+        assertConvertedSame("${v!d(e)}");
+        assertConvertedSame("${v!d??}");
+        assertConvertedSame("${v!d?upperCase}");
+        assertConvertedSame("${v!}");
+        assertConvertedSame("${v!(d + 1)}");
+        assertConvertedSame("${(v!) + 'x'}");
+        assertConverted("${v!(+1)}", "${v!+1}"); 
+        assertConverted("${v!(-1)}", "${v!-1}"); 
+        assertConverted("${v!(d+1)}", "${v!d+1}");
+        assertConverted("${v!(d-1)}", "${v!d-1}");
+        assertConverted("${v!(d * e)}", "${v!d * e}");
+        assertConverted("${v ! (d * e)}", "${v ! d * e}");
+        assertConverted("${v ! <#-- c1 --> (d * e) <#-- c2 -->}", "${v ! <#-- 
c1 --> d * e <#-- c2 -->}");
     }
     
     @Test

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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
new file mode 100644
index 0000000..7955869
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DefaultExpressionTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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;
+
+import java.util.Collections;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class DefaultExpressionTest extends TemplateTest {
+
+    @Test
+    public void testSimpleChaining() throws Exception {
+        assertErrorContains("${a!b!c}", InvalidReferenceException.class, 
"a!b!c");
+        addToDataModel("c", "C");
+        assertOutput("${a!b!c}", "C");
+        addToDataModel("b", "B");
+        assertOutput("${a!b!c}", "B");
+        addToDataModel("a", "A");
+        assertOutput("${a!b!c}", "A");
+        addToDataModel("b", null);
+        addToDataModel("c", null);
+        assertOutput("${a!b!c}", "A");
+    }
+
+    @Test
+    public void testPrecedenceHighEnough() throws Exception {
+        assertOutput("${a!1 * 2}", "2");
+        addToDataModel("a", 2);
+        assertOutput("${(a!1) * 2}", "4");
+        assertOutput("${a!(1 * 2)}", "2");
+        assertOutput("${a!1 * 2}", "4");
+        
+        assertOutput("${a!1 * (b!3)}", "6");
+        assertOutput("${a!(1 * b)!3}", "2");
+        assertOutput("${a!1 * b!3}", "6");
+        addToDataModel("a", null);
+        assertOutput("${a!(1 * b)!3}", "3"); // This will change in FM3 when 
(exp)!defExp won't be special anymore
+        assertOutput("${a!1 * b!3}", "3");
+    }
+
+    @Test
+    public void testPrecedenceLowEnough() throws Exception {
+        addToDataModel("a", Collections.emptyMap());
+        addToDataModel("b", Collections.emptyMap());
+        addToDataModel("c", Collections.singletonMap("cs", "CS"));
+        assertOutput("${a.as!b.bs!c.cs}", "CS");
+        assertOutput("${a['as']!b['bs']!c['cs']}", "CS");
+        
+        addToDataModel("b", Collections.singletonMap("bs", "BS"));
+        assertOutput("${a.as!b.bs!c.cs}", "BS");
+        assertOutput("${a['as']!b['bs']!c['cs']}", "BS");
+        
+        addToDataModel("a", Collections.singletonMap("as", "AS"));
+        assertOutput("${a.as!b.bs!c.cs}", "AS");
+        assertOutput("${a['as']!b['bs']!c['cs']}", "AS");
+        addToDataModel("b", Collections.emptyMap());
+        assertOutput("${a.as!b.bs!c.cs}", "AS");
+        assertOutput("${a['as']!b['bs']!c['cs']}", "AS");
+        addToDataModel("c", Collections.singletonMap("cs", "CS"));
+        assertOutput("${a.as!b.bs!c.cs}", "AS");
+        assertOutput("${a['as']!b['bs']!c['cs']}", "AS");
+    }
+    
+    @Test
+    public void testWithUnaryPrefixOps() throws Exception {
+        assertOutput("${a!(-1)}", "-1");
+        assertOutput("${a!(+1)}", "1");
+        assertErrorContains("${a!-1}", "number");
+        assertOutput("${a!+1}", "1"); // Because: "" + 1
+        addToDataModel("a", 3);
+        assertOutput("${a!-1}", "2");
+        assertOutput("${a!+1}", "4");
+
+        // Why prefix operators has lower precedence:
+        assertOutput("${'x' + u! + v! + 'y'}", "xy");
+        addToDataModel("u", "U");
+        assertOutput("${'x' + u! + v! + 'y'}", "xUy");
+        addToDataModel("v", "V");
+        assertOutput("${'x' + u! + v! + 'y'}", "xUVy");
+        addToDataModel("u", null);
+        assertOutput("${'x' + u! + v! + 'y'}", "xVy");
+    }
+    
+    @Test
+    public void testTerminatesBeforeParam() throws Exception {
+        assertOutput(
+                "<#macro m a b c>[${a}][${b}][${c}]</#macro>"
+                + "<@m a=x! b=y! c=z! /> "
+                + "<@m a=x!'x' b=y!'y' c=z!'z' /> "                        
+                + "<#assign y='Y'>"
+                + "<@m a=x! b=y! c=z! />",
+                "[][][] [x][y][z] [][Y][]");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/__conversion-markers.txt
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/resources/__conversion-markers.txt 
b/freemarker-core-test/src/test/resources/__conversion-markers.txt
deleted file mode 100644
index e69de29..0000000

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 f8bc788..e1db7cf 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
@@ -27,8 +27,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?default('-') expected='-' />
-<@assertEquals actual=(v)?default('-') expected='-' />
+<@assertEquals actual=v!'-' expected='-' />
+<@assertEquals actual=(v)!'-' expected='-' />
 <@isNonFastIRE>${v}</@> <#-- To check that it isn't an IRE.FAST_INSTANCE -->
 <@assertEquals actual=v?? expected=false />
 <@assertEquals actual=(v)?? expected=false />
@@ -37,10 +37,10 @@
 <@assertEquals actual=v?hasContent expected=false />
 <@assertEquals actual=(v)?hasContent expected=false />
 
-<@assertEquals actual=v?default(w, '-') expected='-' />
+<@assertEquals actual=v!w!'-' expected='-' />
 <@assertEquals actual=v!w!'-' expected='-' />
 <#assign w = 'W'>
-<@assertEquals actual=v?default(w, '-') expected='W' />
+<@assertEquals actual=v!w!'-' expected='W' />
 <@assertEquals actual=v!w!'-' expected='W' />
 
 <#list ['V', 1.5] as v>
@@ -48,8 +48,8 @@
        <@assertEquals actual=(v)!'-' expected=v />
        <@assert v?? />
        <@assert (v)?? />
-       <@assertEquals actual=v?default('-') expected=v />
-       <@assertEquals actual=(v)?default('-') expected=v />
+       <@assertEquals actual=v!'-' expected=v />
+       <@assertEquals actual=(v)!'-' expected=v />
        <@assert v?? />
        <@assert (v)?? />
        <@assertEquals actual=v?ifExists expected=v />
@@ -65,8 +65,8 @@
 <@assertEquals actual=(u.v)!'-' expected='-' />
 <@isIRE>${u.v??}</@>
 <@assertEquals actual=(u.v)?? expected=false />
-<@isIRE>${u.v?default('-')}</@>
-<@assertEquals actual=(u.v)?default('-') expected='-' />
+<@isIRE>${u.v!'-'}</@>
+<@assertEquals actual=(u.v)!'-' expected='-' />
 <@isIRE>${u.v??}</@>
 <@assertEquals actual=(u.v)?? expected=false />
 <@isIRE>${u.v?ifExists}</@>
@@ -79,8 +79,8 @@
 <@assertEquals actual=(u.v)!'-' expected='-' />
 <@assertEquals actual=u.v?? expected=false />
 <@assertEquals actual=(u.v)?? expected=false />
-<@assertEquals actual=u.v?default('-') expected='-' />
-<@assertEquals actual=(u.v)?default('-') expected='-' />
+<@assertEquals actual=u.v!'-' expected='-' />
+<@assertEquals actual=(u.v)!'-' expected='-' />
 <@assertEquals actual=u.v?? expected=false />
 <@assertEquals actual=(u.v)?? expected=false />
 <@assertEquals actual=u.v?ifExists expected='' />
@@ -93,8 +93,8 @@
 <@assertEquals actual=(u.v)!'-' expected='V' />
 <@assert u.v?? />
 <@assert (u.v)?? />
-<@assertEquals actual=u.v?default('-') expected='V' />
-<@assertEquals actual=(u.v)?default('-') expected='V' />
+<@assertEquals actual=u.v!'-' expected='V' />
+<@assertEquals actual=(u.v)!'-' expected='V' />
 <@assert u.v?? />
 <@assert (u.v)?? />
 <@assertEquals actual=u.v?ifExists expected='V' />

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/nested.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/nested.ftl
 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/nested.ftl
index 46f4492..8c991b4 100644
--- 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/nested.ftl
+++ 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/nested.ftl
@@ -22,8 +22,8 @@
     ${y} ${count}/${x}: <#nested x, "asdf"> <#-- the second body parameter is 
not used below -->
   </#list>
 </#macro>
-<@repeat count=3>${y?default("undefined")} ${x?default("undefined")} 
${count?default("undefined")}</@repeat>
+<@repeat count=3>${y!"undefined"} ${x!"undefined"} 
${count!"undefined"}</@repeat>
 <#global x = "X">
 <#global y = "Y">
 <#global count = "Count">
-<@repeat count=3 ; param1>${y?default("undefined")} ${x?default("undefined")} 
${count?default("undefined")} ${param1}</@repeat>
\ No newline at end of file
+<@repeat count=3 ; param1>${y!"undefined"} ${x!"undefined"} 
${count!"undefined"} ${param1}</@repeat>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding1.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding1.ftl
 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding1.ftl
index ec95d33..b309fa0 100644
--- 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding1.ftl
+++ 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding1.ftl
@@ -16,8 +16,8 @@
   specific language governing permissions and limitations
   under the License.
 -->
-Output charset: ${.outputEncoding?default("undefined")}
-URL escaping charset: ${.urlEscapingCharset?default("undefined")}
+Output charset: ${.outputEncoding!"undefined"}
+URL escaping charset: ${.urlEscapingCharset!"undefined"}
 
 <#assign s="a/%b">
 <#setting urlEscapingCharset="UTF-16">

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding2.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding2.ftl
 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding2.ftl
index c9a4f9f..83eae70 100644
--- 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding2.ftl
+++ 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding2.ftl
@@ -16,8 +16,8 @@
   specific language governing permissions and limitations
   under the License.
 -->
-Output charset: ${.outputEncoding?default("undefined")}
-URL escaping charset: ${.urlEscapingCharset?default("undefined")}
+Output charset: ${.outputEncoding!"undefined"}
+URL escaping charset: ${.urlEscapingCharset!"undefined"}
 
 <#assign s="a/%b">
 UTF-16: ${s?url}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding3.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding3.ftl
 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding3.ftl
index c9a4f9f..83eae70 100644
--- 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding3.ftl
+++ 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/output-encoding3.ftl
@@ -16,8 +16,8 @@
   specific language governing permissions and limitations
   under the License.
 -->
-Output charset: ${.outputEncoding?default("undefined")}
-URL escaping charset: ${.urlEscapingCharset?default("undefined")}
+Output charset: ${.outputEncoding!"undefined"}
+URL escaping charset: ${.urlEscapingCharset!"undefined"}
 
 <#assign s="a/%b">
 UTF-16: ${s?url}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/var-layers.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/var-layers.ftl
 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/var-layers.ftl
index e11e602..6a33548 100644
--- 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/var-layers.ftl
+++ 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/var-layers.ftl
@@ -23,7 +23,7 @@ ${x} = ${.dataModel.x} = ${.globals.x}
 ${x} = ${.main.x} = ${.namespace.x}
 <#global x = 6>
 ${.globals.x} but ${.dataModel.x} = 4
-${y} = ${.globals.y} = ${.dataModel.y?default("ERROR")}
+${y} = ${.globals.y} = ${.dataModel.y!"ERROR"}
 Invisiblity test 1.: <#if .main.y?? || .namespace.y??>failed<#else>passed</#if>
 Invisiblity test 2.: <#if .main.z?? || .namespace.z??>failed<#else>passed</#if>
 Invisiblity test 3.: <#global q = 1><#if .main.q?? || .namespace.q?? || 
.dataModel.q??>failed<#else>passed</#if>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/varlayers_lib.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/varlayers_lib.ftl
 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/varlayers_lib.ftl
index dfca4f7..0441c7a 100644
--- 
a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/varlayers_lib.ftl
+++ 
b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/varlayers_lib.ftl
@@ -24,5 +24,5 @@
   ${z} = ${z2} = ${x1} = ${.dataModel.x}
   5
   ${x} == ${.globals.x}
-  ${y} == ${.globals.y} == ${.dataModel.y?default("ERROR")}
+  ${y} == ${.globals.y} == ${.dataModel.y!"ERROR"}
 </#macro>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 055632b..817dff5 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
@@ -75,7 +75,7 @@ abstract class ASTExpBuiltIn extends ASTExpression implements 
Cloneable {
     protected ASTExpression target;
     protected String key;
 
-    static final int NUMBER_OF_BIS = 263;
+    static final int NUMBER_OF_BIS = 262;
     static final HashMap<String, ASTExpBuiltIn> BUILT_INS_BY_NAME = new 
HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
 
     static {
@@ -97,7 +97,6 @@ abstract class ASTExpBuiltIn extends ASTExpression implements 
Cloneable {
         putBI("dateIfUnknown", new 
BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATE));
         putBI("dateTime", new 
BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATE_TIME));
         putBI("dateTimeIfUnknown", new 
BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATE_TIME));
-        putBI("default", new BuiltInsForExistenceHandling.defaultBI());
         putBI("double", new doubleBI());
         putBI("endsWith", new BuiltInsForStringsBasic.ends_withBI());
         putBI("ensureEndsWith", new 
BuiltInsForStringsBasic.ensure_ends_withBI());

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 61d33dd..2e6a816 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
@@ -19,9 +19,7 @@
 
 package org.apache.freemarker.core;
 
-import org.apache.freemarker.core.model.ArgumentArrayLayout;
 import org.apache.freemarker.core.model.TemplateBooleanModel;
-import org.apache.freemarker.core.model.TemplateFunctionModel;
 import org.apache.freemarker.core.model.TemplateModel;
 
 /**
@@ -53,60 +51,6 @@ class BuiltInsForExistenceHandling {
         
     }
     
-    static class defaultBI extends 
BuiltInsForExistenceHandling.ExistenceBuiltIn {
-
-        @Override
-        TemplateModel _eval(final Environment env) throws TemplateException {
-            TemplateModel model = evalMaybeNonexistentTarget(env);
-            return model == null ? FIRST_NON_NULL_METHOD : new 
ConstantMethod(model);
-        }
-
-        private static class ConstantMethod implements TemplateFunctionModel {
-            private final TemplateModel constant;
-
-            ConstantMethod(TemplateModel constant) {
-                this.constant = constant;
-            }
-
-            @Override
-            public TemplateModel execute(TemplateModel[] args, CallPlace 
callPlace, Environment env) {
-                return constant;
-            }
-
-            @Override
-            public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
-                return null;
-            }
-
-        }
-
-        /**
-         * A method that goes through the arguments one by one and returns
-         * the first one that is non-null. If all args are null, returns null.
-         */
-        private static final TemplateFunctionModel FIRST_NON_NULL_METHOD = new 
TemplateFunctionModel() {
-
-            @Override
-            public TemplateModel execute(TemplateModel[] args, CallPlace 
callPlace, Environment env)
-                    throws TemplateException {
-                int argsLen = args.length;
-                for (int i = 0; i < argsLen; i++ ) {
-                    TemplateModel result = args[i];
-                    if (result != null) {
-                        return result;
-                    }
-                }
-                return null;
-            }
-
-            @Override
-            public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
-                return null;
-            }
-
-        };
-    }
-    
     static class has_contentBI extends 
BuiltInsForExistenceHandling.ExistenceBuiltIn {
         @Override
         TemplateModel _eval(Environment env) throws TemplateException {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 b5b3441..4527e1a 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
@@ -1617,15 +1617,15 @@ public class DefaultObjectWrapper implements 
RichObjectWrapper {
          * <p>If this property is <tt>false</tt> (the default) then an attempt 
to read
          * a missing bean property is the same as reading an existing bean 
property whose
          * value is <tt>null</tt>. The template can't tell the difference, and 
thus always
-         * can use <tt>?default('something')</tt> and <tt>??</tt> and similar 
expressions
+         * can use <tt>!'something'</tt> and <tt>??</tt> and similar 
expressions
          * to handle the situation.
          *
          * <p>If this property is <tt>true</tt> then an attempt to read a bean 
propertly in
          * the template (like <tt>myBean.aProperty</tt>) that doesn't exist in 
the bean
          * object (as opposed to just holding <tt>null</tt> value) will cause
          * {@link InvalidPropertyException}, which can't be suppressed in the 
template
-         * (not even with 
<tt>myBean.noSuchProperty?default('something')</tt>). This way
-         * <tt>?default('something')</tt> and <tt>??</tt> and similar 
expressions can be used to
+         * (not even with <tt>myBean.noSuchProperty!'something'</tt>). This way
+         * <tt>!'something'</tt> and <tt>??</tt> and similar expressions can 
be used to
          * handle existing properties whose value is <tt>null</tt>, without 
the risk of
          * hiding typos in the property names. Typos will always cause error. 
But mind you, it
          * goes against the basic approach of FreeMarker, so use this feature 
only if you really

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
index e4c96c8..b605988 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
@@ -36,7 +36,7 @@ class EnumModels extends ClassBasedModelFactory {
         if (obj == null) {
             // Return null - it'll manifest itself as undefined in the 
template.
             // We're doing this rather than throw an exception as this way 
-            // people can use someEnumModel?default({}) to gracefully fall 
back 
+            // people can use someEnumModel!{} to gracefully fall back 
             // to an empty hash if they want to.
             return null;
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/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 2d25dcf..dedf283 100644
--- a/freemarker-core/src/main/javacc/FTL.jj
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -1382,6 +1382,45 @@ 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. 
+ */
+ASTExpression PrimaryWithDefaultExpression() :
+{
+    Token exclamTk;
+    ASTExpression priExp, defaultExp;
+}
+{
+    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>.
+    {
+        return priExp;
+    }
+}
+
+/**
  * Lowest level expression, a literal, a variable,
  * or a possibly more complex expression bounded
  * by parentheses.
@@ -1409,7 +1448,7 @@ ASTExpression PrimaryExpression() :
         exp = ASTExpBuiltInVariable()
     )
     (
-        LOOKAHEAD(<DOT> | <OPEN_BRACKET> |<OPEN_PAREN> | <BUILT_IN> | <EXCLAM> 
| <EXISTS>)
+        LOOKAHEAD(<DOT> | <OPEN_BRACKET> |<OPEN_PAREN> | <BUILT_IN> | <EXISTS>)
         exp = AddSubExpression(exp)
     )*
     {
@@ -1434,11 +1473,9 @@ ASTExpression Parenthesis() :
 }
 
 /**
- * A primary expression preceded by zero or
- * more unary operators. (The only unary operator we
- * currently have is the NOT.)
+ * A primary expression preceded by zero or more unary prefix operators.
  */
-ASTExpression UnaryExpression() :
+ASTExpression UnaryPrefixExpression() :
 {
     ASTExpression exp, result;
     boolean haveNot = false;
@@ -1450,7 +1487,7 @@ ASTExpression UnaryExpression() :
         |
         result = ASTExpNot()
         |
-        result = PrimaryExpression()
+        result = PrimaryWithDefaultExpression()
     )
     {
         return result;
@@ -1467,7 +1504,7 @@ ASTExpression ASTExpNot() :
     (
         t = <EXCLAM> { nots.add(t); }
     )+
-    exp = PrimaryExpression()
+    exp = PrimaryWithDefaultExpression()
     {
         for (int i = 0; i < nots.size(); i++) {
             result = new ASTExpNot(exp);
@@ -1491,7 +1528,7 @@ ASTExpression ASTExpNegateOrPlus() :
         |
         t = <MINUS> { isMinus = true; }
     )
-    exp = PrimaryExpression()
+    exp = PrimaryWithDefaultExpression()
     {
         result = new ASTExpNegateOrPlus(exp, isMinus);  
         result.setLocation(template, t, exp);
@@ -1545,7 +1582,7 @@ ASTExpression MultiplicativeExpression() :
     int operation = ASTExpArithmetic.TYPE_MULTIPLICATION;
 }
 {
-    lhs = UnaryExpression() { result = lhs; }
+    lhs = UnaryPrefixExpression() { result = lhs; }
     (
         LOOKAHEAD(<TIMES>|<DIVIDE>|<PERCENT>)
         (
@@ -1557,7 +1594,7 @@ ASTExpression MultiplicativeExpression() :
                 <PERCENT> {operation = ASTExpArithmetic.TYPE_MODULO; }
             )
         )
-        rhs = UnaryExpression()
+        rhs = UnaryPrefixExpression()
         {
             numberLiteralOnly(lhs);
             numberLiteralOnly(rhs);
@@ -1571,7 +1608,6 @@ ASTExpression MultiplicativeExpression() :
     }
 }
 
-
 ASTExpression EqualityExpression() :
 {
     ASTExpression lhs, rhs, result;
@@ -1849,8 +1885,6 @@ ASTExpression AddSubExpression(ASTExpression exp) :
         |
         result = ASTExpBuiltIn(exp)
         |
-        result = DefaultTo(exp)
-        |
         result = Exists(exp)
     )
     {
@@ -1858,32 +1892,6 @@ ASTExpression AddSubExpression(ASTExpression exp) :
     }
 }
 
-ASTExpression DefaultTo(ASTExpression exp) :
-{
-    ASTExpression rhs = null;
-    Token t;
-}
-{
-    t = <EXCLAM>
-    (
-        LOOKAHEAD(<ID><ASSIGNMENT_EQUALS>) { /* Do not consume */ }
-        |
-        [
-            LOOKAHEAD(ASTExpression())
-            rhs = ASTExpression()
-        ]
-    )
-    {
-        ASTExpDefault result = new ASTExpDefault(exp, rhs);
-        if (rhs == null) {
-            result.setLocation(template, exp, t);
-        } else {
-            result.setLocation(template, exp, rhs);
-        }
-        return result;
-    }
-}
-
 ASTExpression Exists(ASTExpression exp) :
 {
     Token t;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/default-xmlns.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/default-xmlns.ftl
 
b/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/default-xmlns.ftl
index 03ceefa..306ddfb 100644
--- 
a/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/default-xmlns.ftl
+++ 
b/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/default-xmlns.ftl
@@ -18,10 +18,10 @@
   under the License.
 -->
 <#assign r = doc.*[0]>
-${r["N:t1"]?default('-')} = No NS
-${r["t2"]?default('-')} = x NS
-${r["y:t3"]?default('-')} = y NS
-${r["./D:t4"]?default('-')} = x NS
+${r["N:t1"]!'-'} = No NS
+${r["t2"]!'-'} = x NS
+${r["y:t3"]!'-'} = y NS
+${r["./D:t4"]!'-'} = x NS
 
 <#assign bool = doc["true()"]>
 ${bool?string}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/f725d36f/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/xmlns5.ftl
----------------------------------------------------------------------
diff --git 
a/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/xmlns5.ftl
 
b/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/xmlns5.ftl
index 078f4d8..de864a8 100644
--- 
a/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/xmlns5.ftl
+++ 
b/freemarker-dom/src/test/resources/org/apache/freemarker/dom/templatesuite/templates/xmlns5.ftl
@@ -18,11 +18,11 @@
   under the License.
 -->
 <#assign r = doc["N:root"]>
-${r["N:t1"][0]?default('-')} = No NS
-${r["xx:t2"][0]?default('-')} = x NS
-${r["t3"][0]?default('-')} = y NS
-${r["xx:t4"][0]?default('-')} = x NS
-${r["//t1"][0]?default('-')} = No NS
-${r["//t2"][0]?default('-')} = -
-${r["//t3"][0]?default('-')} = -
-${r["//t4"][0]?default('-')} = -
+${r["N:t1"][0]!'-'} = No NS
+${r["xx:t2"][0]!'-'} = x NS
+${r["t3"][0]!'-'} = y NS
+${r["xx:t4"][0]!'-'} = x NS
+${r["//t1"][0]!'-'} = No NS
+${r["//t2"][0]!'-'} = -
+${r["//t3"][0]!'-'} = -
+${r["//t4"][0]!'-'} = -

Reply via email to