http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java new file mode 100644 index 0000000..792787a --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java @@ -0,0 +1,220 @@ +/* + * 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.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.ListIterator; + +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateHashModelEx2; +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.model.impl.CollectionAndSequence; + +/** + * AST expression node: <tt>{ keyExp: valueExp, ... }</tt> + */ +final class ASTExpHashLiteral extends ASTExpression { + + private final ArrayList keys, values; + private final int size; + + ASTExpHashLiteral(ArrayList/*<ASTExpression>*/ keys, ArrayList/*<ASTExpression>*/ values) { + this.keys = keys; + this.values = values; + size = keys.size(); + keys.trimToSize(); + values.trimToSize(); + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return new SequenceHash(env); + } + + @Override + public String getCanonicalForm() { + StringBuilder buf = new StringBuilder("{"); + for (int i = 0; i < size; i++) { + ASTExpression key = (ASTExpression) keys.get(i); + ASTExpression value = (ASTExpression) values.get(i); + buf.append(key.getCanonicalForm()); + buf.append(": "); + buf.append(value.getCanonicalForm()); + if (i != size - 1) { + buf.append(", "); + } + } + buf.append("}"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "{...}"; + } + + @Override + boolean isLiteral() { + if (constantValue != null) { + return true; + } + for (int i = 0; i < size; i++) { + ASTExpression key = (ASTExpression) keys.get(i); + ASTExpression value = (ASTExpression) values.get(i); + if (!key.isLiteral() || !value.isLiteral()) { + return false; + } + } + return true; + } + + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ArrayList clonedKeys = (ArrayList) keys.clone(); + for (ListIterator iter = clonedKeys.listIterator(); iter.hasNext(); ) { + iter.set(((ASTExpression) iter.next()).deepCloneWithIdentifierReplaced( + replacedIdentifier, replacement, replacementState)); + } + ArrayList clonedValues = (ArrayList) values.clone(); + for (ListIterator iter = clonedValues.listIterator(); iter.hasNext(); ) { + iter.set(((ASTExpression) iter.next()).deepCloneWithIdentifierReplaced( + replacedIdentifier, replacement, replacementState)); + } + return new ASTExpHashLiteral(clonedKeys, clonedValues); + } + + private class SequenceHash implements TemplateHashModelEx2 { + + private HashMap<String, TemplateModel> map; + private TemplateCollectionModel keyCollection, valueCollection; // ordered lists of keys and values + + SequenceHash(Environment env) throws TemplateException { + map = new LinkedHashMap<>(); + for (int i = 0; i < size; i++) { + ASTExpression keyExp = (ASTExpression) keys.get(i); + ASTExpression valExp = (ASTExpression) values.get(i); + String key = keyExp.evalAndCoerceToPlainText(env); + TemplateModel value = valExp.eval(env); + valExp.assertNonNull(value, env); + map.put(key, value); + } + } + + @Override + public int size() { + return size; + } + + @Override + public TemplateCollectionModel keys() { + if (keyCollection == null) { + keyCollection = new CollectionAndSequence(new NativeStringCollectionCollectionEx(map.keySet())); + } + return keyCollection; + } + + @Override + public TemplateCollectionModel values() { + if (valueCollection == null) { + valueCollection = new CollectionAndSequence(new NativeCollectionEx(map.values())); + } + return valueCollection; + } + + @Override + public TemplateModel get(String key) { + return map.get(key); + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public String toString() { + return getCanonicalForm(); + } + + @Override + public KeyValuePairIterator keyValuePairIterator() throws TemplateModelException { + return new KeyValuePairIterator() { + private final TemplateModelIterator keyIterator = keys().iterator(); + private final TemplateModelIterator valueIterator = values().iterator(); + + @Override + public boolean hasNext() throws TemplateModelException { + return keyIterator.hasNext(); + } + + @Override + public KeyValuePair next() throws TemplateModelException { + return new KeyValuePair() { + private final TemplateModel key = keyIterator.next(); + private final TemplateModel value = valueIterator.next(); + + @Override + public TemplateModel getKey() throws TemplateModelException { + return key; + } + + @Override + public TemplateModel getValue() throws TemplateModelException { + return value; + } + + }; + } + + }; + } + + } + + @Override + int getParameterCount() { + return size * 2; + } + + @Override + Object getParameterValue(int idx) { + checkIndex(idx); + return idx % 2 == 0 ? keys.get(idx / 2) : values.get(idx / 2); + } + + @Override + ParameterRole getParameterRole(int idx) { + checkIndex(idx); + return idx % 2 == 0 ? ParameterRole.ITEM_KEY : ParameterRole.ITEM_VALUE; + } + + private void checkIndex(int idx) { + if (idx >= size * 2) { + throw new IndexOutOfBoundsException(); + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java new file mode 100644 index 0000000..b3fba1f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java @@ -0,0 +1,195 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.ListIterator; + +import org.apache.freemarker.core.model.TemplateMethodModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; + +/** + * AST expression node: {@code [ exp, ... ]} + */ +final class ASTExpListLiteral extends ASTExpression { + + final ArrayList/*<ASTExpression>*/ items; + + ASTExpListLiteral(ArrayList items) { + this.items = items; + items.trimToSize(); + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + NativeSequence list = new NativeSequence(items.size()); + for (Object item : items) { + ASTExpression exp = (ASTExpression) item; + TemplateModel tm = exp.eval(env); + exp.assertNonNull(tm, env); + list.add(tm); + } + return list; + } + + /** + * For {@link TemplateMethodModel} calls, but not for {@link TemplateMethodModelEx}-es, returns the list of + * arguments as {@link String}-s. + */ + List/*<String>*/ getValueList(Environment env) throws TemplateException { + int size = items.size(); + switch(size) { + case 0: { + return Collections.EMPTY_LIST; + } + case 1: { + return Collections.singletonList(((ASTExpression) items.get(0)).evalAndCoerceToPlainText(env)); + } + default: { + List result = new ArrayList(items.size()); + for (ListIterator iterator = items.listIterator(); iterator.hasNext(); ) { + ASTExpression exp = (ASTExpression) iterator.next(); + result.add(exp.evalAndCoerceToPlainText(env)); + } + return result; + } + } + } + + /** + * For {@link TemplateMethodModelEx} calls, returns the list of arguments as {@link TemplateModel}-s. + */ + List/*<TemplateModel>*/ getModelList(Environment env) throws TemplateException { + int size = items.size(); + switch(size) { + case 0: { + return Collections.EMPTY_LIST; + } + case 1: { + return Collections.singletonList(((ASTExpression) items.get(0)).eval(env)); + } + default: { + List result = new ArrayList(items.size()); + for (ListIterator iterator = items.listIterator(); iterator.hasNext(); ) { + ASTExpression exp = (ASTExpression) iterator.next(); + result.add(exp.eval(env)); + } + return result; + } + } + } + + @Override + public String getCanonicalForm() { + StringBuilder buf = new StringBuilder("["); + int size = items.size(); + for (int i = 0; i < size; i++) { + ASTExpression value = (ASTExpression) items.get(i); + buf.append(value.getCanonicalForm()); + if (i != size - 1) { + buf.append(", "); + } + } + buf.append("]"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "[...]"; + } + + @Override + boolean isLiteral() { + if (constantValue != null) { + return true; + } + for (int i = 0; i < items.size(); i++) { + ASTExpression exp = (ASTExpression) items.get(i); + if (!exp.isLiteral()) { + return false; + } + } + return true; + } + + // A hacky routine used by ASTDirVisit and ASTDirRecurse + TemplateSequenceModel evaluateStringsToNamespaces(Environment env) throws TemplateException { + TemplateSequenceModel val = (TemplateSequenceModel) eval(env); + NativeSequence result = new NativeSequence(val.size()); + for (int i = 0; i < items.size(); i++) { + Object itemExpr = items.get(i); + if (itemExpr instanceof ASTExpStringLiteral) { + String s = ((ASTExpStringLiteral) itemExpr).getAsString(); + try { + Environment.Namespace ns = env.importLib(s, null); + result.add(ns); + } catch (IOException ioe) { + throw new _MiscTemplateException(((ASTExpStringLiteral) itemExpr), + "Couldn't import library ", new _DelayedJQuote(s), ": ", + new _DelayedGetMessage(ioe)); + } + } else { + result.add(val.get(i)); + } + } + return result; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ArrayList clonedValues = (ArrayList) items.clone(); + for (ListIterator iter = clonedValues.listIterator(); iter.hasNext(); ) { + iter.set(((ASTExpression) iter.next()).deepCloneWithIdentifierReplaced( + replacedIdentifier, replacement, replacementState)); + } + return new ASTExpListLiteral(clonedValues); + } + + @Override + int getParameterCount() { + return items != null ? items.size() : 0; + } + + @Override + Object getParameterValue(int idx) { + checkIndex(idx); + return items.get(idx); + } + + @Override + ParameterRole getParameterRole(int idx) { + checkIndex(idx); + return ParameterRole.ITEM_VALUE; + } + + private void checkIndex(int idx) { + if (items == null || idx >= items.size()) { + throw new IndexOutOfBoundsException(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java new file mode 100644 index 0000000..86e376f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java @@ -0,0 +1,147 @@ +/* + * 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. + */ + +/* + * 22 October 1999: This class added by Holger Arendt. + */ + +package org.apache.freemarker.core; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.freemarker.core.model.TemplateMethodModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util._NullWriter; + + +/** + * AST expression node: {@code exp(args)}. + */ +final class ASTExpMethodCall extends ASTExpression { + + private final ASTExpression target; + private final ASTExpListLiteral arguments; + + ASTExpMethodCall(ASTExpression target, ArrayList arguments) { + this(target, new ASTExpListLiteral(arguments)); + } + + private ASTExpMethodCall(ASTExpression target, ASTExpListLiteral arguments) { + this.target = target; + this.arguments = arguments; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel targetModel = target.eval(env); + if (targetModel instanceof TemplateMethodModel) { + TemplateMethodModel targetMethod = (TemplateMethodModel) targetModel; + List argumentStrings = + targetMethod instanceof TemplateMethodModelEx + ? arguments.getModelList(env) + : arguments.getValueList(env); + Object result = targetMethod.exec(argumentStrings); + return env.getObjectWrapper().wrap(result); + } else if (targetModel instanceof ASTDirMacro) { + ASTDirMacro func = (ASTDirMacro) targetModel; + env.setLastReturnValue(null); + if (!func.isFunction()) { + throw new _MiscTemplateException(env, "A macro cannot be called in an expression. (Functions can be.)"); + } + Writer prevOut = env.getOut(); + try { + env.setOut(_NullWriter.INSTANCE); + env.invoke(func, null, arguments.items, null, null); + } catch (IOException e) { + // Should not occur + throw new TemplateException("Unexpected exception during function execution", e, env); + } finally { + env.setOut(prevOut); + } + return env.getLastReturnValue(); + } else { + throw new NonMethodException(target, targetModel, env); + } + } + + @Override + public String getCanonicalForm() { + StringBuilder buf = new StringBuilder(); + buf.append(target.getCanonicalForm()); + buf.append("("); + String list = arguments.getCanonicalForm(); + buf.append(list.substring(1, list.length() - 1)); + buf.append(")"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "...(...)"; + } + + TemplateModel getConstantValue() { + return null; + } + + @Override + boolean isLiteral() { + return false; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpMethodCall( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + (ASTExpListLiteral) arguments.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 1 + arguments.items.size(); + } + + @Override + Object getParameterValue(int idx) { + if (idx == 0) { + return target; + } else if (idx < getParameterCount()) { + return arguments.items.get(idx - 1); + } else { + throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx == 0) { + return ParameterRole.CALLEE; + } else if (idx < getParameterCount()) { + return ParameterRole.ARGUMENT_VALUE; + } else { + throw new IndexOutOfBoundsException(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java new file mode 100644 index 0000000..a211ef7 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java @@ -0,0 +1,110 @@ +/* + * 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 org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.impl.SimpleNumber; + +/** + * AST expression node: {@code -exp} or {@code +exp}. + */ +final class ASTExpNegateOrPlus extends ASTExpression { + + private static final int TYPE_MINUS = 0; + private static final int TYPE_PLUS = 1; + + private final ASTExpression target; + private final boolean isMinus; + private static final Integer MINUS_ONE = Integer.valueOf(-1); + + ASTExpNegateOrPlus(ASTExpression target, boolean isMinus) { + this.target = target; + this.isMinus = isMinus; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateNumberModel targetModel = null; + TemplateModel tm = target.eval(env); + try { + targetModel = (TemplateNumberModel) tm; + } catch (ClassCastException cce) { + throw new NonNumericalException(target, tm, env); + } + if (!isMinus) { + return targetModel; + } + target.assertNonNull(targetModel, env); + Number n = targetModel.getAsNumber(); + // [FM3] Add ArithmeticEngine.negate, then use the engine from the env + n = ConservativeArithmeticEngine.INSTANCE.multiply(MINUS_ONE, n); + return new SimpleNumber(n); + } + + @Override + public String getCanonicalForm() { + String op = isMinus ? "-" : "+"; + return op + target.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return isMinus ? "-..." : "+..."; + } + + @Override + boolean isLiteral() { + return target.isLiteral(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpNegateOrPlus( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + isMinus); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return target; + case 1: return Integer.valueOf(isMinus ? TYPE_MINUS : TYPE_PLUS); + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.RIGHT_HAND_OPERAND; + case 1: return ParameterRole.AST_NODE_SUBTYPE; + 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/ASTExpNot.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java new file mode 100644 index 0000000..19dd088 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java @@ -0,0 +1,76 @@ +/* + * 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 expression node: {@code !exp}. + */ +final class ASTExpNot extends ASTExpBoolean { + + private final ASTExpression target; + + ASTExpNot(ASTExpression target) { + this.target = target; + } + + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return (!target.evalToBoolean(env)); + } + + @Override + public String getCanonicalForm() { + return "!" + target.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return "!"; + } + + @Override + boolean isLiteral() { + return target.isLiteral(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpNot( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return target; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.RIGHT_HAND_OPERAND; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java new file mode 100644 index 0000000..01847a6 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java @@ -0,0 +1,92 @@ +/* + * 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 org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.impl.SimpleNumber; + +/** + * AST expression node: numerical literal + */ +final class ASTExpNumberLiteral extends ASTExpression implements TemplateNumberModel { + + private final Number value; + + public ASTExpNumberLiteral(Number value) { + this.value = value; + } + + @Override + TemplateModel _eval(Environment env) { + return new SimpleNumber(value); + } + + @Override + public String evalAndCoerceToPlainText(Environment env) throws TemplateException { + return env.formatNumberToPlainText(this, this, false); + } + + @Override + public Number getAsNumber() { + return value; + } + + String getName() { + return "the number: '" + value + "'"; + } + + @Override + public String getCanonicalForm() { + return value.toString(); + } + + @Override + String getNodeTypeSymbol() { + return getCanonicalForm(); + } + + @Override + boolean isLiteral() { + return true; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpNumberLiteral(value); + } + + @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/ASTExpOr.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java new file mode 100644 index 0000000..5673ec3 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java @@ -0,0 +1,82 @@ +/* + * 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 expression node: {@code exp || exp}. + */ +final class ASTExpOr extends ASTExpBoolean { + + private final ASTExpression lho; + private final ASTExpression rho; + + ASTExpOr(ASTExpression lho, ASTExpression rho) { + this.lho = lho; + this.rho = rho; + } + + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return lho.evalToBoolean(env) || rho.evalToBoolean(env); + } + + @Override + public String getCanonicalForm() { + return lho.getCanonicalForm() + " || " + rho.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return "||"; + } + + @Override + boolean isLiteral() { + return constantValue != null || (lho.isLiteral() && rho.isLiteral()); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpOr( + lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return lho; + case 1: return rho; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.forBinaryOperatorOperand(idx); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java new file mode 100644 index 0000000..eabccbf --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.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 org.apache.freemarker.core.model.TemplateModel; + +/** + * AST expression node: {@code (exp)}. + */ +final class ASTExpParenthesis extends ASTExpression { + + private final ASTExpression nested; + + ASTExpParenthesis(ASTExpression nested) { + this.nested = nested; + } + + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return nested.evalToBoolean(env); + } + + @Override + public String getCanonicalForm() { + return "(" + nested.getCanonicalForm() + ")"; + } + + @Override + String getNodeTypeSymbol() { + return "(...)"; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return nested.eval(env); + } + + @Override + public boolean isLiteral() { + return nested.isLiteral(); + } + + ASTExpression getNestedExpression() { + return nested; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpParenthesis( + nested.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return nested; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.ENCLOSED_OPERAND; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java new file mode 100644 index 0000000..194c402 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java @@ -0,0 +1,119 @@ +/* + * 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 org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.BugException; + +/** + * AST expression node: {@code exp .. exp}, {@code exp ..< exp} (or {@code exp ..! exp}), {@code exp ..* exp}. + */ +final class ASTExpRange extends ASTExpression { + + static final int END_INCLUSIVE = 0; + static final int END_EXCLUSIVE = 1; + static final int END_UNBOUND = 2; + static final int END_SIZE_LIMITED = 3; + + final ASTExpression lho; + final ASTExpression rho; + final int endType; + + ASTExpRange(ASTExpression lho, ASTExpression rho, int endType) { + this.lho = lho; + this.rho = rho; + this.endType = endType; + } + + int getEndType() { + return endType; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + final int begin = lho.evalToNumber(env).intValue(); + if (endType != END_UNBOUND) { + final int lhoValue = rho.evalToNumber(env).intValue(); + return new BoundedRangeModel( + begin, endType != END_SIZE_LIMITED ? lhoValue : begin + lhoValue, + endType == END_INCLUSIVE, endType == END_SIZE_LIMITED); + } else { + return new ListableRightUnboundedRangeModel(begin); + } + } + + // Surely this way we can tell that it won't be a boolean without evaluating the range, but why was this important? + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + throw new NonBooleanException(this, new BoundedRangeModel(0, 0, false, false), env); + } + + @Override + public String getCanonicalForm() { + String rhs = rho != null ? rho.getCanonicalForm() : ""; + return lho.getCanonicalForm() + getNodeTypeSymbol() + rhs; + } + + @Override + String getNodeTypeSymbol() { + switch (endType) { + case END_EXCLUSIVE: return "..<"; + case END_INCLUSIVE: return ".."; + case END_UNBOUND: return ".."; + case END_SIZE_LIMITED: return "..*"; + default: throw new BugException(endType); + } + } + + @Override + boolean isLiteral() { + boolean rightIsLiteral = rho == null || rho.isLiteral(); + return constantValue != null || (lho.isLiteral() && rightIsLiteral); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpRange( + lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + endType); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return lho; + case 1: return rho; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.forBinaryOperatorOperand(idx); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java new file mode 100644 index 0000000..96c15df --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java @@ -0,0 +1,211 @@ +/* + * 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.StringReader; +import java.util.List; + +import org.apache.freemarker.core.model.TemplateMarkupOutputModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.outputformat.OutputFormat; +import org.apache.freemarker.core.util.FTLUtil; + +/** + * AST expression node: string literal + */ +final class ASTExpStringLiteral extends ASTExpression implements TemplateScalarModel { + + private final String value; + + /** {@link List} of {@link String}-s and {@link ASTInterpolation}-s. */ + private List<Object> dynamicValue; + + ASTExpStringLiteral(String value) { + this.value = value; + } + + /** + * @param parentTkMan + * The token source of the template that contains this string literal. As of this writing, we only need + * this to share the {@code namingConvetion} with that. + */ + void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException { + // The way this works is incorrect (the literal should be parsed without un-escaping), + // but we can't fix this backward compatibly. + if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) { + + Template parentTemplate = getTemplate(); + ParsingConfiguration pCfg = parentTemplate.getParsingConfiguration(); + + try { + SimpleCharStream simpleCharacterStream = new SimpleCharStream( + new StringReader(value), + beginLine, beginColumn + 1, + value.length()); + simpleCharacterStream.setTabSize(pCfg.getTabSize()); + + FMParserTokenManager tkMan = new FMParserTokenManager( + simpleCharacterStream); + + FMParser parser = new FMParser(parentTemplate, false, + tkMan, pCfg, null, null, + null); + // We continue from the parent parser's current state: + parser.setupStringLiteralMode(parentTkMan, outputFormat); + try { + dynamicValue = parser.StaticTextAndInterpolations(); + } finally { + // The parent parser continues from this parser's current state: + parser.tearDownStringLiteralMode(parentTkMan); + } + } catch (ParseException e) { + e.setTemplate(parentTemplate); + throw e; + } + constantValue = null; + } + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + if (dynamicValue == null) { + return new SimpleScalar(value); + } else { + // This should behave like concatenating the values with `+`. Thus, an interpolated expression that + // returns markup promotes the result of the whole expression to markup. + + // Exactly one of these is non-null, depending on if the result will be plain text or markup, which can + // change during evaluation, depending on the result of the interpolations: + StringBuilder plainTextResult = null; + TemplateMarkupOutputModel<?> markupResult = null; + + for (Object part : dynamicValue) { + Object calcedPart = + part instanceof String ? part + : ((ASTInterpolation) part).calculateInterpolatedStringOrMarkup(env); + if (markupResult != null) { + TemplateMarkupOutputModel<?> partMO = calcedPart instanceof String + ? markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart) + : (TemplateMarkupOutputModel<?>) calcedPart; + markupResult = _EvalUtil.concatMarkupOutputs(this, markupResult, partMO); + } else { // We are using `plainTextOutput` (or nothing yet) + if (calcedPart instanceof String) { + String partStr = (String) calcedPart; + if (plainTextResult == null) { + plainTextResult = new StringBuilder(partStr); + } else { + plainTextResult.append(partStr); + } + } else { // `calcedPart` is TemplateMarkupOutputModel + TemplateMarkupOutputModel<?> moPart = (TemplateMarkupOutputModel<?>) calcedPart; + if (plainTextResult != null) { + TemplateMarkupOutputModel<?> leftHandMO = moPart.getOutputFormat() + .fromPlainTextByEscaping(plainTextResult.toString()); + markupResult = _EvalUtil.concatMarkupOutputs(this, leftHandMO, moPart); + plainTextResult = null; + } else { + markupResult = moPart; + } + } + } + } // for each part + return markupResult != null ? markupResult + : plainTextResult != null ? new SimpleScalar(plainTextResult.toString()) + : SimpleScalar.EMPTY_STRING; + } + } + + @Override + public String getAsString() { + return value; + } + + /** + * Tells if this is something like <tt>"${foo}"</tt>, which is usually a user mistake. + */ + boolean isSingleInterpolationLiteral() { + return dynamicValue != null && dynamicValue.size() == 1 + && dynamicValue.get(0) instanceof ASTInterpolation; + } + + @Override + public String getCanonicalForm() { + if (dynamicValue == null) { + return FTLUtil.toStringLiteral(value); + } else { + StringBuilder sb = new StringBuilder(); + sb.append('"'); + for (Object child : dynamicValue) { + if (child instanceof ASTInterpolation) { + sb.append(((ASTInterpolation) child).getCanonicalFormInStringLiteral()); + } else { + sb.append(FTLUtil.escapeStringLiteralPart((String) child, '"')); + } + } + sb.append('"'); + return sb.toString(); + } + } + + @Override + String getNodeTypeSymbol() { + return dynamicValue == null ? getCanonicalForm() : "dynamic \"...\""; + } + + @Override + boolean isLiteral() { + return dynamicValue == null; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ASTExpStringLiteral cloned = new ASTExpStringLiteral(value); + // FIXME: replacedIdentifier should be searched inside interpolatedOutput too: + cloned.dynamicValue = dynamicValue; + return cloned; + } + + @Override + int getParameterCount() { + return dynamicValue == null ? 0 : dynamicValue.size(); + } + + @Override + Object getParameterValue(int idx) { + checkIndex(idx); + return dynamicValue.get(idx); + } + + private void checkIndex(int idx) { + if (dynamicValue == null || idx >= dynamicValue.size()) { + throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + checkIndex(idx); + return ParameterRole.VALUE_PART; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java new file mode 100644 index 0000000..59ceddc --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java @@ -0,0 +1,105 @@ +/* + * 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 org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST expression node: Reference to a "top-level" (local, current namespace, global, data-model) variable + */ +final class ASTExpVariable extends ASTExpression { + + private final String name; + + ASTExpVariable(String name) { + this.name = name; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + try { + return env.getVariable(name); + } catch (NullPointerException e) { + if (env == null) { + throw new _MiscTemplateException( + "Variables are not available (certainly you are in a parse-time executed directive). " + + "The name of the variable you tried to read: ", name); + } else { + throw e; + } + } + } + + @Override + public String getCanonicalForm() { + return _StringUtil.toFTLTopLevelIdentifierReference(name); + } + + /** + * The name of the identifier without any escaping or other syntactical distortions. + */ + String getName() { + return name; + } + + @Override + String getNodeTypeSymbol() { + return getCanonicalForm(); + } + + @Override + boolean isLiteral() { + return false; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + if (name.equals(replacedIdentifier)) { + if (replacementState.replacementAlreadyInUse) { + ASTExpression clone = replacement.deepCloneWithIdentifierReplaced(null, null, replacementState); + clone.copyLocationFrom(replacement); + return clone; + } else { + replacementState.replacementAlreadyInUse = true; + return replacement; + } + } else { + return new ASTExpVariable(name); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java new file mode 100644 index 0000000..be00f66 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java @@ -0,0 +1,208 @@ +/* + * 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 org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateMarkupOutputModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.BeanModel; + +/** + * AST expression node superclass + */ +abstract class ASTExpression extends ASTNode { + + /** + * @param env might be {@code null}, if this kind of expression can be evaluated during parsing (as opposed to + * during template execution). + */ + abstract TemplateModel _eval(Environment env) throws TemplateException; + + abstract boolean isLiteral(); + + // Used to store a constant return value for this expression. Only if it + // is possible, of course. + + TemplateModel constantValue; + + // Hook in here to set the constant value if possible. + + @Override + void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) { + super.setLocation(template, beginColumn, beginLine, endColumn, endLine); + if (isLiteral()) { + try { + constantValue = _eval(null); + } catch (Exception e) { + // deliberately ignore. + } + } + } + + final TemplateModel getAsTemplateModel(Environment env) throws TemplateException { + return eval(env); + } + + final TemplateModel eval(Environment env) throws TemplateException { + return constantValue != null ? constantValue : _eval(env); + } + + String evalAndCoerceToPlainText(Environment env) throws TemplateException { + return _EvalUtil.coerceModelToPlainText(eval(env), this, null, env); + } + + /** + * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection. + */ + String evalAndCoerceToPlainText(Environment env, String seqTip) throws TemplateException { + return _EvalUtil.coerceModelToPlainText(eval(env), this, seqTip, env); + } + + Object evalAndCoerceToStringOrMarkup(Environment env) throws TemplateException { + return _EvalUtil.coerceModelToStringOrMarkup(eval(env), this, null, env); + } + + /** + * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection. + */ + Object evalAndCoerceToStringOrMarkup(Environment env, String seqTip) throws TemplateException { + return _EvalUtil.coerceModelToStringOrMarkup(eval(env), this, seqTip, env); + } + + String evalAndCoerceToStringOrUnsupportedMarkup(Environment env) throws TemplateException { + return _EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, null, env); + } + + /** + * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection. + */ + String evalAndCoerceToStringOrUnsupportedMarkup(Environment env, String seqTip) throws TemplateException { + return _EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, seqTip, env); + } + + Number evalToNumber(Environment env) throws TemplateException { + TemplateModel model = eval(env); + return modelToNumber(model, env); + } + + Number modelToNumber(TemplateModel model, Environment env) throws TemplateException { + if (model instanceof TemplateNumberModel) { + return _EvalUtil.modelToNumber((TemplateNumberModel) model, this); + } else { + throw new NonNumericalException(this, model, env); + } + } + + boolean evalToBoolean(Environment env) throws TemplateException { + return evalToBoolean(env, null); + } + + boolean evalToBoolean(Configuration cfg) throws TemplateException { + return evalToBoolean(null, cfg); + } + + TemplateModel evalToNonMissing(Environment env) throws TemplateException { + TemplateModel result = eval(env); + assertNonNull(result, env); + return result; + } + + private boolean evalToBoolean(Environment env, Configuration cfg) throws TemplateException { + TemplateModel model = eval(env); + return modelToBoolean(model, env, cfg); + } + + boolean modelToBoolean(TemplateModel model, Environment env) throws TemplateException { + return modelToBoolean(model, env, null); + } + + boolean modelToBoolean(TemplateModel model, Configuration cfg) throws TemplateException { + return modelToBoolean(model, null, cfg); + } + + private boolean modelToBoolean(TemplateModel model, Environment env, Configuration cfg) throws TemplateException { + if (model instanceof TemplateBooleanModel) { + return ((TemplateBooleanModel) model).getAsBoolean(); + } else { + throw new NonBooleanException(this, model, env); + } + } + + final ASTExpression deepCloneWithIdentifierReplaced( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ASTExpression clone = deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, replacementState); + if (clone.beginLine == 0) { + clone.copyLocationFrom(this); + } + return clone; + } + + static class ReplacemenetState { + /** + * If the replacement expression is not in use yet, we don't have to deepClone it. + */ + boolean replacementAlreadyInUse; + } + + /** + * This should return an equivalent new expression object (or an identifier replacement expression). + * The position need not be filled, unless it will be different from the position of what we were cloning. + */ + protected abstract ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState); + + static boolean isEmpty(TemplateModel model) throws TemplateModelException { + if (model instanceof BeanModel) { + return ((BeanModel) model).isEmpty(); + } else if (model instanceof TemplateSequenceModel) { + return ((TemplateSequenceModel) model).size() == 0; + } else if (model instanceof TemplateScalarModel) { + String s = ((TemplateScalarModel) model).getAsString(); + return (s == null || s.length() == 0); + } else if (model == null) { + return true; + } else if (model instanceof TemplateMarkupOutputModel) { // Note: happens just after FTL string check + TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) model; + return mo.getOutputFormat().isEmpty(mo); + } else if (model instanceof TemplateCollectionModel) { + return !((TemplateCollectionModel) model).iterator().hasNext(); + } else if (model instanceof TemplateHashModel) { + return ((TemplateHashModel) model).isEmpty(); + } else if (model instanceof TemplateNumberModel + || model instanceof TemplateDateModel + || model instanceof TemplateBooleanModel) { + return false; + } else { + return true; + } + } + + void assertNonNull(TemplateModel model, Environment env) throws InvalidReferenceException { + if (model == null) throw InvalidReferenceException.getInstance(this, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java new file mode 100644 index 0000000..8c3f8fa --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.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.io.IOException; +import java.io.Writer; +import java.text.NumberFormat; +import java.util.Locale; + +import org.apache.freemarker.core.outputformat.MarkupOutputFormat; +import org.apache.freemarker.core.util.FTLUtil; + +/** + * AST interpolation node: <tt>#{exp}</tt> + */ +final class ASTHashInterpolation extends ASTInterpolation { + + private final ASTExpression expression; + private final boolean hasFormat; + private final int minFracDigits; + private final int maxFracDigits; + /** For OutputFormat-based auto-escaping */ + private final MarkupOutputFormat autoEscapeOutputFormat; + private volatile FormatHolder formatCache; // creating new NumberFormat is slow operation + + ASTHashInterpolation(ASTExpression expression, MarkupOutputFormat autoEscapeOutputFormat) { + this.expression = expression; + hasFormat = false; + minFracDigits = 0; + maxFracDigits = 0; + this.autoEscapeOutputFormat = autoEscapeOutputFormat; + } + + ASTHashInterpolation(ASTExpression expression, + int minFracDigits, int maxFracDigits, + MarkupOutputFormat autoEscapeOutputFormat) { + this.expression = expression; + hasFormat = true; + this.minFracDigits = minFracDigits; + this.maxFracDigits = maxFracDigits; + this.autoEscapeOutputFormat = autoEscapeOutputFormat; + } + + @Override + ASTElement[] accept(Environment env) throws TemplateException, IOException { + String s = calculateInterpolatedStringOrMarkup(env); + Writer out = env.getOut(); + if (autoEscapeOutputFormat != null) { + autoEscapeOutputFormat.output(s, out); + } else { + out.write(s); + } + return null; + } + + @Override + protected String calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException { + Number num = expression.evalToNumber(env); + + FormatHolder fmth = formatCache; // atomic sampling + if (fmth == null || !fmth.locale.equals(env.getLocale())) { + synchronized (this) { + fmth = formatCache; + if (fmth == null || !fmth.locale.equals(env.getLocale())) { + NumberFormat fmt = NumberFormat.getNumberInstance(env.getLocale()); + if (hasFormat) { + fmt.setMinimumFractionDigits(minFracDigits); + fmt.setMaximumFractionDigits(maxFracDigits); + } else { + fmt.setMinimumFractionDigits(0); + fmt.setMaximumFractionDigits(50); + } + fmt.setGroupingUsed(false); + formatCache = new FormatHolder(fmt, env.getLocale()); + fmth = formatCache; + } + } + } + // We must use Format even if hasFormat == false. + // Some locales may use non-Arabic digits, thus replacing the + // decimal separator in the result of toString() is not enough. + return fmth.format.format(num); + } + + @Override + protected String dump(boolean canonical, boolean inStringLiteral) { + StringBuilder buf = new StringBuilder("#{"); + final String exprCF = expression.getCanonicalForm(); + buf.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF); + if (hasFormat) { + buf.append(" ; "); + buf.append("m"); + buf.append(minFracDigits); + buf.append("M"); + buf.append(maxFracDigits); + } + buf.append("}"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#{...}"; + } + + @Override + boolean heedsOpeningWhitespace() { + return true; + } + + @Override + boolean heedsTrailingWhitespace() { + return true; + } + + private static class FormatHolder { + final NumberFormat format; + final Locale locale; + + FormatHolder(NumberFormat format, Locale locale) { + this.format = format; + this.locale = locale; + } + } + + @Override + int getParameterCount() { + return 3; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return expression; + case 1: return Integer.valueOf(minFracDigits); + case 2: return Integer.valueOf(maxFracDigits); + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.CONTENT; + case 1: return ParameterRole.MINIMUM_DECIMALS; + case 2: return ParameterRole.MAXIMUM_DECIMALS; + 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/ASTImplicitParent.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java new file mode 100644 index 0000000..4d3c339 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java @@ -0,0 +1,101 @@ +/* + * 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-like node, used where there's no other parent for a list of {@link ASTElement}-s. Most often occurs as + * the root node of the AST. + */ +final class ASTImplicitParent extends ASTElement { + + ASTImplicitParent() { } + + @Override + ASTElement postParseCleanup(boolean stripWhitespace) + throws ParseException { + super.postParseCleanup(stripWhitespace); + return getChildCount() == 1 ? getChild(0) : this; + } + + /** + * Processes the contents of the internal <tt>ASTElement</tt> list, + * and outputs the resulting text. + */ + @Override + ASTElement[] accept(Environment env) + throws TemplateException, IOException { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + return getChildrenCanonicalForm(); + } else { + if (getParent() == null) { + return "root"; + } + return getNodeTypeSymbol(); // ASTImplicitParent is uninteresting in a stack trace. + } + } + + @Override + protected boolean isOutputCacheable() { + int ln = getChildCount(); + for (int i = 0; i < ln; i++) { + if (!getChild(i).isOutputCacheable()) { + return false; + } + } + return true; + } + + @Override + String getNodeTypeSymbol() { + return "#mixed_content"; + } + + @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/ASTInterpolation.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java new file mode 100644 index 0000000..028acc2 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java @@ -0,0 +1,51 @@ +/* + * 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 org.apache.freemarker.core.model.TemplateMarkupOutputModel; + +/** + * AST interpolation node superclass. + */ +abstract class ASTInterpolation extends ASTElement { + + protected abstract String dump(boolean canonical, boolean inStringLiteral); + + @Override + protected final String dump(boolean canonical) { + return dump(canonical, false); + } + + final String getCanonicalFormInStringLiteral() { + return dump(true, true); + } + + /** + * Returns the already type-converted value that this interpolation will insert into the output. + * + * @return A {@link String} or {@link TemplateMarkupOutputModel}. Not {@code null}. + */ + protected abstract Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException; + + @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/ASTNode.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java new file mode 100644 index 0000000..18e34c1 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java @@ -0,0 +1,233 @@ +/* + * 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 node: The superclass of all AST nodes + */ +abstract class ASTNode { + + private Template template; + int beginColumn, beginLine, endColumn, endLine; + + /** This is needed for an ?eval hack; the expression AST nodes will be the descendants of the template, however, + * we can't give their position in the template, only in the dynamic string that's evaluated. That's signaled + * by a negative line numbers, starting from this constant as line 1. */ + static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000; + + final void setLocation(Template template, Token begin, Token end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + final void setLocation(Template template, Token tagBegin, Token tagEnd, TemplateElements children) { + ASTElement lastChild = children.getLast(); + if (lastChild != null) { + // [<#if exp>children]<#else> + setLocation(template, tagBegin, lastChild); + } else { + // [<#if exp>]<#else> + setLocation(template, tagBegin, tagEnd); + } + } + + final void setLocation(Template template, Token begin, ASTNode end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + final void setLocation(Template template, ASTNode begin, Token end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + final void setLocation(Template template, ASTNode begin, ASTNode end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) { + this.template = template; + this.beginColumn = beginColumn; + this.beginLine = beginLine; + this.endColumn = endColumn; + this.endLine = endLine; + } + + public final int getBeginColumn() { + return beginColumn; + } + + public final int getBeginLine() { + return beginLine; + } + + public final int getEndColumn() { + return endColumn; + } + + public final int getEndLine() { + return endLine; + } + + /** + * Returns a string that indicates + * where in the template source, this object is. + */ + public String getStartLocation() { + return MessageUtil.formatLocationForEvaluationError(template, beginLine, beginColumn); + } + + /** + * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS + * when viewing error messages. + */ + public String getStartLocationQuoted() { + return getStartLocation(); + } + + public String getEndLocation() { + return MessageUtil.formatLocationForEvaluationError(template, endLine, endColumn); + } + + /** + * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS + * when viewing error messages. + */ + public String getEndLocationQuoted() { + return getEndLocation(); + } + + public final String getSource() { + String s; + if (template != null) { + s = template.getSource(beginColumn, beginLine, endColumn, endLine); + } else { + s = null; + } + + // Can't just return null for backward-compatibility... + return s != null ? s : getCanonicalForm(); + } + + @Override + public String toString() { + String s; + try { + s = getSource(); + } catch (Exception e) { // REVISIT: A bit of a hack? (JR) + s = null; + } + return s != null ? s : getCanonicalForm(); + } + + /** + * @return whether the point in the template file specified by the + * column and line numbers is contained within this template object. + */ + public boolean contains(int column, int line) { + if (line < beginLine || line > endLine) { + return false; + } + if (line == beginLine) { + if (column < beginColumn) { + return false; + } + } + if (line == endLine) { + if (column > endColumn) { + return false; + } + } + return true; + } + + public Template getTemplate() { + return template; + } + + ASTNode copyLocationFrom(ASTNode from) { + template = from.template; + beginColumn = from.beginColumn; + beginLine = from.beginLine; + endColumn = from.endColumn; + endLine = from.endLine; + return this; + } + + /** + * FTL generated from the AST of the node, which must be parseable to an AST that does the same as the original + * source, assuming we turn off automatic white-space removal when parsing the canonical form. + * + * @see ASTElement#getDescription() + * @see #getNodeTypeSymbol() + */ + abstract public String getCanonicalForm(); + + /** + * A very sort single-line string that describes what kind of AST node this is, without describing any + * embedded expression or child element. Examples: {@code "#if"}, {@code "+"}, <tt>"${...}</tt>. These values should + * be suitable as tree node labels in a tree view. Yet, they should be consistent and complete enough so that an AST + * that is equivalent with the original could be reconstructed from the tree view. Thus, for literal values that are + * leaf nodes the symbols should be the canonical form of value. + * + * Note that {@link ASTElement#getDescription()} has similar role, only it doesn't go under the element level + * (i.e. down to the expression level), instead it always prints the embedded expressions itself. + * + * @see #getCanonicalForm() + * @see ASTElement#getDescription() + */ + abstract String getNodeTypeSymbol(); + + /** + * Returns highest valid parameter index + 1. So one should scan indexes with {@link #getParameterValue(int)} + * starting from 0 up until but excluding this. For example, for the binary "+" operator this will give 2, so the + * legal indexes are 0 and 1. Note that if a parameter is optional in a template-object-type and happens to be + * omitted in an instance, this will still return the same value and the value of that parameter will be + * {@code null}. + */ + abstract int getParameterCount(); + + /** + * Returns the value of the parameter identified by the index. For example, the binary "+" operator will have an + * LHO {@link ASTExpression} at index 0, and and RHO {@link ASTExpression} at index 1. Or, the binary "." operator will + * have an LHO {@link ASTExpression} at index 0, and an RHO {@link String}(!) at index 1. Or, the {@code #include} + * directive will have a path {@link ASTExpression} at index 0, a "parse" {@link ASTExpression} at index 1, etc. + * + * <p>The index value doesn't correspond to the source-code location in general. It's an arbitrary identifier + * that corresponds to the role of the parameter instead. This also means that when a parameter is omitted, the + * index of the other parameters won't shift. + * + * @return {@code null} or any kind of {@link Object}, very often an {@link ASTExpression}. However, if there's + * a {@link ASTNode} stored inside the returned value, it must itself be be a {@link ASTNode} + * too, otherwise the AST couldn't be (easily) fully traversed. That is, non-{@link ASTNode} values + * can only be used for leafs. + * + * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. + */ + abstract Object getParameterValue(int idx); + + /** + * Returns the role of the parameter at the given index, like {@link ParameterRole#LEFT_HAND_OPERAND}. + * + * As of this writing (2013-06-17), for directive parameters it will always give {@link ParameterRole#UNKNOWN}, + * because there was no need to be more specific so far. This should be improved as need. + * + * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. + */ + abstract ParameterRole getParameterRole(int idx); + +}
