http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java new file mode 100644 index 0000000..c3db14e --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java @@ -0,0 +1,111 @@ +/* + * 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.io.IOException; + +import org.apache.freemarker.core.ASTExpression.ReplacemenetState; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #escape}. + */ +class ASTDirEscape extends ASTDirective { + + private final String variable; + private final ASTExpression expr; + private ASTExpression escapedExpr; + + + ASTDirEscape(String variable, ASTExpression expr, ASTExpression escapedExpr) { + this.variable = variable; + this.expr = expr; + this.escapedExpr = escapedExpr; + } + + void setContent(TemplateElements children) { + setChildren(children); + // We don't need it anymore at this point + escapedExpr = null; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + return getChildBuffer(); + } + + ASTExpression doEscape(ASTExpression expression) { + return escapedExpr.deepCloneWithIdentifierReplaced(variable, expression, new ReplacemenetState()); + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()) + .append(' ').append(_StringUtil.toFTLTopLevelIdentifierReference(variable)) + .append(" as ").append(expr.getCanonicalForm()); + if (canonical) { + sb.append('>'); + sb.append(getChildrenCanonicalForm()); + sb.append("</").append(getNodeTypeSymbol()).append('>'); + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#escape"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return variable; + case 1: return expr; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.PLACEHOLDER_VARIABLE; + case 1: return ParameterRole.EXPRESSION_TEMPLATE; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + boolean isOutputCacheable() { + return true; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java new file mode 100644 index 0000000..08b5c42 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java @@ -0,0 +1,70 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: {@code #fallback}. + */ +final class ASTDirFallback extends ASTDirective { + + @Override + ASTElement[] accept(Environment env) throws IOException, TemplateException { + env.fallback(); + return null; + } + + @Override + protected String dump(boolean canonical) { + return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol(); + } + + @Override + String getNodeTypeSymbol() { + return "#fallback"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + + @Override + boolean isShownInStackTrace() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java new file mode 100644 index 0000000..ad7aff4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java @@ -0,0 +1,65 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: {@code #flush} + */ +final class ASTDirFlush extends ASTDirective { + + @Override + ASTElement[] accept(Environment env) throws IOException { + env.getOut().flush(); + return null; + } + + @Override + protected String dump(boolean canonical) { + return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol(); + } + + @Override + String getNodeTypeSymbol() { + return "#flush"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java new file mode 100644 index 0000000..d04b0a0 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java @@ -0,0 +1,107 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: Container for a group of related {@code #if}, {@code #elseif} and {@code #else} directives. + * Each such block is a nested {@link ASTDirIfOrElseOrElseIf}. Note that if an {@code #if} stands alone, + * {@link ASTDirIfOrElseOrElseIf} doesn't need this parent element. + */ +final class ASTDirIfElseIfElseContainer extends ASTDirective { + + ASTDirIfElseIfElseContainer(ASTDirIfOrElseOrElseIf block) { + setChildBufferCapacity(1); + addBlock(block); + } + + void addBlock(ASTDirIfOrElseOrElseIf block) { + addChild(block); + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + int ln = getChildCount(); + for (int i = 0; i < ln; i++) { + ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf) getChild(i); + ASTExpression condition = cblock.condition; + env.replaceElementStackTop(cblock); + if (condition == null || condition.evalToBoolean(env)) { + return cblock.getChildBuffer(); + } + } + return null; + } + + @Override + ASTElement postParseCleanup(boolean stripWhitespace) + throws ParseException { + if (getChildCount() == 1) { + ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf) getChild(0); + cblock.setLocation(getTemplate(), cblock, this); + return cblock.postParseCleanup(stripWhitespace); + } else { + return super.postParseCleanup(stripWhitespace); + } + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + StringBuilder buf = new StringBuilder(); + int ln = getChildCount(); + for (int i = 0; i < ln; i++) { + ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf) getChild(i); + buf.append(cblock.dump(canonical)); + } + buf.append("</#if>"); + return buf.toString(); + } else { + return getNodeTypeSymbol(); + } + } + + @Override + String getNodeTypeSymbol() { + return "#if-#elseif-#else-container"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java new file mode 100644 index 0000000..136b5b7 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java @@ -0,0 +1,114 @@ +/* + * 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.io.IOException; + +import org.apache.freemarker.core.util.BugException; + +/** + * AST directive node: An element that represents a conditionally executed block: {@code #if}, {@code #elseif} or + * {@code #elseif}. Note that when an {@code #if} has related {@code #elseif}-s or {@code #else}, an + * {@link ASTDirIfElseIfElseContainer} parent must be used. For a lonely {@code #if}, no such parent is needed. + */ +final class ASTDirIfOrElseOrElseIf extends ASTDirective { + + static final int TYPE_IF = 0; + static final int TYPE_ELSE = 1; + static final int TYPE_ELSE_IF = 2; + + final ASTExpression condition; + private final int type; + + ASTDirIfOrElseOrElseIf(ASTExpression condition, TemplateElements children, int type) { + this.condition = condition; + setChildren(children); + this.type = type; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + if (condition == null || condition.evalToBoolean(env)) { + return getChildBuffer(); + } + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder buf = new StringBuilder(); + if (canonical) buf.append('<'); + buf.append(getNodeTypeSymbol()); + if (condition != null) { + buf.append(' '); + buf.append(condition.getCanonicalForm()); + } + if (canonical) { + buf.append(">"); + buf.append(getChildrenCanonicalForm()); + if (!(getParent() instanceof ASTDirIfElseIfElseContainer)) { + buf.append("</#if>"); + } + } + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + if (type == TYPE_ELSE) { + return "#else"; + } else if (type == TYPE_IF) { + return "#if"; + } else if (type == TYPE_ELSE_IF) { + return "#elseif"; + } else { + throw new BugException("Unknown type"); + } + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return condition; + case 1: return Integer.valueOf(type); + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.CONDITION; + case 1: return ParameterRole.AST_NODE_SUBTYPE; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java new file mode 100644 index 0000000..38e88bf --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java @@ -0,0 +1,125 @@ +/* + * 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.io.IOException; + +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #import} + */ +final class ASTDirImport extends ASTDirective { + + private ASTExpression importedTemplateNameExp; + private String targetNsVarName; + + /** + * @param template the template that this directive is a part of. + * @param importedTemplateNameExp the name of the template to be included. + * @param targetNsVarName the name of the variable to assign this library's namespace to + */ + ASTDirImport(Template template, + ASTExpression importedTemplateNameExp, + String targetNsVarName) { + this.targetNsVarName = targetNsVarName; + this.importedTemplateNameExp = importedTemplateNameExp; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + final String importedTemplateName = importedTemplateNameExp.evalAndCoerceToPlainText(env); + final String fullImportedTemplateName; + try { + fullImportedTemplateName = env.toFullTemplateName(getTemplate().getLookupName(), importedTemplateName); + } catch (MalformedTemplateNameException e) { + throw new _MiscTemplateException(e, env, + "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n", + e.getMalformednessDescription()); + } + + try { + env.importLib(fullImportedTemplateName, targetNsVarName); + } catch (IOException e) { + throw new _MiscTemplateException(e, env, + "Template importing failed (for parameter value ", + new _DelayedJQuote(importedTemplateName), + "):\n", new _DelayedGetMessage(e)); + } + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder buf = new StringBuilder(); + if (canonical) buf.append('<'); + buf.append(getNodeTypeSymbol()); + buf.append(' '); + buf.append(importedTemplateNameExp.getCanonicalForm()); + buf.append(" as "); + buf.append(_StringUtil.toFTLTopLevelTragetIdentifier(targetNsVarName)); + if (canonical) buf.append("/>"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#import"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return importedTemplateNameExp; + case 1: return targetNsVarName; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.TEMPLATE_NAME; + case 1: return ParameterRole.NAMESPACE; + default: throw new IndexOutOfBoundsException(); + } + } + + public String getTemplateName() { + return importedTemplateNameExp.toString(); + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + + @Override + boolean isShownInStackTrace() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java new file mode 100644 index 0000000..2088d62 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java @@ -0,0 +1,174 @@ +/* + * 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.io.IOException; + +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #include} + */ +final class ASTDirInclude extends ASTDirective { + + private final ASTExpression includedTemplateNameExp, ignoreMissingExp; + private final Boolean ignoreMissingExpPrecalcedValue; + + /** + * @param template the template that this <tt>#include</tt> is a part of. + * @param includedTemplateNameExp the path of the template to be included. + */ + ASTDirInclude(Template template, + ASTExpression includedTemplateNameExp, + ASTExpression ignoreMissingExp) throws ParseException { + this.includedTemplateNameExp = includedTemplateNameExp; + + this.ignoreMissingExp = ignoreMissingExp; + if (ignoreMissingExp != null && ignoreMissingExp.isLiteral()) { + try { + try { + ignoreMissingExpPrecalcedValue = Boolean.valueOf( + ignoreMissingExp.evalToBoolean(template.getConfiguration())); + } catch (NonBooleanException e) { + throw new ParseException("Expected a boolean as the value of the \"ignore_missing\" attribute", + ignoreMissingExp, e); + } + } catch (TemplateException e) { + // evaluation of literals must not throw a TemplateException + throw new BugException(e); + } + } else { + ignoreMissingExpPrecalcedValue = null; + } + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + final String includedTemplateName = includedTemplateNameExp.evalAndCoerceToPlainText(env); + final String fullIncludedTemplateName; + try { + fullIncludedTemplateName = env.toFullTemplateName(getTemplate().getLookupName(), includedTemplateName); + } catch (MalformedTemplateNameException e) { + throw new _MiscTemplateException(e, env, + "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n", + e.getMalformednessDescription()); + } + + final boolean ignoreMissing; + if (ignoreMissingExpPrecalcedValue != null) { + ignoreMissing = ignoreMissingExpPrecalcedValue.booleanValue(); + } else if (ignoreMissingExp != null) { + ignoreMissing = ignoreMissingExp.evalToBoolean(env); + } else { + ignoreMissing = false; + } + + final Template includedTemplate; + try { + includedTemplate = env.getTemplateForInclusion(fullIncludedTemplateName, ignoreMissing); + } catch (IOException e) { + throw new _MiscTemplateException(e, env, + "Template inclusion failed (for parameter value ", + new _DelayedJQuote(includedTemplateName), + "):\n", new _DelayedGetMessage(e)); + } + + if (includedTemplate != null) { + env.include(includedTemplate); + } + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder buf = new StringBuilder(); + if (canonical) buf.append('<'); + buf.append(getNodeTypeSymbol()); + buf.append(' '); + buf.append(includedTemplateNameExp.getCanonicalForm()); + if (ignoreMissingExp != null) { + buf.append(" ignore_missing=").append(ignoreMissingExp.getCanonicalForm()); + } + if (canonical) buf.append("/>"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#include"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return includedTemplateNameExp; + case 1: return ignoreMissingExp; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.TEMPLATE_NAME; + case 1: return ParameterRole.IGNORE_MISSING_PARAMETER; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + + private boolean getYesNo(ASTExpression exp, String s) throws TemplateException { + try { + return _StringUtil.getYesNo(s); + } catch (IllegalArgumentException iae) { + throw new _MiscTemplateException(exp, + "Value must be boolean (or one of these strings: " + + "\"n\", \"no\", \"f\", \"false\", \"y\", \"yes\", \"t\", \"true\"), but it was ", + new _DelayedJQuote(s), "."); + } + } + +/* + boolean heedsOpeningWhitespace() { + return true; + } + + boolean heedsTrailingWhitespace() { + return true; + } +*/ + + @Override + boolean isShownInStackTrace() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java new file mode 100644 index 0000000..292d767 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java @@ -0,0 +1,120 @@ +/* + * 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.io.IOException; + +import org.apache.freemarker.core.ASTDirList.IterationContext; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #items} + */ +class ASTDirItems extends ASTDirective { + + private final String loopVarName; + private final String loopVar2Name; + + /** + * @param loopVar2Name + * For non-hash listings always {@code null}, for hash listings {@code loopVarName} and + * {@code loopVarName2} holds the key- and value loop variable names. + */ + ASTDirItems(String loopVarName, String loopVar2Name, TemplateElements children) { + this.loopVarName = loopVarName; + this.loopVar2Name = loopVar2Name; + setChildren(children); + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + final IterationContext iterCtx = ASTDirList.findEnclosingIterationContext(env, null); + if (iterCtx == null) { + // The parser should prevent this situation + throw new _MiscTemplateException(env, + getNodeTypeSymbol(), " without iteration in context"); + } + + iterCtx.loopForItemsElement(env, getChildBuffer(), loopVarName, loopVar2Name); + return null; + } + + @Override + boolean isNestedBlockRepeater() { + return true; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + sb.append(" as "); + sb.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName)); + if (loopVar2Name != null) { + sb.append(", "); + sb.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVar2Name)); + } + if (canonical) { + sb.append('>'); + sb.append(getChildrenCanonicalForm()); + sb.append("</"); + sb.append(getNodeTypeSymbol()); + sb.append('>'); + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#items"; + } + + @Override + int getParameterCount() { + return loopVar2Name != null ? 2 : 1; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: + if (loopVarName == null) throw new IndexOutOfBoundsException(); + return loopVarName; + case 1: + if (loopVar2Name == null) throw new IndexOutOfBoundsException(); + return loopVar2Name; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: + if (loopVarName == null) throw new IndexOutOfBoundsException(); + return ParameterRole.TARGET_LOOP_VARIABLE; + case 1: + if (loopVar2Name == null) throw new IndexOutOfBoundsException(); + return ParameterRole.TARGET_LOOP_VARIABLE; + default: throw new IndexOutOfBoundsException(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java new file mode 100644 index 0000000..0675882 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java @@ -0,0 +1,462 @@ +/* + * 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.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateHashModelEx; +import org.apache.freemarker.core.model.TemplateHashModelEx2; +import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePair; +import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.SimpleNumber; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #list} element, or pre-{@code #else} section of it inside a + * {@link ASTDirListElseContainer}. + */ +final class ASTDirList extends ASTDirective { + + private final ASTExpression listedExp; + private final String loopVarName; + private final String loopVar2Name; + private final boolean hashListing; + + /** + * @param listedExp + * a variable referring to a sequence or collection or extended hash to list + * @param loopVarName + * The name of the variable that will hold the value of the current item when looping through listed value, + * or {@code null} if we have a nested {@code #items}. If this is a hash listing then this variable will holds the value + * of the hash key. + * @param loopVar2Name + * The name of the variable that will hold the value of the current item when looping through the list, + * or {@code null} if we have a nested {@code #items}. If this is a hash listing then it variable will hold the value + * from the key-value pair. + * @param childrenBeforeElse + * The nested content to execute if the listed value wasn't empty; can't be {@code null}. If the loop variable + * was specified in the start tag, this is also what we will iterate over. + * @param hashListing + * Whether this is a key-value pair listing, or a usual listing. This is properly set even if we have + * a nested {@code #items}. + */ + ASTDirList(ASTExpression listedExp, + String loopVarName, + String loopVar2Name, + TemplateElements childrenBeforeElse, + boolean hashListing) { + this.listedExp = listedExp; + this.loopVarName = loopVarName; + this.loopVar2Name = loopVar2Name; + setChildren(childrenBeforeElse); + this.hashListing = hashListing; + } + + boolean isHashListing() { + return hashListing; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + acceptWithResult(env); + return null; + } + + boolean acceptWithResult(Environment env) throws TemplateException, IOException { + TemplateModel listedValue = listedExp.eval(env); + if (listedValue == null) { + listedExp.assertNonNull(null, env); + } + + return env.visitIteratorBlock(new IterationContext(listedValue, loopVarName, loopVar2Name)); + } + + /** + * @param loopVariableName + * Then name of the loop variable whose context we are looking for, or {@code null} if we simply look for + * the innermost context. + * @return The matching context or {@code null} if no such context exists. + */ + static IterationContext findEnclosingIterationContext(Environment env, String loopVariableName) + throws _MiscTemplateException { + LocalContextStack ctxStack = env.getLocalContextStack(); + if (ctxStack != null) { + for (int i = ctxStack.size() - 1; i >= 0; i--) { + Object ctx = ctxStack.get(i); + if (ctx instanceof IterationContext + && (loopVariableName == null + || loopVariableName.equals(((IterationContext) ctx).getLoopVariableName()) + || loopVariableName.equals(((IterationContext) ctx).getLoopVariable2Name()) + )) { + return (IterationContext) ctx; + } + } + } + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder buf = new StringBuilder(); + if (canonical) buf.append('<'); + buf.append(getNodeTypeSymbol()); + buf.append(' '); + buf.append(listedExp.getCanonicalForm()); + if (loopVarName != null) { + buf.append(" as "); + buf.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName)); + if (loopVar2Name != null) { + buf.append(", "); + buf.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVar2Name)); + } + } + if (canonical) { + buf.append(">"); + buf.append(getChildrenCanonicalForm()); + if (!(getParent() instanceof ASTDirListElseContainer)) { + buf.append("</"); + buf.append(getNodeTypeSymbol()); + buf.append('>'); + } + } + return buf.toString(); + } + + @Override + int getParameterCount() { + return 1 + (loopVarName != null ? 1 : 0) + (loopVar2Name != null ? 1 : 0); + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: + return listedExp; + case 1: + if (loopVarName == null) throw new IndexOutOfBoundsException(); + return loopVarName; + case 2: + if (loopVar2Name == null) throw new IndexOutOfBoundsException(); + return loopVar2Name; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: + return ParameterRole.LIST_SOURCE; + case 1: + if (loopVarName == null) throw new IndexOutOfBoundsException(); + return ParameterRole.TARGET_LOOP_VARIABLE; + case 2: + if (loopVar2Name == null) throw new IndexOutOfBoundsException(); + return ParameterRole.TARGET_LOOP_VARIABLE; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + String getNodeTypeSymbol() { + return "#list"; + } + + @Override + boolean isNestedBlockRepeater() { + return loopVarName != null; + } + + /** + * Holds the context of a #list directive. + */ + class IterationContext implements LocalContext { + + private static final String LOOP_STATE_HAS_NEXT = "_has_next"; // lenght: 9 + private static final String LOOP_STATE_INDEX = "_index"; // length 6 + + private Object openedIterator; + private boolean hasNext; + private TemplateModel loopVar; + private TemplateModel loopVar2; + private int index; + private boolean alreadyEntered; + private Collection localVarNames = null; + + /** If the {@code #list} has nested {@code #items}, it's {@code null} outside the {@code #items}. */ + private String loopVarName; + /** Used if we list key-value pairs */ + private String loopVar2Name; + + private final TemplateModel listedValue; + + public IterationContext(TemplateModel listedValue, String loopVarName, String loopVar2Name) { + this.listedValue = listedValue; + this.loopVarName = loopVarName; + this.loopVar2Name = loopVar2Name; + } + + boolean accept(Environment env) throws TemplateException, IOException { + return executeNestedContent(env, getChildBuffer()); + } + + void loopForItemsElement(Environment env, ASTElement[] childBuffer, String loopVarName, String loopVar2Name) + throws + TemplateException, IOException { + try { + if (alreadyEntered) { + throw new _MiscTemplateException(env, + "The #items directive was already entered earlier for this listing."); + } + alreadyEntered = true; + this.loopVarName = loopVarName; + this.loopVar2Name = loopVar2Name; + executeNestedContent(env, childBuffer); + } finally { + this.loopVarName = null; + this.loopVar2Name = null; + } + } + + /** + * Executes the given block for the {@link #listedValue}: if {@link #loopVarName} is non-{@code null}, then for + * each list item once, otherwise once if {@link #listedValue} isn't empty. + */ + private boolean executeNestedContent(Environment env, ASTElement[] childBuffer) + throws TemplateException, IOException { + return !hashListing + ? executedNestedContentForCollOrSeqListing(env, childBuffer) + : executedNestedContentForHashListing(env, childBuffer); + } + + private boolean executedNestedContentForCollOrSeqListing(Environment env, ASTElement[] childBuffer) + throws IOException, TemplateException { + final boolean listNotEmpty; + if (listedValue instanceof TemplateCollectionModel) { + final TemplateCollectionModel collModel = (TemplateCollectionModel) listedValue; + final TemplateModelIterator iterModel + = openedIterator == null ? collModel.iterator() + : ((TemplateModelIterator) openedIterator); + listNotEmpty = iterModel.hasNext(); + if (listNotEmpty) { + if (loopVarName != null) { + try { + do { + loopVar = iterModel.next(); + hasNext = iterModel.hasNext(); + env.visit(childBuffer); + index++; + } while (hasNext); + } catch (ASTDirBreak.Break br) { + // Silently exit loop + } + openedIterator = null; + } else { + // We must reuse this later, because TemplateCollectionModel-s that wrap an Iterator only + // allow one iterator() call. + openedIterator = iterModel; + env.visit(childBuffer); + } + } + } else if (listedValue instanceof TemplateSequenceModel) { + final TemplateSequenceModel seqModel = (TemplateSequenceModel) listedValue; + final int size = seqModel.size(); + listNotEmpty = size != 0; + if (listNotEmpty) { + if (loopVarName != null) { + try { + for (index = 0; index < size; index++) { + loopVar = seqModel.get(index); + hasNext = (size > index + 1); + env.visit(childBuffer); + } + } catch (ASTDirBreak.Break br) { + // Silently exit loop + } + } else { + env.visit(childBuffer); + } + } + } else if (listedValue instanceof TemplateHashModelEx + && !NonSequenceOrCollectionException.isWrappedIterable(listedValue)) { + throw new NonSequenceOrCollectionException(env, + new _ErrorDescriptionBuilder("The value you try to list is ", + new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)), + ", thus you must specify two loop variables after the \"as\"; one for the key, and " + + "another for the value, like ", "<#... as k, v>", ")." + )); + } else { + throw new NonSequenceOrCollectionException( + listedExp, listedValue, env); + } + return listNotEmpty; + } + + private boolean executedNestedContentForHashListing(Environment env, ASTElement[] childBuffer) + throws IOException, TemplateException { + final boolean hashNotEmpty; + if (listedValue instanceof TemplateHashModelEx) { + TemplateHashModelEx listedHash = (TemplateHashModelEx) listedValue; + if (listedHash instanceof TemplateHashModelEx2) { + KeyValuePairIterator kvpIter + = openedIterator == null ? ((TemplateHashModelEx2) listedHash).keyValuePairIterator() + : (KeyValuePairIterator) openedIterator; + hashNotEmpty = kvpIter.hasNext(); + if (hashNotEmpty) { + if (loopVarName != null) { + try { + do { + KeyValuePair kvp = kvpIter.next(); + loopVar = kvp.getKey(); + loopVar2 = kvp.getValue(); + hasNext = kvpIter.hasNext(); + env.visit(childBuffer); + index++; + } while (hasNext); + } catch (ASTDirBreak.Break br) { + // Silently exit loop + } + openedIterator = null; + } else { + // We will reuse this at the #iterms + openedIterator = kvpIter; + env.visit(childBuffer); + } + } + } else { // not a TemplateHashModelEx2, but still a TemplateHashModelEx + TemplateModelIterator keysIter = listedHash.keys().iterator(); + hashNotEmpty = keysIter.hasNext(); + if (hashNotEmpty) { + if (loopVarName != null) { + try { + do { + loopVar = keysIter.next(); + if (!(loopVar instanceof TemplateScalarModel)) { + throw new NonStringException(env, + new _ErrorDescriptionBuilder( + "When listing key-value pairs of traditional hash " + + "implementations, all keys must be strings, but one of them " + + "was ", + new _DelayedAOrAn(new _DelayedFTLTypeDescription(loopVar)), "." + ).tip("The listed value's TemplateModel class was ", + new _DelayedShortClassName(listedValue.getClass()), + ", which doesn't implement ", + new _DelayedShortClassName(TemplateHashModelEx2.class), + ", which leads to this restriction.")); + } + loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString()); + hasNext = keysIter.hasNext(); + env.visit(childBuffer); + index++; + } while (hasNext); + } catch (ASTDirBreak.Break br) { + // Silently exit loop + } + } else { + env.visit(childBuffer); + } + } + } + } else if (listedValue instanceof TemplateCollectionModel + || listedValue instanceof TemplateSequenceModel) { + throw new NonSequenceOrCollectionException(env, + new _ErrorDescriptionBuilder("The value you try to list is ", + new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)), + ", thus you must specify only one loop variable after the \"as\" (there's no separate " + + "key and value)." + )); + } else { + throw new NonExtendedHashException( + listedExp, listedValue, env); + } + return hashNotEmpty; + } + + String getLoopVariableName() { + return loopVarName; + } + + String getLoopVariable2Name() { + return loopVar2Name; + } + + @Override + public TemplateModel getLocalVariable(String name) { + String loopVarName = this.loopVarName; + if (loopVarName != null && name.startsWith(loopVarName)) { + switch(name.length() - loopVarName.length()) { + case 0: + return loopVar; + case 6: + if (name.endsWith(LOOP_STATE_INDEX)) { + return new SimpleNumber(index); + } + break; + case 9: + if (name.endsWith(LOOP_STATE_HAS_NEXT)) { + return hasNext ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + break; + } + } + + if (name.equals(loopVar2Name)) { + return loopVar2; + } + + return null; + } + + @Override + public Collection getLocalVariableNames() { + String loopVarName = this.loopVarName; + if (loopVarName != null) { + if (localVarNames == null) { + localVarNames = new ArrayList(3); + localVarNames.add(loopVarName); + localVarNames.add(loopVarName + LOOP_STATE_INDEX); + localVarNames.add(loopVarName + LOOP_STATE_HAS_NEXT); + } + return localVarNames; + } else { + return Collections.EMPTY_LIST; + } + } + + boolean hasNext() { + return hasNext; + } + + int getIndex() { + return index; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java new file mode 100644 index 0000000..a85b81c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java @@ -0,0 +1,88 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: When a {@code #list} has an {@code #else}, this is the parent of the two nodes. + */ +class ASTDirListElseContainer extends ASTDirective { + + private final ASTDirList listPart; + private final ASTDirElseOfList elsePart; + + public ASTDirListElseContainer(ASTDirList listPart, ASTDirElseOfList elsePart) { + setChildBufferCapacity(2); + addChild(listPart); + addChild(elsePart); + this.listPart = listPart; + this.elsePart = elsePart; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + if (!listPart.acceptWithResult(env)) { + return elsePart.accept(env); + } + return null; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + StringBuilder buf = new StringBuilder(); + int ln = getChildCount(); + for (int i = 0; i < ln; i++) { + ASTElement element = getChild(i); + buf.append(element.dump(canonical)); + } + buf.append("</#list>"); + return buf.toString(); + } else { + return getNodeTypeSymbol(); + } + } + + @Override + String getNodeTypeSymbol() { + return "#list-#else-container"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java new file mode 100644 index 0000000..5bb2712 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java @@ -0,0 +1,325 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #macro} + */ +final class ASTDirMacro extends ASTDirective implements TemplateModel { + + static final ASTDirMacro DO_NOTHING_MACRO = new ASTDirMacro(".pass", + Collections.EMPTY_LIST, + Collections.EMPTY_MAP, + null, false, + TemplateElements.EMPTY); + + final static int TYPE_MACRO = 0; + final static int TYPE_FUNCTION = 1; + + private final String name; + private final String[] paramNames; + private final Map paramDefaults; + private final String catchAllParamName; + private final boolean function; + + ASTDirMacro(String name, List argumentNames, Map args, + String catchAllParamName, boolean function, + TemplateElements children) { + this.name = name; + paramNames = (String[]) argumentNames.toArray(new String[argumentNames.size()]); + paramDefaults = args; + + this.function = function; + this.catchAllParamName = catchAllParamName; + + setChildren(children); + } + + public String getCatchAll() { + return catchAllParamName; + } + + public String[] getArgumentNames() { + return paramNames.clone(); + } + + String[] getArgumentNamesInternal() { + return paramNames; + } + + boolean hasArgNamed(String name) { + return paramDefaults.containsKey(name); + } + + public String getName() { + return name; + } + + @Override + ASTElement[] accept(Environment env) { + env.visitMacroDef(this); + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + sb.append(' '); + sb.append(_StringUtil.toFTLTopLevelTragetIdentifier(name)); + if (function) sb.append('('); + int argCnt = paramNames.length; + for (int i = 0; i < argCnt; i++) { + if (function) { + if (i != 0) { + sb.append(", "); + } + } else { + sb.append(' '); + } + String argName = paramNames[i]; + sb.append(_StringUtil.toFTLTopLevelIdentifierReference(argName)); + if (paramDefaults != null && paramDefaults.get(argName) != null) { + sb.append('='); + ASTExpression defaultExpr = (ASTExpression) paramDefaults.get(argName); + if (function) { + sb.append(defaultExpr.getCanonicalForm()); + } else { + MessageUtil.appendExpressionAsUntearable(sb, defaultExpr); + } + } + } + if (catchAllParamName != null) { + if (function) { + if (argCnt != 0) { + sb.append(", "); + } + } else { + sb.append(' '); + } + sb.append(catchAllParamName); + sb.append("..."); + } + if (function) sb.append(')'); + if (canonical) { + sb.append('>'); + sb.append(getChildrenCanonicalForm()); + sb.append("</").append(getNodeTypeSymbol()).append('>'); + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return function ? "#function" : "#macro"; + } + + public boolean isFunction() { + return function; + } + + class Context implements LocalContext { + final Environment.Namespace localVars; + final ASTElement[] nestedContentBuffer; + final Environment.Namespace nestedContentNamespace; + final List nestedContentParameterNames; + final LocalContextStack prevLocalContextStack; + final Context prevMacroContext; + + Context(Environment env, + ASTElement[] nestedContentBuffer, + List nestedContentParameterNames) { + localVars = env.new Namespace(); + this.nestedContentBuffer = nestedContentBuffer; + nestedContentNamespace = env.getCurrentNamespace(); + this.nestedContentParameterNames = nestedContentParameterNames; + prevLocalContextStack = env.getLocalContextStack(); + prevMacroContext = env.getCurrentMacroContext(); + } + + + ASTDirMacro getMacro() { + return ASTDirMacro.this; + } + + // Set default parameters, check if all the required parameters are defined. + void sanityCheck(Environment env) throws TemplateException { + boolean resolvedAnArg, hasUnresolvedArg; + ASTExpression firstUnresolvedExpression; + InvalidReferenceException firstReferenceException; + do { + firstUnresolvedExpression = null; + firstReferenceException = null; + resolvedAnArg = hasUnresolvedArg = false; + for (int i = 0; i < paramNames.length; ++i) { + String argName = paramNames[i]; + if (localVars.get(argName) == null) { + ASTExpression valueExp = (ASTExpression) paramDefaults.get(argName); + if (valueExp != null) { + try { + TemplateModel tm = valueExp.eval(env); + if (tm == null) { + if (!hasUnresolvedArg) { + firstUnresolvedExpression = valueExp; + hasUnresolvedArg = true; + } + } else { + localVars.put(argName, tm); + resolvedAnArg = true; + } + } catch (InvalidReferenceException e) { + if (!hasUnresolvedArg) { + hasUnresolvedArg = true; + firstReferenceException = e; + } + } + } else { + boolean argWasSpecified = localVars.containsKey(argName); + throw new _MiscTemplateException(env, + new _ErrorDescriptionBuilder( + "When calling macro ", new _DelayedJQuote(name), + ", required parameter ", new _DelayedJQuote(argName), + " (parameter #", Integer.valueOf(i + 1), ") was ", + (argWasSpecified + ? "specified, but had null/missing value." + : "not specified.") + ).tip(argWasSpecified + ? new Object[] { + "If the parameter value expression on the caller side is known to " + + "be legally null/missing, you may want to specify a default " + + "value for it with the \"!\" operator, like " + + "paramValue!defaultValue." } + : new Object[] { + "If the omission was deliberate, you may consider making the " + + "parameter optional in the macro by specifying a default value " + + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" } + )); + } + } + } + } while (resolvedAnArg && hasUnresolvedArg); + if (hasUnresolvedArg) { + if (firstReferenceException != null) { + throw firstReferenceException; + } else { + throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env); + } + } + } + + /** + * @return the local variable of the given name + * or null if it doesn't exist. + */ + @Override + public TemplateModel getLocalVariable(String name) throws TemplateModelException { + return localVars.get(name); + } + + Environment.Namespace getLocals() { + return localVars; + } + + /** + * Set a local variable in this macro + */ + void setLocalVar(String name, TemplateModel var) { + localVars.put(name, var); + } + + @Override + public Collection getLocalVariableNames() throws TemplateModelException { + HashSet result = new HashSet(); + for (TemplateModelIterator it = localVars.keys().iterator(); it.hasNext(); ) { + result.add(it.next().toString()); + } + return result; + } + } + + @Override + int getParameterCount() { + return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/; + } + + @Override + Object getParameterValue(int idx) { + if (idx == 0) { + return name; + } else { + final int argDescsEnd = paramNames.length * 2 + 1; + if (idx < argDescsEnd) { + String paramName = paramNames[(idx - 1) / 2]; + if (idx % 2 != 0) { + return paramName; + } else { + return paramDefaults.get(paramName); + } + } else if (idx == argDescsEnd) { + return catchAllParamName; + } else if (idx == argDescsEnd + 1) { + return Integer.valueOf(function ? TYPE_FUNCTION : TYPE_MACRO); + } else { + throw new IndexOutOfBoundsException(); + } + } + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx == 0) { + return ParameterRole.ASSIGNMENT_TARGET; + } else { + final int argDescsEnd = paramNames.length * 2 + 1; + if (idx < argDescsEnd) { + if (idx % 2 != 0) { + return ParameterRole.PARAMETER_NAME; + } else { + return ParameterRole.PARAMETER_DEFAULT; + } + } else if (idx == argDescsEnd) { + return ParameterRole.CATCH_ALL_PARAMETER_NAME; + } else if (idx == argDescsEnd + 1) { + return ParameterRole.AST_NODE_SUBTYPE; + } else { + throw new IndexOutOfBoundsException(); + } + } + } + + @Override + boolean isNestedBlockRepeater() { + // Because of recursive calls + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java new file mode 100644 index 0000000..f08d3b2 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java @@ -0,0 +1,159 @@ +/* + * 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.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; + +/** + * AST directive node: {@code #nested}. + */ +final class ASTDirNested extends ASTDirective { + + + private List bodyParameters; + + + ASTDirNested(List bodyParameters) { + this.bodyParameters = bodyParameters; + } + + List getBodyParameters() { + return bodyParameters; + } + + /** + * There is actually a subtle but essential point in the code below. + * A macro operates in the context in which it's defined. However, + * a nested block within a macro instruction is defined in the + * context in which the macro was invoked. So, we actually need to + * temporarily switch the namespace and macro context back to + * what it was before macro invocation to implement this properly. + * I (JR) realized this thanks to some incisive comments from Daniel Dekany. + */ + @Override + ASTElement[] accept(Environment env) throws IOException, TemplateException { + Context bodyContext = new Context(env); + env.invokeNestedContent(bodyContext); + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + if (bodyParameters != null) { + for (int i = 0; i < bodyParameters.size(); i++) { + sb.append(' '); + sb.append(((ASTExpression) bodyParameters.get(i)).getCanonicalForm()); + } + } + if (canonical) sb.append('>'); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#nested"; + } + + @Override + int getParameterCount() { + return bodyParameters != null ? bodyParameters.size() : 0; + } + + @Override + Object getParameterValue(int idx) { + checkIndex(idx); + return bodyParameters.get(idx); + } + + @Override + ParameterRole getParameterRole(int idx) { + checkIndex(idx); + return ParameterRole.PASSED_VALUE; + } + + private void checkIndex(int idx) { + if (bodyParameters == null || idx >= bodyParameters.size()) { + throw new IndexOutOfBoundsException(); + } + } + + /* + boolean heedsOpeningWhitespace() { + return true; + } + + boolean heedsTrailingWhitespace() { + return true; + } + */ + + @Override + boolean isShownInStackTrace() { + return true; + } + + class Context implements LocalContext { + ASTDirMacro.Context invokingMacroContext; + Environment.Namespace bodyVars; + + Context(Environment env) throws TemplateException { + invokingMacroContext = env.getCurrentMacroContext(); + List bodyParameterNames = invokingMacroContext.nestedContentParameterNames; + if (bodyParameters != null) { + for (int i = 0; i < bodyParameters.size(); i++) { + ASTExpression exp = (ASTExpression) bodyParameters.get(i); + TemplateModel tm = exp.eval(env); + if (bodyParameterNames != null && i < bodyParameterNames.size()) { + String bodyParameterName = (String) bodyParameterNames.get(i); + if (bodyVars == null) { + bodyVars = env.new Namespace(); + } + bodyVars.put(bodyParameterName, tm); + } + } + } + } + + @Override + public TemplateModel getLocalVariable(String name) throws TemplateModelException { + return bodyVars == null ? null : bodyVars.get(name); + } + + @Override + public Collection getLocalVariableNames() { + List bodyParameterNames = invokingMacroContext.nestedContentParameterNames; + return bodyParameterNames == null ? Collections.EMPTY_LIST : bodyParameterNames; + } + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java new file mode 100644 index 0000000..f1d1f43 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java @@ -0,0 +1,77 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: {@code #noautoesc}. + */ +final class ASTDirNoAutoEsc extends ASTDirective { + + ASTDirNoAutoEsc(TemplateElements children) { + setChildren(children); + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + return "<" + getNodeTypeSymbol() + "\">" + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">"; + } else { + return getNodeTypeSymbol(); + } + } + + @Override + String getNodeTypeSymbol() { + return "#noautoesc"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isIgnorable(boolean stripWhitespace) { + return getChildCount() == 0; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java new file mode 100644 index 0000000..e2f3648 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java @@ -0,0 +1,78 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: {@code #noescape}. + */ +class ASTDirNoEscape extends ASTDirective { + + ASTDirNoEscape(TemplateElements children) { + setChildren(children); + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + return "<" + getNodeTypeSymbol() + '>' + getChildrenCanonicalForm() + + "</" + getNodeTypeSymbol() + '>'; + } else { + return getNodeTypeSymbol(); + } + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + String getNodeTypeSymbol() { + return "#noescape"; + } + + @Override + boolean isOutputCacheable() { + return true; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java new file mode 100644 index 0000000..ee59a0c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java @@ -0,0 +1,85 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: {@code #outputformat}. + */ +final class ASTDirOutputFormat extends ASTDirective { + + private final ASTExpression paramExp; + + ASTDirOutputFormat(TemplateElements children, ASTExpression paramExp) { + this.paramExp = paramExp; + setChildren(children); + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + return "<" + getNodeTypeSymbol() + " \"" + paramExp.getCanonicalForm() + "\">" + + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">"; + } else { + return getNodeTypeSymbol(); + } + } + + @Override + String getNodeTypeSymbol() { + return "#outputformat"; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx == 0) return paramExp; + else + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx == 0) return ParameterRole.VALUE; + else + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isIgnorable(boolean stripWhitespace) { + return getChildCount() == 0; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java new file mode 100644 index 0000000..f19e9b2 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java @@ -0,0 +1,75 @@ +/* + * 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.io.IOException; + +/** + * AST directive node: {@code #recover}. + */ +final class ASTDirRecover extends ASTDirective { + + ASTDirRecover(TemplateElements children) { + setChildren(children); + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + StringBuilder buf = new StringBuilder(); + buf.append('<').append(getNodeTypeSymbol()).append('>'); + buf.append(getChildrenCanonicalForm()); + return buf.toString(); + } else { + return getNodeTypeSymbol(); + } + } + + @Override + String getNodeTypeSymbol() { + return "#recover"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +}
