You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:53:28 UTC
[45/51] [partial] incubator-freemarker git commit: Migrated from Ant
to Gradle, and modularized the project. This is an incomplete migration;
there are some TODO-s in the build scripts, and release related tasks are
still missing. What works: Building th
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;
+ }
+
+}