http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java new file mode 100644 index 0000000..15e632a --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java @@ -0,0 +1,313 @@ +/* + * 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.HashSet; +import java.util.Set; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateHashModelEx; +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.TemplateModelIterator; +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.CollectionAndSequence; +import org.apache.freemarker.core.model.impl.SimpleNumber; +import org.apache.freemarker.core.model.impl.SimpleScalar; + +/** + * AST expression node: binary {@code +} operator. Note that this is treated separately from the other 4 arithmetic + * operators, since it's overloaded to mean concatenation of string-s, sequences and hash-es too. + */ +final class ASTExpAddOrConcat extends ASTExpression { + + private final ASTExpression left; + private final ASTExpression right; + + ASTExpAddOrConcat(ASTExpression left, ASTExpression right) { + this.left = left; + this.right = right; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return _eval(env, this, left, left.eval(env), right, right.eval(env)); + } + + /** + * @param leftExp + * Used for error messages only; can be {@code null} + * @param rightExp + * Used for error messages only; can be {@code null} + */ + static TemplateModel _eval(Environment env, + ASTNode parent, + ASTExpression leftExp, TemplateModel leftModel, + ASTExpression rightExp, TemplateModel rightModel) + throws TemplateException { + if (leftModel instanceof TemplateNumberModel && rightModel instanceof TemplateNumberModel) { + Number first = _EvalUtil.modelToNumber((TemplateNumberModel) leftModel, leftExp); + Number second = _EvalUtil.modelToNumber((TemplateNumberModel) rightModel, rightExp); + return _evalOnNumbers(env, parent, first, second); + } else if (leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel) { + return new ConcatenatedSequence((TemplateSequenceModel) leftModel, (TemplateSequenceModel) rightModel); + } else { + boolean hashConcatPossible + = leftModel instanceof TemplateHashModel && rightModel instanceof TemplateHashModel; + try { + // We try string addition first. If hash addition is possible, then instead of throwing exception + // we return null and do hash addition instead. (We can't simply give hash addition a priority, like + // with sequence addition above, as FTL strings are often also FTL hashes.) + Object leftOMOrStr = _EvalUtil.coerceModelToStringOrMarkup( + leftModel, leftExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null, + env); + if (leftOMOrStr == null) { + return _eval_concatenateHashes(leftModel, rightModel); + } + + // Same trick with null return as above. + Object rightOMOrStr = _EvalUtil.coerceModelToStringOrMarkup( + rightModel, rightExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null, + env); + if (rightOMOrStr == null) { + return _eval_concatenateHashes(leftModel, rightModel); + } + + if (leftOMOrStr instanceof String) { + if (rightOMOrStr instanceof String) { + return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr)); + } else { // rightOMOrStr instanceof TemplateMarkupOutputModel + TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr; + return _EvalUtil.concatMarkupOutputs(parent, + rightMO.getOutputFormat().fromPlainTextByEscaping((String) leftOMOrStr), + rightMO); + } + } else { // leftOMOrStr instanceof TemplateMarkupOutputModel + TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftOMOrStr; + if (rightOMOrStr instanceof String) { // markup output + return _EvalUtil.concatMarkupOutputs(parent, + leftMO, + leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr)); + } else { // rightOMOrStr instanceof TemplateMarkupOutputModel + return _EvalUtil.concatMarkupOutputs(parent, + leftMO, + (TemplateMarkupOutputModel<?>) rightOMOrStr); + } + } + } catch (NonStringOrTemplateOutputException e) { + // 2.4: Remove this catch; it's for BC, after reworking hash addition so it doesn't rely on this. But + // user code might throws this (very unlikely), and then in 2.3.x we did catch that too, incorrectly. + if (hashConcatPossible) { + return _eval_concatenateHashes(leftModel, rightModel); + } else { + throw e; + } + } + } + } + + private static TemplateModel _eval_concatenateHashes(TemplateModel leftModel, TemplateModel rightModel) + throws TemplateModelException { + if (leftModel instanceof TemplateHashModelEx && rightModel instanceof TemplateHashModelEx) { + TemplateHashModelEx leftModelEx = (TemplateHashModelEx) leftModel; + TemplateHashModelEx rightModelEx = (TemplateHashModelEx) rightModel; + if (leftModelEx.size() == 0) { + return rightModelEx; + } else if (rightModelEx.size() == 0) { + return leftModelEx; + } else { + return new ConcatenatedHashEx(leftModelEx, rightModelEx); + } + } else { + return new ConcatenatedHash((TemplateHashModel) leftModel, + (TemplateHashModel) rightModel); + } + } + + static TemplateModel _evalOnNumbers(Environment env, ASTNode parent, Number first, Number second) + throws TemplateException { + ArithmeticEngine ae = _EvalUtil.getArithmeticEngine(env, parent); + return new SimpleNumber(ae.add(first, second)); + } + + @Override + boolean isLiteral() { + return constantValue != null || (left.isLiteral() && right.isLiteral()); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpAddOrConcat( + left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + public String getCanonicalForm() { + return left.getCanonicalForm() + " + " + right.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return "+"; + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + return idx == 0 ? left : right; + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.forBinaryOperatorOperand(idx); + } + + private static final class ConcatenatedSequence + implements + TemplateSequenceModel { + private final TemplateSequenceModel left; + private final TemplateSequenceModel right; + + ConcatenatedSequence(TemplateSequenceModel left, TemplateSequenceModel right) { + this.left = left; + this.right = right; + } + + @Override + public int size() + throws TemplateModelException { + return left.size() + right.size(); + } + + @Override + public TemplateModel get(int i) + throws TemplateModelException { + int ls = left.size(); + return i < ls ? left.get(i) : right.get(i - ls); + } + } + + private static class ConcatenatedHash + implements TemplateHashModel { + protected final TemplateHashModel left; + protected final TemplateHashModel right; + + ConcatenatedHash(TemplateHashModel left, TemplateHashModel right) { + this.left = left; + this.right = right; + } + + @Override + public TemplateModel get(String key) + throws TemplateModelException { + TemplateModel model = right.get(key); + return (model != null) ? model : left.get(key); + } + + @Override + public boolean isEmpty() + throws TemplateModelException { + return left.isEmpty() && right.isEmpty(); + } + } + + private static final class ConcatenatedHashEx + extends ConcatenatedHash + implements TemplateHashModelEx { + private CollectionAndSequence keys; + private CollectionAndSequence values; + private int size; + + ConcatenatedHashEx(TemplateHashModelEx left, TemplateHashModelEx right) { + super(left, right); + } + + @Override + public int size() throws TemplateModelException { + initKeys(); + return size; + } + + @Override + public TemplateCollectionModel keys() + throws TemplateModelException { + initKeys(); + return keys; + } + + @Override + public TemplateCollectionModel values() + throws TemplateModelException { + initValues(); + return values; + } + + private void initKeys() + throws TemplateModelException { + if (keys == null) { + HashSet keySet = new HashSet(); + NativeSequence keySeq = new NativeSequence(32); + addKeys(keySet, keySeq, (TemplateHashModelEx) left); + addKeys(keySet, keySeq, (TemplateHashModelEx) right); + size = keySet.size(); + keys = new CollectionAndSequence(keySeq); + } + } + + private static void addKeys(Set set, NativeSequence keySeq, TemplateHashModelEx hash) + throws TemplateModelException { + TemplateModelIterator it = hash.keys().iterator(); + while (it.hasNext()) { + TemplateScalarModel tsm = (TemplateScalarModel) it.next(); + if (set.add(tsm.getAsString())) { + // The first occurence of the key decides the index; + // this is consisten with stuff like java.util.LinkedHashSet. + keySeq.add(tsm); + } + } + } + + private void initValues() + throws TemplateModelException { + if (values == null) { + NativeSequence seq = new NativeSequence(size()); + // Note: size() invokes initKeys() if needed. + + int ln = keys.size(); + for (int i = 0; i < ln; i++) { + seq.add(get(((TemplateScalarModel) keys.get(i)).getAsString())); + } + values = new CollectionAndSequence(seq); + } + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java new file mode 100644 index 0000000..346d526 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.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 &&} operator + */ +final class ASTExpAnd extends ASTExpBoolean { + + private final ASTExpression lho; + private final ASTExpression rho; + + ASTExpAnd(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 ASTExpAnd( + 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); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java new file mode 100644 index 0000000..d580372 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java @@ -0,0 +1,34 @@ +/* + * 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.TemplateModel; + +/** + * AST expression node superclass for expressions returning a boolean. + */ +abstract class ASTExpBoolean extends ASTExpression { + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return evalToBoolean(env) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java new file mode 100644 index 0000000..e38578b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.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; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateModel; + +/** + * AST expression node: boolean literal + */ +final class ASTExpBooleanLiteral extends ASTExpression { + + private final boolean val; + + public ASTExpBooleanLiteral(boolean val) { + this.val = val; + } + + static TemplateBooleanModel getTemplateModel(boolean b) { + return b? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + + @Override + boolean evalToBoolean(Environment env) { + return val; + } + + @Override + public String getCanonicalForm() { + return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE; + } + + @Override + String getNodeTypeSymbol() { + return getCanonicalForm(); + } + + @Override + public String toString() { + return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE; + } + + @Override + TemplateModel _eval(Environment env) { + return val ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + + @Override + boolean isLiteral() { + return true; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpBooleanLiteral(val); + } + + @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/ASTExpBuiltIn.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java new file mode 100644 index 0000000..be559f6 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java @@ -0,0 +1,485 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.freemarker.core.BuiltInsForDates.iso_BI; +import org.apache.freemarker.core.BuiltInsForDates.iso_utc_or_local_BI; +import org.apache.freemarker.core.BuiltInsForMarkupOutputs.markup_stringBI; +import org.apache.freemarker.core.BuiltInsForMultipleTypes.is_dateLikeBI; +import org.apache.freemarker.core.BuiltInsForNodes.ancestorsBI; +import org.apache.freemarker.core.BuiltInsForNodes.childrenBI; +import org.apache.freemarker.core.BuiltInsForNodes.nextSiblingBI; +import org.apache.freemarker.core.BuiltInsForNodes.node_nameBI; +import org.apache.freemarker.core.BuiltInsForNodes.node_namespaceBI; +import org.apache.freemarker.core.BuiltInsForNodes.node_typeBI; +import org.apache.freemarker.core.BuiltInsForNodes.parentBI; +import org.apache.freemarker.core.BuiltInsForNodes.previousSiblingBI; +import org.apache.freemarker.core.BuiltInsForNodes.rootBI; +import org.apache.freemarker.core.BuiltInsForNumbers.absBI; +import org.apache.freemarker.core.BuiltInsForNumbers.byteBI; +import org.apache.freemarker.core.BuiltInsForNumbers.ceilingBI; +import org.apache.freemarker.core.BuiltInsForNumbers.doubleBI; +import org.apache.freemarker.core.BuiltInsForNumbers.floatBI; +import org.apache.freemarker.core.BuiltInsForNumbers.floorBI; +import org.apache.freemarker.core.BuiltInsForNumbers.intBI; +import org.apache.freemarker.core.BuiltInsForNumbers.is_infiniteBI; +import org.apache.freemarker.core.BuiltInsForNumbers.is_nanBI; +import org.apache.freemarker.core.BuiltInsForNumbers.longBI; +import org.apache.freemarker.core.BuiltInsForNumbers.number_to_dateBI; +import org.apache.freemarker.core.BuiltInsForNumbers.roundBI; +import org.apache.freemarker.core.BuiltInsForNumbers.shortBI; +import org.apache.freemarker.core.BuiltInsForOutputFormatRelated.escBI; +import org.apache.freemarker.core.BuiltInsForOutputFormatRelated.no_escBI; +import org.apache.freemarker.core.BuiltInsForSequences.chunkBI; +import org.apache.freemarker.core.BuiltInsForSequences.firstBI; +import org.apache.freemarker.core.BuiltInsForSequences.lastBI; +import org.apache.freemarker.core.BuiltInsForSequences.reverseBI; +import org.apache.freemarker.core.BuiltInsForSequences.seq_containsBI; +import org.apache.freemarker.core.BuiltInsForSequences.seq_index_ofBI; +import org.apache.freemarker.core.BuiltInsForSequences.sortBI; +import org.apache.freemarker.core.BuiltInsForSequences.sort_byBI; +import org.apache.freemarker.core.BuiltInsForStringsMisc.evalBI; +import org.apache.freemarker.core.model.TemplateDateModel; +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.util._DateUtil; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST expression node: {@code exp?name} + */ +abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable { + + protected ASTExpression target; + protected String key; + + static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>(); + static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>(); + static final int NUMBER_OF_BIS = 263; + static final HashMap<String, ASTExpBuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f); + + static { + // Note that you must update NUMBER_OF_BIS if you add new items here! + + putBI("abs", new absBI()); + putBI("ancestors", new ancestorsBI()); + putBI("api", new BuiltInsForMultipleTypes.apiBI()); + putBI("boolean", new BuiltInsForStringsMisc.booleanBI()); + putBI("byte", new byteBI()); + putBI("c", new BuiltInsForMultipleTypes.cBI()); + putBI("cap_first", "capFirst", new BuiltInsForStringsBasic.cap_firstBI()); + putBI("capitalize", new BuiltInsForStringsBasic.capitalizeBI()); + putBI("ceiling", new ceilingBI()); + putBI("children", new childrenBI()); + putBI("chop_linebreak", "chopLinebreak", new BuiltInsForStringsBasic.chop_linebreakBI()); + putBI("contains", new BuiltInsForStringsBasic.containsBI()); + putBI("date", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATE)); + putBI("date_if_unknown", "dateIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATE)); + putBI("datetime", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATETIME)); + putBI("datetime_if_unknown", "datetimeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATETIME)); + putBI("default", new BuiltInsForExistenceHandling.defaultBI()); + putBI("double", new doubleBI()); + putBI("ends_with", "endsWith", new BuiltInsForStringsBasic.ends_withBI()); + putBI("ensure_ends_with", "ensureEndsWith", new BuiltInsForStringsBasic.ensure_ends_withBI()); + putBI("ensure_starts_with", "ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI()); + putBI("esc", new escBI()); + putBI("eval", new evalBI()); + putBI("exists", new BuiltInsForExistenceHandling.existsBI()); + putBI("first", new firstBI()); + putBI("float", new floatBI()); + putBI("floor", new floorBI()); + putBI("chunk", new chunkBI()); + putBI("counter", new BuiltInsForLoopVariables.counterBI()); + putBI("item_cycle", "itemCycle", new BuiltInsForLoopVariables.item_cycleBI()); + putBI("has_api", "hasApi", new BuiltInsForMultipleTypes.has_apiBI()); + putBI("has_content", "hasContent", new BuiltInsForExistenceHandling.has_contentBI()); + putBI("has_next", "hasNext", new BuiltInsForLoopVariables.has_nextBI()); + putBI("html", new BuiltInsForStringsEncoding.htmlBI()); + putBI("if_exists", "ifExists", new BuiltInsForExistenceHandling.if_existsBI()); + putBI("index", new BuiltInsForLoopVariables.indexBI()); + putBI("index_of", "indexOf", new BuiltInsForStringsBasic.index_ofBI(false)); + putBI("int", new intBI()); + putBI("interpret", new BuiltInsForStringsMisc.interpretBI()); + putBI("is_boolean", "isBoolean", new BuiltInsForMultipleTypes.is_booleanBI()); + putBI("is_collection", "isCollection", new BuiltInsForMultipleTypes.is_collectionBI()); + putBI("is_collection_ex", "isCollectionEx", new BuiltInsForMultipleTypes.is_collection_exBI()); + is_dateLikeBI bi = new BuiltInsForMultipleTypes.is_dateLikeBI(); + putBI("is_date", "isDate", bi); // misnomer + putBI("is_date_like", "isDateLike", bi); + putBI("is_date_only", "isDateOnly", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.DATE)); + putBI("is_even_item", "isEvenItem", new BuiltInsForLoopVariables.is_even_itemBI()); + putBI("is_first", "isFirst", new BuiltInsForLoopVariables.is_firstBI()); + putBI("is_last", "isLast", new BuiltInsForLoopVariables.is_lastBI()); + putBI("is_unknown_date_like", "isUnknownDateLike", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.UNKNOWN)); + putBI("is_datetime", "isDatetime", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.DATETIME)); + putBI("is_directive", "isDirective", new BuiltInsForMultipleTypes.is_directiveBI()); + putBI("is_enumerable", "isEnumerable", new BuiltInsForMultipleTypes.is_enumerableBI()); + putBI("is_hash_ex", "isHashEx", new BuiltInsForMultipleTypes.is_hash_exBI()); + putBI("is_hash", "isHash", new BuiltInsForMultipleTypes.is_hashBI()); + putBI("is_infinite", "isInfinite", new is_infiniteBI()); + putBI("is_indexable", "isIndexable", new BuiltInsForMultipleTypes.is_indexableBI()); + putBI("is_macro", "isMacro", new BuiltInsForMultipleTypes.is_macroBI()); + putBI("is_markup_output", "isMarkupOutput", new BuiltInsForMultipleTypes.is_markup_outputBI()); + putBI("is_method", "isMethod", new BuiltInsForMultipleTypes.is_methodBI()); + putBI("is_nan", "isNan", new is_nanBI()); + putBI("is_node", "isNode", new BuiltInsForMultipleTypes.is_nodeBI()); + putBI("is_number", "isNumber", new BuiltInsForMultipleTypes.is_numberBI()); + putBI("is_odd_item", "isOddItem", new BuiltInsForLoopVariables.is_odd_itemBI()); + putBI("is_sequence", "isSequence", new BuiltInsForMultipleTypes.is_sequenceBI()); + putBI("is_string", "isString", new BuiltInsForMultipleTypes.is_stringBI()); + putBI("is_time", "isTime", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.TIME)); + putBI("is_transform", "isTransform", new BuiltInsForMultipleTypes.is_transformBI()); + + putBI("iso_utc", "isoUtc", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true)); + putBI("iso_utc_fz", "isoUtcFZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.TRUE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true)); + putBI("iso_utc_nz", "isoUtcNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true)); + + putBI("iso_utc_ms", "isoUtcMs", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ true)); + putBI("iso_utc_ms_nz", "isoUtcMsNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ true)); + + putBI("iso_utc_m", "isoUtcM", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ true)); + putBI("iso_utc_m_nz", "isoUtcMNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ true)); + + putBI("iso_utc_h", "isoUtcH", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_HOURS, /* useUTC = */ true)); + putBI("iso_utc_h_nz", "isoUtcHNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS, /* useUTC = */ true)); + + putBI("iso_local", "isoLocal", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ false)); + putBI("iso_local_nz", "isoLocalNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ false)); + + putBI("iso_local_ms", "isoLocalMs", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ false)); + putBI("iso_local_ms_nz", "isoLocalMsNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ false)); + + putBI("iso_local_m", "isoLocalM", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ false)); + putBI("iso_local_m_nz", "isoLocalMNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ false)); + + putBI("iso_local_h", "isoLocalH", new iso_utc_or_local_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_HOURS, /* useUTC = */ false)); + putBI("iso_local_h_nz", "isoLocalHNZ", new iso_utc_or_local_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS, /* useUTC = */ false)); + + putBI("iso", new iso_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS)); + putBI("iso_nz", "isoNZ", new iso_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS)); + + putBI("iso_ms", "isoMs", new iso_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS)); + putBI("iso_ms_nz", "isoMsNZ", new iso_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS)); + + putBI("iso_m", "isoM", new iso_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES)); + putBI("iso_m_nz", "isoMNZ", new iso_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES)); + + putBI("iso_h", "isoH", new iso_BI( + /* showOffset = */ null, _DateUtil.ACCURACY_HOURS)); + putBI("iso_h_nz", "isoHNZ", new iso_BI( + /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS)); + + putBI("j_string", "jString", new BuiltInsForStringsEncoding.j_stringBI()); + putBI("join", new BuiltInsForSequences.joinBI()); + putBI("js_string", "jsString", new BuiltInsForStringsEncoding.js_stringBI()); + putBI("json_string", "jsonString", new BuiltInsForStringsEncoding.json_stringBI()); + putBI("keep_after", "keepAfter", new BuiltInsForStringsBasic.keep_afterBI()); + putBI("keep_before", "keepBefore", new BuiltInsForStringsBasic.keep_beforeBI()); + putBI("keep_after_last", "keepAfterLast", new BuiltInsForStringsBasic.keep_after_lastBI()); + putBI("keep_before_last", "keepBeforeLast", new BuiltInsForStringsBasic.keep_before_lastBI()); + putBI("keys", new BuiltInsForHashes.keysBI()); + putBI("last_index_of", "lastIndexOf", new BuiltInsForStringsBasic.index_ofBI(true)); + putBI("last", new lastBI()); + putBI("left_pad", "leftPad", new BuiltInsForStringsBasic.padBI(true)); + putBI("length", new BuiltInsForStringsBasic.lengthBI()); + putBI("long", new longBI()); + putBI("lower_abc", "lowerAbc", new BuiltInsForNumbers.lower_abcBI()); + putBI("lower_case", "lowerCase", new BuiltInsForStringsBasic.lower_caseBI()); + putBI("namespace", new BuiltInsForMultipleTypes.namespaceBI()); + putBI("new", new BuiltInsForStringsMisc.newBI()); + putBI("markup_string", "markupString", new markup_stringBI()); + putBI("node_name", "nodeName", new node_nameBI()); + putBI("node_namespace", "nodeNamespace", new node_namespaceBI()); + putBI("node_type", "nodeType", new node_typeBI()); + putBI("no_esc", "noEsc", new no_escBI()); + putBI("number", new BuiltInsForStringsMisc.numberBI()); + putBI("number_to_date", "numberToDate", new number_to_dateBI(TemplateDateModel.DATE)); + putBI("number_to_time", "numberToTime", new number_to_dateBI(TemplateDateModel.TIME)); + putBI("number_to_datetime", "numberToDatetime", new number_to_dateBI(TemplateDateModel.DATETIME)); + putBI("parent", new parentBI()); + putBI("previous_sibling", "previousSibling", new previousSiblingBI()); + putBI("next_sibling", "nextSibling", new nextSiblingBI()); + putBI("item_parity", "itemParity", new BuiltInsForLoopVariables.item_parityBI()); + putBI("item_parity_cap", "itemParityCap", new BuiltInsForLoopVariables.item_parity_capBI()); + putBI("reverse", new reverseBI()); + putBI("right_pad", "rightPad", new BuiltInsForStringsBasic.padBI(false)); + putBI("root", new rootBI()); + putBI("round", new roundBI()); + putBI("remove_ending", "removeEnding", new BuiltInsForStringsBasic.remove_endingBI()); + putBI("remove_beginning", "removeBeginning", new BuiltInsForStringsBasic.remove_beginningBI()); + putBI("rtf", new BuiltInsForStringsEncoding.rtfBI()); + putBI("seq_contains", "seqContains", new seq_containsBI()); + putBI("seq_index_of", "seqIndexOf", new seq_index_ofBI(1)); + putBI("seq_last_index_of", "seqLastIndexOf", new seq_index_ofBI(-1)); + putBI("short", new shortBI()); + putBI("size", new BuiltInsForMultipleTypes.sizeBI()); + putBI("sort_by", "sortBy", new sort_byBI()); + putBI("sort", new sortBI()); + putBI("split", new BuiltInsForStringsBasic.split_BI()); + putBI("switch", new BuiltInsWithParseTimeParameters.switch_BI()); + putBI("starts_with", "startsWith", new BuiltInsForStringsBasic.starts_withBI()); + putBI("string", new BuiltInsForMultipleTypes.stringBI()); + putBI("substring", new BuiltInsForStringsBasic.substringBI()); + putBI("then", new BuiltInsWithParseTimeParameters.then_BI()); + putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME)); + putBI("time_if_unknown", "timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME)); + putBI("trim", new BuiltInsForStringsBasic.trimBI()); + putBI("uncap_first", "uncapFirst", new BuiltInsForStringsBasic.uncap_firstBI()); + putBI("upper_abc", "upperAbc", new BuiltInsForNumbers.upper_abcBI()); + putBI("upper_case", "upperCase", new BuiltInsForStringsBasic.upper_caseBI()); + putBI("url", new BuiltInsForStringsEncoding.urlBI()); + putBI("url_path", "urlPath", new BuiltInsForStringsEncoding.urlPathBI()); + putBI("values", new BuiltInsForHashes.valuesBI()); + putBI("web_safe", "webSafe", BUILT_INS_BY_NAME.get("html")); // deprecated; use ?html instead + putBI("word_list", "wordList", new BuiltInsForStringsBasic.word_listBI()); + putBI("xhtml", new BuiltInsForStringsEncoding.xhtmlBI()); + putBI("xml", new BuiltInsForStringsEncoding.xmlBI()); + putBI("matches", new BuiltInsForStringsRegexp.matchesBI()); + putBI("groups", new BuiltInsForStringsRegexp.groupsBI()); + putBI("replace", new BuiltInsForStringsRegexp.replace_reBI()); + + + if (NUMBER_OF_BIS < BUILT_INS_BY_NAME.size()) { + throw new AssertionError("Update NUMBER_OF_BIS! Should be: " + BUILT_INS_BY_NAME.size()); + } + } + + private static void putBI(String name, ASTExpBuiltIn bi) { + BUILT_INS_BY_NAME.put(name, bi); + SNAKE_CASE_NAMES.add(name); + CAMEL_CASE_NAMES.add(name); + } + + private static void putBI(String nameSnakeCase, String nameCamelCase, ASTExpBuiltIn bi) { + BUILT_INS_BY_NAME.put(nameSnakeCase, bi); + BUILT_INS_BY_NAME.put(nameCamelCase, bi); + SNAKE_CASE_NAMES.add(nameSnakeCase); + CAMEL_CASE_NAMES.add(nameCamelCase); + } + + /** + * @param target + * Left-hand-operand expression + * @param keyTk + * Built-in name token + */ + static ASTExpBuiltIn newBuiltIn(int incompatibleImprovements, ASTExpression target, Token keyTk, + FMParserTokenManager tokenManager) throws ParseException { + String key = keyTk.image; + ASTExpBuiltIn bi = BUILT_INS_BY_NAME.get(key); + if (bi == null) { + StringBuilder buf = new StringBuilder("Unknown built-in: ").append(_StringUtil.jQuote(key)).append(". "); + + buf.append( + "Help (latest version): http://freemarker.org/docs/ref_builtins.html; " + + "you're using FreeMarker ").append(Configuration.getVersion()).append(".\n" + + "The alphabetical list of built-ins:"); + List<String> names = new ArrayList<>(BUILT_INS_BY_NAME.keySet().size()); + names.addAll(BUILT_INS_BY_NAME.keySet()); + Collections.sort(names); + char lastLetter = 0; + + 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 : names) { + int correctNameNamingConvetion = _StringUtil.getIdentifierNamingConvention(correctName); + if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION + ? correctNameNamingConvetion != ParsingConfiguration.LEGACY_NAMING_CONVENTION + : correctNameNamingConvetion != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) { + if (first) { + first = false; + } else { + buf.append(", "); + } + + char firstChar = correctName.charAt(0); + if (firstChar != lastLetter) { + lastLetter = firstChar; + buf.append('\n'); + } + buf.append(correctName); + } + } + + throw new ParseException(buf.toString(), null, keyTk); + } + + try { + bi = (ASTExpBuiltIn) bi.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + bi.key = key; + bi.target = target; + return bi; + } + + @Override + public String getCanonicalForm() { + return target.getCanonicalForm() + "?" + key; + } + + @Override + String getNodeTypeSymbol() { + return "?" + key; + } + + @Override + boolean isLiteral() { + return false; // be on the safe side. + } + + protected final void checkMethodArgCount(List args, int expectedCnt) throws TemplateModelException { + checkMethodArgCount(args.size(), expectedCnt); + } + + protected final void checkMethodArgCount(int argCnt, int expectedCnt) throws TemplateModelException { + if (argCnt != expectedCnt) { + throw MessageUtil.newArgCntError("?" + key, argCnt, expectedCnt); + } + } + + protected final void checkMethodArgCount(List args, int minCnt, int maxCnt) throws TemplateModelException { + checkMethodArgCount(args.size(), minCnt, maxCnt); + } + + protected final void checkMethodArgCount(int argCnt, int minCnt, int maxCnt) throws TemplateModelException { + if (argCnt < minCnt || argCnt > maxCnt) { + throw MessageUtil.newArgCntError("?" + key, argCnt, minCnt, maxCnt); + } + } + + /** + * Same as {@link #getStringMethodArg}, but checks if {@code args} is big enough, and returns {@code null} if it + * isn't. + */ + protected final String getOptStringMethodArg(List args, int argIdx) + throws TemplateModelException { + return args.size() > argIdx ? getStringMethodArg(args, argIdx) : null; + } + + /** + * Gets a method argument and checks if it's a string; it does NOT check if {@code args} is big enough. + */ + protected final String getStringMethodArg(List args, int argIdx) + throws TemplateModelException { + TemplateModel arg = (TemplateModel) args.get(argIdx); + if (!(arg instanceof TemplateScalarModel)) { + throw MessageUtil.newMethodArgMustBeStringException("?" + key, argIdx, arg); + } else { + return _EvalUtil.modelToString((TemplateScalarModel) arg, null, null); + } + } + + /** + * Gets a method argument and checks if it's a number; it does NOT check if {@code args} is big enough. + */ + protected final Number getNumberMethodArg(List args, int argIdx) + throws TemplateModelException { + TemplateModel arg = (TemplateModel) args.get(argIdx); + if (!(arg instanceof TemplateNumberModel)) { + throw MessageUtil.newMethodArgMustBeNumberException("?" + key, argIdx, arg); + } else { + return _EvalUtil.modelToNumber((TemplateNumberModel) arg, null); + } + } + + protected final TemplateModelException newMethodArgInvalidValueException(int argIdx, Object[] details) { + return MessageUtil.newMethodArgInvalidValueException("?" + key, argIdx, details); + } + + protected final TemplateModelException newMethodArgsInvalidValueException(Object[] details) { + return MessageUtil.newMethodArgsInvalidValueException("?" + key, details); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + try { + ASTExpBuiltIn clone = (ASTExpBuiltIn) clone(); + clone.target = target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState); + return clone; + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Internal error: " + e); + } + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return target; + case 1: return key; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.LEFT_HAND_OPERAND; + case 1: return ParameterRole.RIGHT_HAND_OPERAND; + 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/ASTExpBuiltInVariable.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java new file mode 100644 index 0000000..ece2099 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java @@ -0,0 +1,298 @@ +/* + * 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.nio.charset.Charset; +import java.util.Arrays; +import java.util.Date; + +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.impl.SimpleDate; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST expression node: {@code .name} + */ +final class ASTExpBuiltInVariable extends ASTExpression { + + static final String TEMPLATE_NAME_CC = "templateName"; + static final String TEMPLATE_NAME = "template_name"; + static final String MAIN_TEMPLATE_NAME_CC = "mainTemplateName"; + static final String MAIN_TEMPLATE_NAME = "main_template_name"; + static final String CURRENT_TEMPLATE_NAME_CC = "currentTemplateName"; + static final String CURRENT_TEMPLATE_NAME = "current_template_name"; + static final String NAMESPACE = "namespace"; + static final String MAIN = "main"; + static final String GLOBALS = "globals"; + static final String LOCALS = "locals"; + static final String DATA_MODEL_CC = "dataModel"; + static final String DATA_MODEL = "data_model"; + static final String LANG = "lang"; + static final String LOCALE = "locale"; + static final String LOCALE_OBJECT_CC = "localeObject"; + static final String LOCALE_OBJECT = "locale_object"; + static final String CURRENT_NODE_CC = "currentNode"; + static final String CURRENT_NODE = "current_node"; + static final String NODE = "node"; + static final String PASS = "pass"; + static final String VARS = "vars"; + static final String VERSION = "version"; + static final String INCOMPATIBLE_IMPROVEMENTS_CC = "incompatibleImprovements"; + static final String INCOMPATIBLE_IMPROVEMENTS = "incompatible_improvements"; + static final String ERROR = "error"; + static final String OUTPUT_ENCODING_CC = "outputEncoding"; + static final String OUTPUT_ENCODING = "output_encoding"; + static final String OUTPUT_FORMAT_CC = "outputFormat"; + static final String OUTPUT_FORMAT = "output_format"; + static final String AUTO_ESC_CC = "autoEsc"; + static final String AUTO_ESC = "auto_esc"; + static final String URL_ESCAPING_CHARSET_CC = "urlEscapingCharset"; + static final String URL_ESCAPING_CHARSET = "url_escaping_charset"; + static final String NOW = "now"; + + static final String[] SPEC_VAR_NAMES = new String[] { + AUTO_ESC_CC, + AUTO_ESC, + CURRENT_NODE_CC, + CURRENT_TEMPLATE_NAME_CC, + CURRENT_NODE, + CURRENT_TEMPLATE_NAME, + DATA_MODEL_CC, + DATA_MODEL, + ERROR, + GLOBALS, + INCOMPATIBLE_IMPROVEMENTS_CC, + INCOMPATIBLE_IMPROVEMENTS, + LANG, + LOCALE, + LOCALE_OBJECT_CC, + LOCALE_OBJECT, + LOCALS, + MAIN, + MAIN_TEMPLATE_NAME_CC, + MAIN_TEMPLATE_NAME, + NAMESPACE, + NODE, + NOW, + OUTPUT_ENCODING_CC, + OUTPUT_FORMAT_CC, + OUTPUT_ENCODING, + OUTPUT_FORMAT, + PASS, + TEMPLATE_NAME_CC, + TEMPLATE_NAME, + URL_ESCAPING_CHARSET_CC, + URL_ESCAPING_CHARSET, + VARS, + VERSION + }; + + private final String name; + private final TemplateModel parseTimeValue; + + ASTExpBuiltInVariable(Token nameTk, FMParserTokenManager tokenManager, TemplateModel parseTimeValue) + throws ParseException { + String name = nameTk.image; + this.parseTimeValue = parseTimeValue; + if (Arrays.binarySearch(SPEC_VAR_NAMES, name) < 0) { + StringBuilder sb = new StringBuilder(); + sb.append("Unknown special variable name: "); + sb.append(_StringUtil.jQuote(name)).append("."); + + int shownNamingConvention; + { + int namingConvention = tokenManager.namingConvention; + shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION + ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */; + } + + { + String correctName; + if (name.equals("auto_escape") || name.equals("auto_escaping") || name.equals("autoesc")) { + correctName = "auto_esc"; + } else if (name.equals("autoEscape") || name.equals("autoEscaping")) { + correctName = "autoEsc"; + } else { + correctName = null; + } + if (correctName != null) { + sb.append(" You may meant: "); + sb.append(_StringUtil.jQuote(correctName)).append("."); + } + } + + sb.append("\nThe allowed special variable names are: "); + boolean first = true; + for (final String correctName : SPEC_VAR_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, nameTk); + } + + this.name = name.intern(); + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + if (parseTimeValue != null) { + return parseTimeValue; + } + if (name == NAMESPACE) { + return env.getCurrentNamespace(); + } + if (name == MAIN) { + return env.getMainNamespace(); + } + if (name == GLOBALS) { + return env.getGlobalVariables(); + } + if (name == LOCALS) { + ASTDirMacro.Context ctx = env.getCurrentMacroContext(); + return ctx == null ? null : ctx.getLocals(); + } + if (name == DATA_MODEL || name == DATA_MODEL_CC) { + return env.getDataModel(); + } + if (name == VARS) { + return new VarsHash(env); + } + if (name == LOCALE) { + return new SimpleScalar(env.getLocale().toString()); + } + if (name == LOCALE_OBJECT || name == LOCALE_OBJECT_CC) { + return env.getObjectWrapper().wrap(env.getLocale()); + } + if (name == LANG) { + return new SimpleScalar(env.getLocale().getLanguage()); + } + if (name == CURRENT_NODE || name == NODE || name == CURRENT_NODE_CC) { + return env.getCurrentVisitorNode(); + } + if (name == MAIN_TEMPLATE_NAME || name == MAIN_TEMPLATE_NAME_CC) { + return SimpleScalar.newInstanceOrNull(env.getMainTemplate().getLookupName()); + } + // [FM3] Some of these two should be removed. + if (name == CURRENT_TEMPLATE_NAME || name == CURRENT_TEMPLATE_NAME_CC + || name == TEMPLATE_NAME || name == TEMPLATE_NAME_CC) { + return SimpleScalar.newInstanceOrNull(env.getCurrentTemplate().getLookupName()); + } + if (name == PASS) { + return ASTDirMacro.DO_NOTHING_MACRO; + } + if (name == OUTPUT_ENCODING || name == OUTPUT_ENCODING_CC) { + Charset encoding = env.getOutputEncoding(); + return encoding != null ? new SimpleScalar(encoding.name()) : null; + } + if (name == URL_ESCAPING_CHARSET || name == URL_ESCAPING_CHARSET_CC) { + Charset charset = env.getURLEscapingCharset(); + return charset != null ? new SimpleScalar(charset.name()) : null; + } + if (name == ERROR) { + return new SimpleScalar(env.getCurrentRecoveredErrorMessage()); + } + if (name == NOW) { + return new SimpleDate(new Date(), TemplateDateModel.DATETIME); + } + if (name == VERSION) { + return new SimpleScalar(Configuration.getVersion().toString()); + } + if (name == INCOMPATIBLE_IMPROVEMENTS || name == INCOMPATIBLE_IMPROVEMENTS_CC) { + return new SimpleScalar(env.getConfiguration().getIncompatibleImprovements().toString()); + } + + throw new _MiscTemplateException(this, + "Invalid special variable: ", name); + } + + @Override + public String toString() { + return "." + name; + } + + @Override + public String getCanonicalForm() { + return "." + name; + } + + @Override + String getNodeTypeSymbol() { + return getCanonicalForm(); + } + + @Override + boolean isLiteral() { + return false; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return this; + } + + static class VarsHash implements TemplateHashModel { + + Environment env; + + VarsHash(Environment env) { + this.env = env; + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + return env.getVariable(key); + } + + @Override + public boolean isEmpty() { + return false; + } + } + + @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/ASTExpComparison.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java new file mode 100644 index 0000000..4e3559f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java @@ -0,0 +1,104 @@ +/* + * 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.util.BugException; + +/** + * AST expression node: Comparison operators, like {@code ==}, {@code !=}, {@code <}, etc. + */ +final class ASTExpComparison extends ASTExpBoolean { + + private final ASTExpression left; + private final ASTExpression right; + private final int operation; + private final String opString; + + ASTExpComparison(ASTExpression left, ASTExpression right, String opString) { + this.left = left; + this.right = right; + opString = opString.intern(); + this.opString = opString; + if (opString == "==" || opString == "=") { + operation = _EvalUtil.CMP_OP_EQUALS; + } else if (opString == "!=") { + operation = _EvalUtil.CMP_OP_NOT_EQUALS; + } else if (opString == "gt" || opString == "\\gt" || opString == ">" || opString == ">") { + operation = _EvalUtil.CMP_OP_GREATER_THAN; + } else if (opString == "gte" || opString == "\\gte" || opString == ">=" || opString == ">=") { + operation = _EvalUtil.CMP_OP_GREATER_THAN_EQUALS; + } else if (opString == "lt" || opString == "\\lt" || opString == "<" || opString == "<") { + operation = _EvalUtil.CMP_OP_LESS_THAN; + } else if (opString == "lte" || opString == "\\lte" || opString == "<=" || opString == "<=") { + operation = _EvalUtil.CMP_OP_LESS_THAN_EQUALS; + } else { + throw new BugException("Unknown comparison operator " + opString); + } + } + + /* + * WARNING! This algorithm is duplicated in SequenceBuiltins.modelsEqual. + * Thus, if you update this method, then you have to update that too! + */ + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return _EvalUtil.compare(left, operation, opString, right, this, env); + } + + @Override + public String getCanonicalForm() { + return left.getCanonicalForm() + ' ' + opString + ' ' + right.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return opString; + } + + @Override + boolean isLiteral() { + return constantValue != null || (left.isLiteral() && right.isLiteral()); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpComparison( + left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + opString); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + return idx == 0 ? left : right; + } + + @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/ASTExpDefault.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java new file mode 100644 index 0000000..b891374 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java @@ -0,0 +1,142 @@ +/* + * 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.Constants; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateHashModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; + +/** {@code exp!defExp}, {@code (exp)!defExp} and the same two with {@code (exp)!}. */ +class ASTExpDefault extends ASTExpression { + + static private class EmptyStringAndSequence + implements TemplateScalarModel, TemplateSequenceModel, TemplateHashModelEx { + @Override + public String getAsString() { + return ""; + } + @Override + public TemplateModel get(int i) { + return null; + } + @Override + public TemplateModel get(String s) { + return null; + } + @Override + public int size() { + return 0; + } + @Override + public boolean isEmpty() { + return true; + } + @Override + public TemplateCollectionModel keys() { + return Constants.EMPTY_COLLECTION; + } + @Override + public TemplateCollectionModel values() { + return Constants.EMPTY_COLLECTION; + } + + } + + static final TemplateModel EMPTY_STRING_AND_SEQUENCE = new EmptyStringAndSequence(); + + private final ASTExpression lho, rho; + + ASTExpDefault(ASTExpression lho, ASTExpression rho) { + this.lho = lho; + this.rho = rho; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel left; + if (lho instanceof ASTExpParenthesis) { + boolean lastFIRE = env.setFastInvalidReferenceExceptions(true); + try { + left = lho.eval(env); + } catch (InvalidReferenceException ire) { + left = null; + } finally { + env.setFastInvalidReferenceExceptions(lastFIRE); + } + } else { + left = lho.eval(env); + } + + if (left != null) return left; + else if (rho == null) return EMPTY_STRING_AND_SEQUENCE; + else return rho.eval(env); + } + + @Override + boolean isLiteral() { + return false; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner(String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpDefault( + lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + rho != null + ? rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState) + : null); + } + + @Override + public String getCanonicalForm() { + if (rho == null) { + return lho.getCanonicalForm() + '!'; + } + return lho.getCanonicalForm() + '!' + rho.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return "...!..."; + } + + @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/ASTExpDot.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java new file mode 100644 index 0000000..1e6a742 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.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.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST expression node: {@code .} operator. + */ +final class ASTExpDot extends ASTExpression { + private final ASTExpression target; + private final String key; + + ASTExpDot(ASTExpression target, String key) { + this.target = target; + this.key = key; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel leftModel = target.eval(env); + if (leftModel instanceof TemplateHashModel) { + return ((TemplateHashModel) leftModel).get(key); + } + throw new NonHashException(target, leftModel, env); + } + + @Override + public String getCanonicalForm() { + return target.getCanonicalForm() + getNodeTypeSymbol() + _StringUtil.toFTLIdentifierReferenceAfterDot(key); + } + + @Override + String getNodeTypeSymbol() { + return "."; + } + + @Override + boolean isLiteral() { + return target.isLiteral(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpDot( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + key); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + return idx == 0 ? target : key; + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.forBinaryOperatorOperand(idx); + } + + String getRHO() { + return key; + } + + boolean onlyHasIdentifiers() { + return (target instanceof ASTExpVariable) || ((target instanceof ASTExpDot) && ((ASTExpDot) target).onlyHasIdentifiers()); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java new file mode 100644 index 0000000..b904ce4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java @@ -0,0 +1,284 @@ +/* + * 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.Constants; +import org.apache.freemarker.core.model.TemplateHashModel; +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.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; + +/** + * AST expression node: {@code target[keyExpression]}, where, in FM 2.3, {@code keyExpression} can be string, a number + * or a range, and {@code target} can be a hash or a sequence. + */ +final class ASTExpDynamicKeyName extends ASTExpression { + + private final ASTExpression keyExpression; + private final ASTExpression target; + + ASTExpDynamicKeyName(ASTExpression target, ASTExpression keyExpression) { + this.target = target; + this.keyExpression = keyExpression; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel targetModel = target.eval(env); + target.assertNonNull(targetModel, env); + + TemplateModel keyModel = keyExpression.eval(env); + keyExpression.assertNonNull(keyModel, env); + if (keyModel instanceof TemplateNumberModel) { + int index = keyExpression.modelToNumber(keyModel, env).intValue(); + return dealWithNumericalKey(targetModel, index, env); + } + if (keyModel instanceof TemplateScalarModel) { + String key = _EvalUtil.modelToString((TemplateScalarModel) keyModel, keyExpression, env); + return dealWithStringKey(targetModel, key, env); + } + if (keyModel instanceof RangeModel) { + return dealWithRangeKey(targetModel, (RangeModel) keyModel, env); + } + throw new UnexpectedTypeException(keyExpression, keyModel, "number, range, or string", + new Class[] { TemplateNumberModel.class, TemplateScalarModel.class, ASTExpRange.class }, env); + } + + static private Class[] NUMERICAL_KEY_LHO_EXPECTED_TYPES; + static { + NUMERICAL_KEY_LHO_EXPECTED_TYPES = new Class[1 + NonStringException.STRING_COERCABLE_TYPES.length]; + NUMERICAL_KEY_LHO_EXPECTED_TYPES[0] = TemplateSequenceModel.class; + for (int i = 0; i < NonStringException.STRING_COERCABLE_TYPES.length; i++) { + NUMERICAL_KEY_LHO_EXPECTED_TYPES[i + 1] = NonStringException.STRING_COERCABLE_TYPES[i]; + } + } + + private TemplateModel dealWithNumericalKey(TemplateModel targetModel, + int index, + Environment env) + throws TemplateException { + if (targetModel instanceof TemplateSequenceModel) { + TemplateSequenceModel tsm = (TemplateSequenceModel) targetModel; + int size; + try { + size = tsm.size(); + } catch (Exception e) { + size = Integer.MAX_VALUE; + } + return index < size ? tsm.get(index) : null; + } + + try { + String s = target.evalAndCoerceToPlainText(env); + try { + return new SimpleScalar(s.substring(index, index + 1)); + } catch (IndexOutOfBoundsException e) { + if (index < 0) { + throw new _MiscTemplateException("Negative index not allowed: ", Integer.valueOf(index)); + } + if (index >= s.length()) { + throw new _MiscTemplateException( + "String index out of range: The index was ", Integer.valueOf(index), + " (0-based), but the length of the string is only ", Integer.valueOf(s.length()) , "."); + } + throw new RuntimeException("Can't explain exception", e); + } + } catch (NonStringException e) { + throw new UnexpectedTypeException( + target, targetModel, + "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC, + NUMERICAL_KEY_LHO_EXPECTED_TYPES, + (targetModel instanceof TemplateHashModel + ? "You had a numberical value inside the []. Currently that's only supported for " + + "sequences (lists) and strings. To get a Map item with a non-string key, " + + "use myMap?api.get(myKey)." + : null), + env); + } + } + + private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env) + throws TemplateException { + if (targetModel instanceof TemplateHashModel) { + return((TemplateHashModel) targetModel).get(key); + } + throw new NonHashException(target, targetModel, env); + } + + private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env) + throws TemplateException { + final TemplateSequenceModel targetSeq; + final String targetStr; + if (targetModel instanceof TemplateSequenceModel) { + targetSeq = (TemplateSequenceModel) targetModel; + targetStr = null; + } else { + targetSeq = null; + try { + targetStr = target.evalAndCoerceToPlainText(env); + } catch (NonStringException e) { + throw new UnexpectedTypeException( + target, target.eval(env), + "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC, + NUMERICAL_KEY_LHO_EXPECTED_TYPES, env); + } + } + + final int size = range.size(); + final boolean rightUnbounded = range.isRightUnbounded(); + final boolean rightAdaptive = range.isRightAdaptive(); + + // Right bounded empty ranges are accepted even if the begin index is out of bounds. That's because a such range + // produces an empty sequence, which thus doesn't contain any illegal indexes. + if (!rightUnbounded && size == 0) { + return emptyResult(targetSeq != null); + } + + final int firstIdx = range.getBegining(); + if (firstIdx < 0) { + throw new _MiscTemplateException(keyExpression, + "Negative range start index (", Integer.valueOf(firstIdx), + ") isn't allowed for a range used for slicing."); + } + + final int targetSize = targetStr != null ? targetStr.length() : targetSeq.size(); + final int step = range.getStep(); + + // Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like + // ranges with exclusive end index of at most targetSize. Thence a such range is just an empty list of indexes, + // and thus it isn't out-of-bounds. + // Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a to high firstIndex. + // Right-bounded ranges at this point aren't empty, so the right index surely can't reach targetSize. + if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) { + throw new _MiscTemplateException(keyExpression, + "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ", + (targetStr != null ? "string" : "sequence"), + " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"), + ". ", "(Note that indices are 0-based)."); + } + + final int resultSize; + if (!rightUnbounded) { + final int lastIdx = firstIdx + (size - 1) * step; + if (lastIdx < 0) { + if (!rightAdaptive) { + throw new _MiscTemplateException(keyExpression, + "Negative range end index (", Integer.valueOf(lastIdx), + ") isn't allowed for a range used for slicing."); + } else { + resultSize = firstIdx + 1; + } + } else if (lastIdx >= targetSize) { + if (!rightAdaptive) { + throw new _MiscTemplateException(keyExpression, + "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, because the sliced ", + (targetStr != null ? "string" : "sequence"), + " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"), + ". (Note that indices are 0-based)."); + } else { + resultSize = Math.abs(targetSize - firstIdx); + } + } else { + resultSize = size; + } + } else { + resultSize = targetSize - firstIdx; + } + + if (resultSize == 0) { + return emptyResult(targetSeq != null); + } + if (targetSeq != null) { + NativeSequence resultSeq = new NativeSequence(resultSize); + int srcIdx = firstIdx; + for (int i = 0; i < resultSize; i++) { + resultSeq.add(targetSeq.get(srcIdx)); + srcIdx += step; + } + // List items are already wrapped, so the wrapper will be null: + return resultSeq; + } else { + final int exclEndIdx; + if (step < 0 && resultSize > 1) { + if (!(range.isAffactedByStringSlicingBug() && resultSize == 2)) { + throw new _MiscTemplateException(keyExpression, + "Decreasing ranges aren't allowed for slicing strings (as it would give reversed text). " + + "The index range was: first = ", Integer.valueOf(firstIdx), + ", last = ", Integer.valueOf(firstIdx + (resultSize - 1) * step)); + } else { + // Emulate the legacy bug, where "foo"[n .. n-1] gives "" instead of an error (if n >= 1). + // Fix this in FTL [2.4] + exclEndIdx = firstIdx; + } + } else { + exclEndIdx = firstIdx + resultSize; + } + + return new SimpleScalar(targetStr.substring(firstIdx, exclEndIdx)); + } + } + + private TemplateModel emptyResult(boolean seq) { + return seq ? Constants.EMPTY_SEQUENCE : TemplateScalarModel.EMPTY_STRING; + } + + @Override + public String getCanonicalForm() { + return target.getCanonicalForm() + + "[" + + keyExpression.getCanonicalForm() + + "]"; + } + + @Override + String getNodeTypeSymbol() { + return "...[...]"; + } + + @Override + boolean isLiteral() { + return constantValue != null || (target.isLiteral() && keyExpression.isLiteral()); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + return idx == 0 ? target : keyExpression; + } + + @Override + ParameterRole getParameterRole(int idx) { + return idx == 0 ? ParameterRole.LEFT_HAND_OPERAND : ParameterRole.ENCLOSED_OPERAND; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpDynamicKeyName( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + keyExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java new file mode 100644 index 0000000..72b8182 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.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; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateModel; + +/** + * AST expression node: {@code ??} operator. + */ +class ASTExpExists extends ASTExpression { + + protected final ASTExpression exp; + + ASTExpExists(ASTExpression exp) { + this.exp = exp; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel tm; + if (exp instanceof ASTExpParenthesis) { + boolean lastFIRE = env.setFastInvalidReferenceExceptions(true); + try { + tm = exp.eval(env); + } catch (InvalidReferenceException ire) { + tm = null; + } finally { + env.setFastInvalidReferenceExceptions(lastFIRE); + } + } else { + tm = exp.eval(env); + } + return tm == null ? TemplateBooleanModel.FALSE : TemplateBooleanModel.TRUE; + } + + @Override + boolean isLiteral() { + return false; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner(String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpExists( + exp.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + public String getCanonicalForm() { + return exp.getCanonicalForm() + getNodeTypeSymbol(); + } + + @Override + String getNodeTypeSymbol() { + return "??"; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + return exp; + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.LEFT_HAND_OPERAND; + } + +}
