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]!'-'} = -
