http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java new file mode 100644 index 0000000..b95b3fc --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java @@ -0,0 +1,130 @@ +/* + * 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.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNodeModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; + + +/** + * AST directive node: {@code #recurse}. + */ +final class ASTDirRecurse extends ASTDirective { + + ASTExpression targetNode, namespaces; + + ASTDirRecurse(ASTExpression targetNode, ASTExpression namespaces) { + this.targetNode = targetNode; + this.namespaces = namespaces; + } + + @Override + ASTElement[] accept(Environment env) throws IOException, TemplateException { + TemplateModel node = targetNode == null ? null : targetNode.eval(env); + if (node != null && !(node instanceof TemplateNodeModel)) { + throw new NonNodeException(targetNode, node, "node", env); + } + + TemplateModel nss = namespaces == null ? null : namespaces.eval(env); + if (namespaces instanceof ASTExpStringLiteral) { + nss = env.importLib(((TemplateScalarModel) nss).getAsString(), null); + } else if (namespaces instanceof ASTExpListLiteral) { + nss = ((ASTExpListLiteral) namespaces).evaluateStringsToNamespaces(env); + } + if (nss != null) { + if (nss instanceof TemplateHashModel) { + NativeSequence ss = new NativeSequence(1); + ss.add(nss); + nss = ss; + } else if (!(nss instanceof TemplateSequenceModel)) { + if (namespaces != null) { + throw new NonSequenceException(namespaces, nss, env); + } else { + // Should not occur + throw new _MiscTemplateException(env, "Expecting a sequence of namespaces after \"using\""); + } + } + } + + env.recurse((TemplateNodeModel) node, (TemplateSequenceModel) nss); + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + if (targetNode != null) { + sb.append(' '); + sb.append(targetNode.getCanonicalForm()); + } + if (namespaces != null) { + sb.append(" using "); + sb.append(namespaces.getCanonicalForm()); + } + if (canonical) sb.append("/>"); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#recurse"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return targetNode; + case 1: return namespaces; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.NODE; + case 1: return ParameterRole.NAMESPACE; + default: 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/ASTDirReturn.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java new file mode 100644 index 0000000..0e82a74 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java @@ -0,0 +1,91 @@ +/* + * 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; + +/** + * AST directive node: {@code #return}. + */ +final class ASTDirReturn extends ASTDirective { + + private ASTExpression exp; + + ASTDirReturn(ASTExpression exp) { + this.exp = exp; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException { + if (exp != null) { + env.setLastReturnValue(exp.eval(env)); + } + if (nextSibling() == null && getParent() instanceof ASTDirMacro) { + // Avoid unnecessary exception throwing + return null; + } + throw Return.INSTANCE; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + if (exp != null) { + sb.append(' '); + sb.append(exp.getCanonicalForm()); + } + if (canonical) sb.append("/>"); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#return"; + } + + public static class Return extends RuntimeException { + static final Return INSTANCE = new Return(); + private Return() { + } + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return exp; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.VALUE; + } + + @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/ASTDirSep.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java new file mode 100644 index 0000000..9e83e83 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java @@ -0,0 +1,89 @@ +/* + * 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; + +/** + * AST directive node: {@code #sep}. + */ +class ASTDirSep extends ASTDirective { + + public ASTDirSep(TemplateElements children) { + 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"); + } + + if (iterCtx.hasNext()) { + return getChildBuffer(); + } + return null; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + if (canonical) { + sb.append('>'); + sb.append(getChildrenCanonicalForm()); + sb.append("</"); + sb.append(getNodeTypeSymbol()); + sb.append('>'); + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#sep"; + } + + @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/ASTDirSetting.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java new file mode 100644 index 0000000..68a0672 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java @@ -0,0 +1,172 @@ +/* + * 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.Arrays; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST directive node: {@code #setting}. + */ +final class ASTDirSetting extends ASTDirective { + + private final String key; + private final ASTExpression value; + + static final String[] SETTING_NAMES = new String[] { + // Must be sorted alphabetically! + MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY_CAMEL_CASE, + MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY_SNAKE_CASE, + MutableProcessingConfiguration.DATE_FORMAT_KEY_CAMEL_CASE, + MutableProcessingConfiguration.DATE_FORMAT_KEY_SNAKE_CASE, + MutableProcessingConfiguration.DATETIME_FORMAT_KEY_CAMEL_CASE, + MutableProcessingConfiguration.DATETIME_FORMAT_KEY_SNAKE_CASE, + MutableProcessingConfiguration.LOCALE_KEY, + MutableProcessingConfiguration.NUMBER_FORMAT_KEY_CAMEL_CASE, + MutableProcessingConfiguration.NUMBER_FORMAT_KEY_SNAKE_CASE, + MutableProcessingConfiguration.OUTPUT_ENCODING_KEY_CAMEL_CASE, + MutableProcessingConfiguration.OUTPUT_ENCODING_KEY_SNAKE_CASE, + MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE, + MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY, + MutableProcessingConfiguration.TIME_FORMAT_KEY_CAMEL_CASE, + MutableProcessingConfiguration.TIME_ZONE_KEY_CAMEL_CASE, + MutableProcessingConfiguration.TIME_FORMAT_KEY_SNAKE_CASE, + MutableProcessingConfiguration.TIME_ZONE_KEY_SNAKE_CASE, + MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY_CAMEL_CASE, + MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY_SNAKE_CASE + }; + + ASTDirSetting(Token keyTk, FMParserTokenManager tokenManager, ASTExpression value, Configuration cfg) + throws ParseException { + String key = keyTk.image; + if (Arrays.binarySearch(SETTING_NAMES, key) < 0) { + StringBuilder sb = new StringBuilder(); + if (Configuration.ExtendableBuilder.getSettingNames(true).contains(key) + || Configuration.ExtendableBuilder.getSettingNames(false).contains(key)) { + sb.append("The setting name is recognized, but changing this setting from inside a template isn't " + + "supported."); + } else { + sb.append("Unknown setting name: "); + sb.append(_StringUtil.jQuote(key)).append("."); + sb.append(" The allowed setting names are: "); + + int shownNamingConvention; + { + int namingConvention = tokenManager.namingConvention; + shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION + ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */; + } + + boolean first = true; + for (String correctName : SETTING_NAMES) { + int correctNameNamingConvention = _StringUtil.getIdentifierNamingConvention(correctName); + if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION + ? correctNameNamingConvention != ParsingConfiguration.LEGACY_NAMING_CONVENTION + : correctNameNamingConvention != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) { + if (first) { + first = false; + } else { + sb.append(", "); + } + + sb.append(correctName); + } + } + } + throw new ParseException(sb.toString(), null, keyTk); + } + + this.key = key; + this.value = value; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException { + TemplateModel mval = value.eval(env); + String strval; + if (mval instanceof TemplateScalarModel) { + strval = ((TemplateScalarModel) mval).getAsString(); + } else if (mval instanceof TemplateBooleanModel) { + strval = ((TemplateBooleanModel) mval).getAsBoolean() ? "true" : "false"; + } else if (mval instanceof TemplateNumberModel) { + strval = ((TemplateNumberModel) mval).getAsNumber().toString(); + } else { + strval = value.evalAndCoerceToStringOrUnsupportedMarkup(env); + } + try { + env.setSetting(key, strval); + } catch (ConfigurationException e) { + throw new _MiscTemplateException(env, e.getMessage(), e.getCause()); + } + 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(key)); + sb.append('='); + sb.append(value.getCanonicalForm()); + if (canonical) sb.append("/>"); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#setting"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return key; + case 1: return value; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.ITEM_KEY; + case 1: return ParameterRole.ITEM_VALUE; + 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/ASTDirStop.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java new file mode 100644 index 0000000..f453734 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java @@ -0,0 +1,81 @@ +/* + * 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; + +/** + * AST directive node: {@code #stop}. + */ +final class ASTDirStop extends ASTDirective { + + private ASTExpression exp; + + ASTDirStop(ASTExpression exp) { + this.exp = exp; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException { + if (exp == null) { + throw new StopException(env); + } + throw new StopException(env, exp.evalAndCoerceToPlainText(env)); + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + if (exp != null) { + sb.append(' '); + sb.append(exp.getCanonicalForm()); + } + if (canonical) sb.append("/>"); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#stop"; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return exp; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.MESSAGE; + } + + @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/ASTDirSwitch.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java new file mode 100644 index 0000000..e66c419 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java @@ -0,0 +1,129 @@ +/* + * 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 #switch}. + */ +final class ASTDirSwitch extends ASTDirective { + + private ASTDirCase defaultCase; + private final ASTExpression searched; + + /** + * @param searched the expression to be tested. + */ + ASTDirSwitch(ASTExpression searched) { + this.searched = searched; + setChildBufferCapacity(4); + } + + void addCase(ASTDirCase cas) { + if (cas.condition == null) { + defaultCase = cas; + } + addChild(cas); + } + + @Override + ASTElement[] accept(Environment env) + throws TemplateException, IOException { + boolean processedCase = false; + int ln = getChildCount(); + try { + for (int i = 0; i < ln; i++) { + ASTDirCase cas = (ASTDirCase) getChild(i); + boolean processCase = false; + + // Fall through if a previous case tested true. + if (processedCase) { + processCase = true; + } else if (cas.condition != null) { + // Otherwise, if this case isn't the default, test it. + processCase = _EvalUtil.compare( + searched, + _EvalUtil.CMP_OP_EQUALS, "case==", cas.condition, cas.condition, env); + } + if (processCase) { + env.visit(cas); + processedCase = true; + } + } + + // If we didn't process any nestedElements, and we have a default, + // process it. + if (!processedCase && defaultCase != null) { + env.visit(defaultCase); + } + } catch (ASTDirBreak.Break br) { + // #break was called + } + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder buf = new StringBuilder(); + if (canonical) buf.append('<'); + buf.append(getNodeTypeSymbol()); + buf.append(' '); + buf.append(searched.getCanonicalForm()); + if (canonical) { + buf.append('>'); + int ln = getChildCount(); + for (int i = 0; i < ln; i++) { + ASTDirCase cas = (ASTDirCase) getChild(i); + buf.append(cas.getCanonicalForm()); + } + buf.append("</").append(getNodeTypeSymbol()).append('>'); + } + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#switch"; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return searched; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.VALUE; + } + + @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/ASTDirTOrTrOrTl.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java new file mode 100644 index 0000000..937bc18 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java @@ -0,0 +1,109 @@ +/* + * 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; + +/** + * AST directive node: {@code #t}, {@code #tr}, {@code #tl}. + */ +final class ASTDirTOrTrOrTl extends ASTDirective { + + private static final int TYPE_T = 0; + private static final int TYPE_LT = 1; + private static final int TYPE_RT = 2; + private static final int TYPE_NT = 3; + + final boolean left, right; + + ASTDirTOrTrOrTl(boolean left, boolean right) { + this.left = left; + this.right = right; + } + + @Override + ASTElement[] accept(Environment env) { + // This instruction does nothing at render-time, only parse-time. + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + if (canonical) sb.append("/>"); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + if (left && right) { + return "#t"; + } else if (left) { + return "#lt"; + } else if (right) { + return "#rt"; + } else { + return "#nt"; + } + } + + @Override + boolean isIgnorable(boolean stripWhitespace) { + return true; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + int type; + if (left && right) { + type = TYPE_T; + } else if (left) { + type = TYPE_LT; + } else if (right) { + type = TYPE_RT; + } else { + type = TYPE_NT; + } + return Integer.valueOf(type); + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.AST_NODE_SUBTYPE; + } + + @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/ASTDirUserDefined.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java new file mode 100644 index 0000000..6042bd8 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java @@ -0,0 +1,343 @@ +/* + * 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.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateTransformModel; +import org.apache.freemarker.core.util.ObjectFactory; +import org.apache.freemarker.core.util._StringUtil; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * AST directive node: {@code <@exp .../>} or {@code <@exp ...>...</@...>}. Calls an user-defined directive (like a + * macro). + */ +final class ASTDirUserDefined extends ASTDirective implements DirectiveCallPlace { + + private ASTExpression nameExp; + private Map namedArgs; + private List positionalArgs, bodyParameterNames; + private transient volatile SoftReference/*List<Map.Entry<String,ASTExpression>>*/ sortedNamedArgsCache; + private CustomDataHolder customDataHolder; + + ASTDirUserDefined(ASTExpression nameExp, + Map namedArgs, + TemplateElements children, + List bodyParameterNames) { + this.nameExp = nameExp; + this.namedArgs = namedArgs; + setChildren(children); + this.bodyParameterNames = bodyParameterNames; + } + + ASTDirUserDefined(ASTExpression nameExp, + List positionalArgs, + TemplateElements children, + List bodyParameterNames) { + this.nameExp = nameExp; + this.positionalArgs = positionalArgs; + setChildren(children); + this.bodyParameterNames = bodyParameterNames; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + TemplateModel tm = nameExp.eval(env); + if (tm == ASTDirMacro.DO_NOTHING_MACRO) return null; // shortcut here. + if (tm instanceof ASTDirMacro) { + ASTDirMacro macro = (ASTDirMacro) tm; + if (macro.isFunction()) { + throw new _MiscTemplateException(env, + "Routine ", new _DelayedJQuote(macro.getName()), " is a function, not a directive. " + + "Functions can only be called from expressions, like in ${f()}, ${x + f()} or ", + "<@someDirective someParam=f() />", "."); + } + env.invoke(macro, namedArgs, positionalArgs, bodyParameterNames, getChildBuffer()); + } else { + boolean isDirectiveModel = tm instanceof TemplateDirectiveModel; + if (isDirectiveModel || tm instanceof TemplateTransformModel) { + Map args; + if (namedArgs != null && !namedArgs.isEmpty()) { + args = new HashMap(); + for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = (Map.Entry) it.next(); + String key = (String) entry.getKey(); + ASTExpression valueExp = (ASTExpression) entry.getValue(); + TemplateModel value = valueExp.eval(env); + args.put(key, value); + } + } else { + args = Collections.emptyMap(); + } + if (isDirectiveModel) { + env.visit(getChildBuffer(), (TemplateDirectiveModel) tm, args, bodyParameterNames); + } else { + env.visitAndTransform(getChildBuffer(), (TemplateTransformModel) tm, args); + } + } else if (tm == null) { + throw InvalidReferenceException.getInstance(nameExp, env); + } else { + throw new NonUserDefinedDirectiveLikeException(nameExp, tm, env); + } + } + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append('@'); + MessageUtil.appendExpressionAsUntearable(sb, nameExp); + boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')'; + if (positionalArgs != null) { + for (int i = 0; i < positionalArgs.size(); i++) { + ASTExpression argExp = (ASTExpression) positionalArgs.get(i); + if (i != 0) { + sb.append(','); + } + sb.append(' '); + sb.append(argExp.getCanonicalForm()); + } + } else { + List entries = getSortedNamedArgs(); + for (int i = 0; i < entries.size(); i++) { + Map.Entry entry = (Map.Entry) entries.get(i); + ASTExpression argExp = (ASTExpression) entry.getValue(); + sb.append(' '); + sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) entry.getKey())); + sb.append('='); + MessageUtil.appendExpressionAsUntearable(sb, argExp); + } + } + if (bodyParameterNames != null && !bodyParameterNames.isEmpty()) { + sb.append("; "); + for (int i = 0; i < bodyParameterNames.size(); i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) bodyParameterNames.get(i))); + } + } + if (canonical) { + if (getChildCount() == 0) { + sb.append("/>"); + } else { + sb.append('>'); + sb.append(getChildrenCanonicalForm()); + sb.append("</@"); + if (!nameIsInParen + && (nameExp instanceof ASTExpVariable + || (nameExp instanceof ASTExpDot && ((ASTExpDot) nameExp).onlyHasIdentifiers()))) { + sb.append(nameExp.getCanonicalForm()); + } + sb.append('>'); + } + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "@"; + } + + @Override + int getParameterCount() { + return 1/*nameExp*/ + + (positionalArgs != null ? positionalArgs.size() : 0) + + (namedArgs != null ? namedArgs.size() * 2 : 0) + + (bodyParameterNames != null ? bodyParameterNames.size() : 0); + } + + @Override + Object getParameterValue(int idx) { + if (idx == 0) { + return nameExp; + } else { + int base = 1; + final int positionalArgsSize = positionalArgs != null ? positionalArgs.size() : 0; + if (idx - base < positionalArgsSize) { + return positionalArgs.get(idx - base); + } else { + base += positionalArgsSize; + final int namedArgsSize = namedArgs != null ? namedArgs.size() : 0; + if (idx - base < namedArgsSize * 2) { + Map.Entry namedArg = (Map.Entry) getSortedNamedArgs().get((idx - base) / 2); + return (idx - base) % 2 == 0 ? namedArg.getKey() : namedArg.getValue(); + } else { + base += namedArgsSize * 2; + final int bodyParameterNamesSize = bodyParameterNames != null ? bodyParameterNames.size() : 0; + if (idx - base < bodyParameterNamesSize) { + return bodyParameterNames.get(idx - base); + } else { + throw new IndexOutOfBoundsException(); + } + } + } + } + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx == 0) { + return ParameterRole.CALLEE; + } else { + int base = 1; + final int positionalArgsSize = positionalArgs != null ? positionalArgs.size() : 0; + if (idx - base < positionalArgsSize) { + return ParameterRole.ARGUMENT_VALUE; + } else { + base += positionalArgsSize; + final int namedArgsSize = namedArgs != null ? namedArgs.size() : 0; + if (idx - base < namedArgsSize * 2) { + return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE; + } else { + base += namedArgsSize * 2; + final int bodyParameterNamesSize = bodyParameterNames != null ? bodyParameterNames.size() : 0; + if (idx - base < bodyParameterNamesSize) { + return ParameterRole.TARGET_LOOP_VARIABLE; + } else { + throw new IndexOutOfBoundsException(); + } + } + } + } + } + + /** + * Returns the named args by source-code order; it's not meant to be used during template execution, too slow for + * that! + */ + private List/*<Map.Entry<String, ASTExpression>>*/ getSortedNamedArgs() { + Reference ref = sortedNamedArgsCache; + if (ref != null) { + List res = (List) ref.get(); + if (res != null) return res; + } + + List res = MiscUtil.sortMapOfExpressions(namedArgs); + sortedNamedArgsCache = new SoftReference(res); + return res; + } + + @Override + @SuppressFBWarnings(value={ "IS2_INCONSISTENT_SYNC", "DC_DOUBLECHECK" }, justification="Performance tricks") + public Object getOrCreateCustomData(Object providerIdentity, ObjectFactory objectFactory) + throws CallPlaceCustomDataInitializationException { + // We are using double-checked locking, utilizing Java memory model "final" trick. + // Note that this.customDataHolder is NOT volatile. + + CustomDataHolder customDataHolder = this.customDataHolder; // Findbugs false alarm + if (customDataHolder == null) { // Findbugs false alarm + synchronized (this) { + customDataHolder = this.customDataHolder; + if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) { + customDataHolder = createNewCustomData(providerIdentity, objectFactory); + this.customDataHolder = customDataHolder; + } + } + } + + if (customDataHolder.providerIdentity != providerIdentity) { + synchronized (this) { + customDataHolder = this.customDataHolder; + if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) { + customDataHolder = createNewCustomData(providerIdentity, objectFactory); + this.customDataHolder = customDataHolder; + } + } + } + + return customDataHolder.customData; + } + + private CustomDataHolder createNewCustomData(Object provierIdentity, ObjectFactory objectFactory) + throws CallPlaceCustomDataInitializationException { + CustomDataHolder customDataHolder; + Object customData; + try { + customData = objectFactory.createObject(); + } catch (Exception e) { + throw new CallPlaceCustomDataInitializationException( + "Failed to initialize custom data for provider identity " + + _StringUtil.tryToString(provierIdentity) + " via factory " + + _StringUtil.tryToString(objectFactory), e); + } + if (customData == null) { + throw new NullPointerException("ObjectFactory.createObject() has returned null"); + } + customDataHolder = new CustomDataHolder(provierIdentity, customData); + return customDataHolder; + } + + @Override + public boolean isNestedOutputCacheable() { + return isChildrenOutputCacheable(); + } + +/* + //REVISIT + boolean heedsOpeningWhitespace() { + return nestedBlock == null; + } + + //REVISIT + boolean heedsTrailingWhitespace() { + return nestedBlock == null; + }*/ + + /** + * Used for implementing double check locking in implementing the + * {@link DirectiveCallPlace#getOrCreateCustomData(Object, ObjectFactory)}. + */ + private static class CustomDataHolder { + + private final Object providerIdentity; + private final Object customData; + public CustomDataHolder(Object providerIdentity, Object customData) { + this.providerIdentity = providerIdentity; + this.customData = customData; + } + + } + + @Override + boolean isNestedBlockRepeater() { + 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/ASTDirVisit.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java new file mode 100644 index 0000000..4a4023b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java @@ -0,0 +1,126 @@ +/* + * 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.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNodeModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; + + +/** + * AST directive node: {@code #visit}. + */ +final class ASTDirVisit extends ASTDirective { + + ASTExpression targetNode, namespaces; + + ASTDirVisit(ASTExpression targetNode, ASTExpression namespaces) { + this.targetNode = targetNode; + this.namespaces = namespaces; + } + + @Override + ASTElement[] accept(Environment env) throws IOException, TemplateException { + TemplateModel node = targetNode.eval(env); + if (!(node instanceof TemplateNodeModel)) { + throw new NonNodeException(targetNode, node, env); + } + + TemplateModel nss = namespaces == null ? null : namespaces.eval(env); + if (namespaces instanceof ASTExpStringLiteral) { + nss = env.importLib(((TemplateScalarModel) nss).getAsString(), null); + } else if (namespaces instanceof ASTExpListLiteral) { + nss = ((ASTExpListLiteral) namespaces).evaluateStringsToNamespaces(env); + } + if (nss != null) { + if (nss instanceof Environment.Namespace) { + NativeSequence ss = new NativeSequence(1); + ss.add(nss); + nss = ss; + } else if (!(nss instanceof TemplateSequenceModel)) { + if (namespaces != null) { + throw new NonSequenceException(namespaces, nss, env); + } else { + // Should not occur + throw new _MiscTemplateException(env, "Expecting a sequence of namespaces after \"using\""); + } + } + } + env.invokeNodeHandlerFor((TemplateNodeModel) node, (TemplateSequenceModel) nss); + return null; + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + sb.append(' '); + sb.append(targetNode.getCanonicalForm()); + if (namespaces != null) { + sb.append(" using "); + sb.append(namespaces.getCanonicalForm()); + } + if (canonical) sb.append("/>"); + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#visit"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return targetNode; + case 1: return namespaces; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.NODE; + case 1: return ParameterRole.NAMESPACE; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + boolean isNestedBlockRepeater() { + 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/ASTDirective.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java new file mode 100644 index 0000000..778fed1 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java @@ -0,0 +1,98 @@ +/* + * 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 java.util.Set; +import java.util.TreeSet; + +/** + * AST directive node superclass. + */ +abstract class ASTDirective extends ASTElement { + + private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames, + String commonName) { + allNames.add(commonName); + lcNames.add(commonName); + ccNames.add(commonName); + } + + private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames, + String lcName, String ccName) { + allNames.add(lcName); + allNames.add(ccName); + lcNames.add(lcName); + ccNames.add(ccName); + } + + static final Set<String> ALL_BUILT_IN_DIRECTIVE_NAMES; + static final Set<String> LEGACY_BUILT_IN_DIRECTIVE_NAMES; + static final Set<String> CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES; + static { + Set<String> allNames = new TreeSet(); + Set<String> lcNames = new TreeSet(); + Set<String> ccNames = new TreeSet(); + + addName(allNames, lcNames, ccNames, "assign"); + addName(allNames, lcNames, ccNames, "attempt"); + addName(allNames, lcNames, ccNames, "autoesc", "autoEsc"); + addName(allNames, lcNames, ccNames, "break"); + addName(allNames, lcNames, ccNames, "case"); + addName(allNames, lcNames, ccNames, "compress"); + addName(allNames, lcNames, ccNames, "default"); + addName(allNames, lcNames, ccNames, "else"); + addName(allNames, lcNames, ccNames, "elseif", "elseIf"); + addName(allNames, lcNames, ccNames, "escape"); + addName(allNames, lcNames, ccNames, "fallback"); + addName(allNames, lcNames, ccNames, "flush"); + addName(allNames, lcNames, ccNames, "ftl"); + addName(allNames, lcNames, ccNames, "function"); + addName(allNames, lcNames, ccNames, "global"); + addName(allNames, lcNames, ccNames, "if"); + addName(allNames, lcNames, ccNames, "import"); + addName(allNames, lcNames, ccNames, "include"); + addName(allNames, lcNames, ccNames, "items"); + addName(allNames, lcNames, ccNames, "list"); + addName(allNames, lcNames, ccNames, "local"); + addName(allNames, lcNames, ccNames, "lt"); + addName(allNames, lcNames, ccNames, "macro"); + addName(allNames, lcNames, ccNames, "nested"); + addName(allNames, lcNames, ccNames, "noautoesc", "noAutoEsc"); + addName(allNames, lcNames, ccNames, "noescape", "noEscape"); + addName(allNames, lcNames, ccNames, "noparse", "noParse"); + addName(allNames, lcNames, ccNames, "nt"); + addName(allNames, lcNames, ccNames, "outputformat", "outputFormat"); + addName(allNames, lcNames, ccNames, "recover"); + addName(allNames, lcNames, ccNames, "recurse"); + addName(allNames, lcNames, ccNames, "return"); + addName(allNames, lcNames, ccNames, "rt"); + addName(allNames, lcNames, ccNames, "sep"); + addName(allNames, lcNames, ccNames, "setting"); + addName(allNames, lcNames, ccNames, "stop"); + addName(allNames, lcNames, ccNames, "switch"); + addName(allNames, lcNames, ccNames, "t"); + addName(allNames, lcNames, ccNames, "visit"); + + ALL_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(allNames); + LEGACY_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(lcNames); + CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(ccNames); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java new file mode 100644 index 0000000..1e5a7b4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java @@ -0,0 +1,151 @@ +/* + * 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.io.Writer; + +import org.apache.freemarker.core.model.TemplateMarkupOutputModel; +import org.apache.freemarker.core.outputformat.MarkupOutputFormat; +import org.apache.freemarker.core.outputformat.OutputFormat; +import org.apache.freemarker.core.util.FTLUtil; + +/** + * AST interpolation node: <tt>${exp}</tt> + */ +final class ASTDollarInterpolation extends ASTInterpolation { + + private final ASTExpression expression; + + /** For {@code #escape x as ...} (legacy auto-escaping) */ + private final ASTExpression escapedExpression; + + /** For OutputFormat-based auto-escaping */ + private final OutputFormat outputFormat; + private final MarkupOutputFormat markupOutputFormat; + private final boolean autoEscape; + + ASTDollarInterpolation( + ASTExpression expression, ASTExpression escapedExpression, + OutputFormat outputFormat, boolean autoEscape) { + this.expression = expression; + this.escapedExpression = escapedExpression; + this.outputFormat = outputFormat; + markupOutputFormat + = (MarkupOutputFormat) (outputFormat instanceof MarkupOutputFormat ? outputFormat : null); + this.autoEscape = autoEscape; + } + + /** + * Outputs the string value of the enclosed expression. + */ + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + final Object moOrStr = calculateInterpolatedStringOrMarkup(env); + final Writer out = env.getOut(); + if (moOrStr instanceof String) { + final String s = (String) moOrStr; + if (autoEscape) { + markupOutputFormat.output(s, out); + } else { + out.write(s); + } + } else { + final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr; + final MarkupOutputFormat moOF = mo.getOutputFormat(); + // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic! + if (moOF != outputFormat && !outputFormat.isOutputFormatMixingAllowed()) { + final String srcPlainText; + // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic! + srcPlainText = moOF.getSourcePlainText(mo); + if (srcPlainText == null) { + throw new _TemplateModelException(escapedExpression, + "The value to print is in ", new _DelayedToString(moOF), + " format, which differs from the current output format, ", + new _DelayedToString(outputFormat), ". Format conversion wasn't possible."); + } + if (outputFormat instanceof MarkupOutputFormat) { + ((MarkupOutputFormat) outputFormat).output(srcPlainText, out); + } else { + out.write(srcPlainText); + } + } else { + moOF.output(mo, out); + } + } + return null; + } + + @Override + protected Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException { + return _EvalUtil.coerceModelToStringOrMarkup(escapedExpression.eval(env), escapedExpression, null, env); + } + + @Override + protected String dump(boolean canonical, boolean inStringLiteral) { + StringBuilder sb = new StringBuilder(); + sb.append("${"); + final String exprCF = expression.getCanonicalForm(); + sb.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF); + sb.append("}"); + if (!canonical && expression != escapedExpression) { + sb.append(" auto-escaped"); + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "${...}"; + } + + @Override + boolean heedsOpeningWhitespace() { + return true; + } + + @Override + boolean heedsTrailingWhitespace() { + return true; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return expression; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.CONTENT; + } + + @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/ASTElement.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java new file mode 100644 index 0000000..a9cbfc0 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java @@ -0,0 +1,445 @@ +/* + * 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.Collections; +import java.util.Enumeration; + +import org.apache.freemarker.core.util._ArrayEnumeration; + +/** + * AST non-expression node superclass: Superclass of directive calls, interpolations, static text, top-level comments, + * or other such non-expression node in the parsed template. Some information that can be found here can be accessed + * through the {@link Environment#getCurrentDirectiveCallPlace()}, which is a published API, and thus promises backward + * compatibility. + */ +// TODO [FM3] Get rid of "public" and thus the "_" prefix +abstract class ASTElement extends ASTNode { + + private static final int INITIAL_CHILD_BUFFER_CAPACITY = 6; + + private ASTElement parent; + + /** + * Contains 1 or more nested elements with optional trailing {@code null}-s, or is {@code null} exactly if there are + * no nested elements. + */ + private ASTElement[] childBuffer; + + /** + * Contains the number of elements in the {@link #childBuffer}, not counting the trailing {@code null}-s. If this is + * 0, then and only then {@link #childBuffer} must be {@code null}. + */ + private int childCount; + + /** + * The index of the element in the parent's {@link #childBuffer} array. + * + * @since 2.3.23 + */ + private int index; + + /** + * Executes this {@link ASTElement}. Usually should not be called directly, but through + * {@link Environment#visit(ASTElement)} or a similar {@link Environment} method. + * + * @param env + * The runtime environment + * + * @return The template elements to execute (meant to be used for nested elements), or {@code null}. Can have + * <em>trailing</em> {@code null}-s (unused buffer capacity). Returning the nested elements instead of + * executing them inside this method is a trick used for decreasing stack usage when there's nothing to do + * after the children was processed anyway. + */ + abstract ASTElement[] accept(Environment env) throws TemplateException, IOException; + + /** + * One-line description of the element, that contain all the information that is used in {@link #getCanonicalForm()} + * , except the nested content (elements) of the element. The expressions inside the element (the parameters) has to + * be shown. Meant to be used for stack traces, also for tree views that don't go down to the expression-level. + * There are no backward-compatibility guarantees regarding the format used ATM, but it must be regular enough to be + * machine-parseable, and it must contain all information necessary for restoring an AST equivalent to the original. + * + * This final implementation calls {@link #dump(boolean) dump(false)}. + * + * @see #getCanonicalForm() + * @see #getNodeTypeSymbol() + */ + public final String getDescription() { + return dump(false); + } + + /** + * This final implementation calls {@link #dump(boolean) dump(false)}. + */ + @Override + public final String getCanonicalForm() { + return dump(true); + } + + final String getChildrenCanonicalForm() { + return getChildrenCanonicalForm(childBuffer); + } + + static String getChildrenCanonicalForm(ASTElement[] children) { + if (children == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (ASTElement child : children) { + if (child == null) { + break; + } + sb.append(child.getCanonicalForm()); + } + return sb.toString(); + } + + /** + * Tells if the element should show up in error stack traces. Note that this will be ignored for the top (current) + * element of a stack trace, as that's always shown. + */ + boolean isShownInStackTrace() { + return false; + } + + /** + * Tells if this element possibly executes its nested content for many times. This flag is useful when a template + * AST is modified for running time limiting (see {@link ThreadInterruptionSupportTemplatePostProcessor}). Elements + * that use {@link #childBuffer} should not need this, as the insertion of the timeout checks is impossible there, + * given their rigid nested element schema. + */ + abstract boolean isNestedBlockRepeater(); + + /** + * Brings the implementation of {@link #getCanonicalForm()} and {@link #getDescription()} to a single place. Don't + * call those methods in method on {@code this}, because that will result in infinite recursion! + * + * @param canonical + * if {@code true}, it calculates the return value of {@link #getCanonicalForm()}, otherwise of + * {@link #getDescription()}. + */ + abstract protected String dump(boolean canonical); + + // Methods to implement TemplateNodeModel + + public String getNodeName() { + String className = getClass().getName(); + int shortNameOffset = className.lastIndexOf('.') + 1; + return className.substring(shortNameOffset); + } + + // Methods so that we can implement the Swing TreeNode API. + + public boolean isLeaf() { + return childCount == 0; + } + + public int getIndex(ASTElement node) { + for (int i = 0; i < childCount; i++) { + if (childBuffer[i].equals(node)) { + return i; + } + } + return -1; + } + + public int getChildCount() { + return childCount; + } + + /** + * Note: For element with {@code #nestedBlock}, this will hide the {@code #nestedBlock} when that's a + * {@link ASTImplicitParent}. + */ + public Enumeration children() { + return childBuffer != null + ? new _ArrayEnumeration(childBuffer, childCount) + : Collections.enumeration(Collections.EMPTY_LIST); + } + + public void setChildAt(int index, ASTElement element) { + if (index < childCount && index >= 0) { + childBuffer[index] = element; + element.index = index; + element.parent = this; + } else { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + childCount); + } + } + + /** + * The element whose child this element is, or {@code null} if this is the root node. + */ + final ASTElement getParent() { + return parent; + } + + final void setChildBufferCapacity(int capacity) { + int ln = childCount; + ASTElement[] newChildBuffer = new ASTElement[capacity]; + for (int i = 0; i < ln; i++) { + newChildBuffer[i] = childBuffer[i]; + } + childBuffer = newChildBuffer; + } + + /** + * Inserts a new nested element after the last nested element. + */ + final void addChild(ASTElement nestedElement) { + addChild(childCount, nestedElement); + } + + /** + * Inserts a new nested element at the given index, which can also be one higher than the current highest index. + */ + final void addChild(int index, ASTElement nestedElement) { + final int lChildCount = childCount; + + ASTElement[] lChildBuffer = childBuffer; + if (lChildBuffer == null) { + lChildBuffer = new ASTElement[INITIAL_CHILD_BUFFER_CAPACITY]; + childBuffer = lChildBuffer; + } else if (lChildCount == lChildBuffer.length) { + setChildBufferCapacity(lChildCount != 0 ? lChildCount * 2 : 1); + lChildBuffer = childBuffer; + } + // At this point: nestedElements == this.nestedElements, and has sufficient capacity. + + for (int i = lChildCount; i > index; i--) { + ASTElement movedElement = lChildBuffer[i - 1]; + movedElement.index = i; + lChildBuffer[i] = movedElement; + } + nestedElement.index = index; + nestedElement.parent = this; + lChildBuffer[index] = nestedElement; + childCount = lChildCount + 1; + } + + final ASTElement getChild(int index) { + return childBuffer[index]; + } + + /** + * @return Array containing 1 or more nested elements with optional trailing {@code null}-s, or is {@code null} + * exactly if there are no nested elements. + */ + final ASTElement[] getChildBuffer() { + return childBuffer; + } + + /** + * @param buffWithCnt Maybe {@code null} + * + * @since 2.3.24 + */ + final void setChildren(TemplateElements buffWithCnt) { + ASTElement[] childBuffer = buffWithCnt.getBuffer(); + int childCount = buffWithCnt.getCount(); + for (int i = 0; i < childCount; i++) { + ASTElement child = childBuffer[i]; + child.index = i; + child.parent = this; + } + this.childBuffer = childBuffer; + this.childCount = childCount; + } + + final int getIndex() { + return index; + } + + /** + * This is a special case, because a root element is not contained in another element, so we couldn't set the + * private fields. + */ + final void setFieldsForRootElement() { + index = 0; + parent = null; + } + + /** + * Walk the AST subtree rooted by this element, and do simplifications where possible, also removes superfluous + * whitespace. + * + * @param stripWhitespace + * whether to remove superfluous whitespace + * + * @return The element this element should be replaced with in the parent. If it's the same as this element, no + * actual replacement will happen. Note that adjusting the {@link #parent} and {@link #index} of the result + * is the duty of the caller, not of this method. + */ + ASTElement postParseCleanup(boolean stripWhitespace) throws ParseException { + int childCount = this.childCount; + if (childCount != 0) { + for (int i = 0; i < childCount; i++) { + ASTElement te = childBuffer[i]; + + /* + // Assertion: + if (te.getIndex() != i) { + throw new BugException("Invalid index " + te.getIndex() + " (expected: " + + i + ") for: " + te.dump(false)); + } + if (te.getParent() != this) { + throw new BugException("Invalid parent " + te.getParent() + " (expected: " + + this.dump(false) + ") for: " + te.dump(false)); + } + */ + + te = te.postParseCleanup(stripWhitespace); + childBuffer[i] = te; + te.parent = this; + te.index = i; + } + for (int i = 0; i < childCount; i++) { + ASTElement te = childBuffer[i]; + if (te.isIgnorable(stripWhitespace)) { + childCount--; + // As later isIgnorable calls might investigates the siblings, we have to move all the items now. + for (int j = i; j < childCount; j++) { + final ASTElement te2 = childBuffer[j + 1]; + childBuffer[j] = te2; + te2.index = j; + } + childBuffer[childCount] = null; + this.childCount = childCount; + i--; + } + } + if (childCount == 0) { + childBuffer = null; + } else if (childCount < childBuffer.length + && childCount <= childBuffer.length * 3 / 4) { + ASTElement[] trimmedChildBuffer = new ASTElement[childCount]; + for (int i = 0; i < childCount; i++) { + trimmedChildBuffer[i] = childBuffer[i]; + } + childBuffer = trimmedChildBuffer; + } + } + return this; + } + + boolean isIgnorable(boolean stripWhitespace) { + return false; + } + + // The following methods exist to support some fancier tree-walking + // and were introduced to support the whitespace cleanup feature in 2.2 + + ASTElement prevTerminalNode() { + ASTElement prev = previousSibling(); + if (prev != null) { + return prev.getLastLeaf(); + } else if (parent != null) { + return parent.prevTerminalNode(); + } + return null; + } + + ASTElement nextTerminalNode() { + ASTElement next = nextSibling(); + if (next != null) { + return next.getFirstLeaf(); + } else if (parent != null) { + return parent.nextTerminalNode(); + } + return null; + } + + ASTElement previousSibling() { + if (parent == null) { + return null; + } + return index > 0 ? parent.childBuffer[index - 1] : null; + } + + ASTElement nextSibling() { + if (parent == null) { + return null; + } + return index + 1 < parent.childCount ? parent.childBuffer[index + 1] : null; + } + + private ASTElement getFirstChild() { + return childCount == 0 ? null : childBuffer[0]; + } + + private ASTElement getLastChild() { + final int childCount = this.childCount; + return childCount == 0 ? null : childBuffer[childCount - 1]; + } + + private ASTElement getFirstLeaf() { + ASTElement te = this; + while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) { + // A macro or macro invocation is treated as a leaf here for special reasons + te = te.getFirstChild(); + } + return te; + } + + private ASTElement getLastLeaf() { + ASTElement te = this; + while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) { + // A macro or macro invocation is treated as a leaf here for special reasons + te = te.getLastChild(); + } + return te; + } + + /** + * Tells if executing this element has output that only depends on the template content and that has no side + * effects. + */ + boolean isOutputCacheable() { + return false; + } + + boolean isChildrenOutputCacheable() { + int ln = childCount; + for (int i = 0; i < ln; i++) { + if (!childBuffer[i].isOutputCacheable()) { + return false; + } + } + return true; + } + + /** + * determines whether this element's presence on a line indicates that we should not strip opening whitespace in the + * post-parse whitespace gobbling step. + */ + boolean heedsOpeningWhitespace() { + return false; + } + + /** + * determines whether this element's presence on a line indicates that we should not strip trailing whitespace in + * the post-parse whitespace gobbling step. + */ + boolean heedsTrailingWhitespace() { + return false; + } +}
