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/02/16 23:08:28 UTC
[03/54] [partial] incubator-freemarker git commit: Top level package
name change to org.apache.freemarker,
and some of of the internal package structure changes. Other smaller cleanup.
To be continued...
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/Version.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/Version.java b/src/main/java/org/apache/freemarker/core/Version.java
new file mode 100644
index 0000000..037f89b
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/Version.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import org.apache.freemarker.core.util.StringUtil;
+
+/**
+ * Represents a version number plus the further qualifiers and build info. This is
+ * mostly used for representing a FreeMarker version number, but should also be able
+ * to parse the version strings of 3rd party libraries.
+ *
+ * @see Configuration#getVersion()
+ *
+ * @since 2.3.20
+ */
+public final class Version implements Serializable {
+
+ private final int major;
+ private final int minor;
+ private final int micro;
+ private final String extraInfo;
+ private final String originalStringValue;
+
+ private final Boolean gaeCompliant;
+ private final Date buildDate;
+
+ private final int intValue;
+ private volatile String calculatedStringValue; // not final because it's calculated on demand
+ private int hashCode; // not final because it's calculated on demand
+
+ /**
+ * @throws IllegalArgumentException if the version string is malformed
+ */
+ public Version(String stringValue) {
+ this(stringValue, null, null);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the version string is malformed
+ */
+ public Version(String stringValue, Boolean gaeCompliant, Date buildDate) {
+ stringValue = stringValue.trim();
+ originalStringValue = stringValue;
+
+ int[] parts = new int[3];
+ String extraInfoTmp = null;
+ {
+ int partIdx = 0;
+ for (int i = 0; i < stringValue.length(); i++) {
+ char c = stringValue.charAt(i);
+ if (isNumber(c)) {
+ parts[partIdx] = parts[partIdx] * 10 + (c - '0');
+ } else {
+ if (i == 0) {
+ throw new IllegalArgumentException(
+ "The version number string " + StringUtil.jQuote(stringValue)
+ + " doesn't start with a number.");
+ }
+ if (c == '.') {
+ char nextC = i + 1 >= stringValue.length() ? 0 : stringValue.charAt(i + 1);
+ if (nextC == '.') {
+ throw new IllegalArgumentException(
+ "The version number string " + StringUtil.jQuote(stringValue)
+ + " contains multiple dots after a number.");
+ }
+ if (partIdx == 2 || !isNumber(nextC)) {
+ extraInfoTmp = stringValue.substring(i);
+ break;
+ } else {
+ partIdx++;
+ }
+ } else {
+ extraInfoTmp = stringValue.substring(i);
+ break;
+ }
+ }
+ }
+
+ if (extraInfoTmp != null) {
+ char firstChar = extraInfoTmp.charAt(0);
+ if (firstChar == '.' || firstChar == '-' || firstChar == '_') {
+ extraInfoTmp = extraInfoTmp.substring(1);
+ if (extraInfoTmp.length() == 0) {
+ throw new IllegalArgumentException(
+ "The version number string " + StringUtil.jQuote(stringValue)
+ + " has an extra info section opened with \"" + firstChar + "\", but it's empty.");
+ }
+ }
+ }
+ }
+ extraInfo = extraInfoTmp;
+
+ major = parts[0];
+ minor = parts[1];
+ micro = parts[2];
+ intValue = calculateIntValue();
+
+ this.gaeCompliant = gaeCompliant;
+ this.buildDate = buildDate;
+
+ }
+
+ private boolean isNumber(char c) {
+ return c >= '0' && c <= '9';
+ }
+
+ public Version(int major, int minor, int micro) {
+ this(major, minor, micro, null, null, null);
+ }
+
+ /**
+ * Creates an object based on the {@code int} value that uses the same kind of encoding as {@link #intValue()}.
+ *
+ * @since 2.3.24
+ */
+ public Version(int intValue) {
+ this.intValue = intValue;
+
+ this.micro = intValue % 1000;
+ this.minor = (intValue / 1000) % 1000;
+ this.major = intValue / 1000000;
+
+ this.extraInfo = null;
+ this.gaeCompliant = null;
+ this.buildDate = null;
+ originalStringValue = null;
+ }
+
+ public Version(int major, int minor, int micro, String extraInfo, Boolean gaeCompatible, Date buildDate) {
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ this.extraInfo = extraInfo;
+ this.gaeCompliant = gaeCompatible;
+ this.buildDate = buildDate;
+ intValue = calculateIntValue();
+ originalStringValue = null;
+ }
+
+ private int calculateIntValue() {
+ return intValueFor(major, minor, micro);
+ }
+
+ static public int intValueFor(int major, int minor, int micro) {
+ return major * 1000000 + minor * 1000 + micro;
+ }
+
+ private String getStringValue() {
+ if (originalStringValue != null) return originalStringValue;
+
+ String calculatedStringValue = this.calculatedStringValue;
+ if (calculatedStringValue == null) {
+ synchronized (this) {
+ calculatedStringValue = this.calculatedStringValue;
+ if (calculatedStringValue == null) {
+ calculatedStringValue = major + "." + minor + "." + micro;
+ if (extraInfo != null) calculatedStringValue += "-" + extraInfo;
+ this.calculatedStringValue = calculatedStringValue;
+ }
+ }
+ }
+ return calculatedStringValue;
+ }
+
+ /**
+ * Contains the major.minor.micor numbers and the extraInfo part, not the other information.
+ */
+ @Override
+ public String toString() {
+ return getStringValue();
+ }
+
+ /**
+ * The 1st version number, like 1 in "1.2.3".
+ */
+ public int getMajor() {
+ return major;
+ }
+
+ /**
+ * The 2nd version number, like 2 in "1.2.3".
+ */
+ public int getMinor() {
+ return minor;
+ }
+
+ /**
+ * The 3rd version number, like 3 in "1.2.3".
+ */
+ public int getMicro() {
+ return micro;
+ }
+
+ /**
+ * The arbitrary string after the micro version number without leading dot, dash or underscore,
+ * like "RC03" in "2.4.0-RC03".
+ * This is usually a qualifier (RC, SNAPHOST, nightly, beta, etc) and sometimes build info (like
+ * date).
+ */
+ public String getExtraInfo() {
+ return extraInfo;
+ }
+
+ /**
+ * @return The Google App Engine compliance, or {@code null}.
+ */
+ public Boolean isGAECompliant() {
+ return gaeCompliant;
+ }
+
+ /**
+ * @return The build date if known, or {@code null}.
+ */
+ public Date getBuildDate() {
+ return buildDate;
+ }
+
+ /**
+ * @return major * 1000000 + minor * 1000 + micro.
+ */
+ public int intValue() {
+ return intValue;
+ }
+
+ @Override
+ public int hashCode() {
+ int r = hashCode;
+ if (r != 0) return r;
+ synchronized (this) {
+ if (hashCode == 0) {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (buildDate == null ? 0 : buildDate.hashCode());
+ result = prime * result + (extraInfo == null ? 0 : extraInfo.hashCode());
+ result = prime * result + (gaeCompliant == null ? 0 : gaeCompliant.hashCode());
+ result = prime * result + intValue;
+ if (result == 0) result = -1; // 0 is reserved for "not set"
+ hashCode = result;
+ }
+ return hashCode;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+
+ Version other = (Version) obj;
+
+ if (intValue != other.intValue) return false;
+
+ if (other.hashCode() != hashCode()) return false;
+
+ if (buildDate == null) {
+ if (other.buildDate != null) return false;
+ } else if (!buildDate.equals(other.buildDate)) {
+ return false;
+ }
+
+ if (extraInfo == null) {
+ if (other.extraInfo != null) return false;
+ } else if (!extraInfo.equals(other.extraInfo)) {
+ return false;
+ }
+
+ if (gaeCompliant == null) {
+ if (other.gaeCompliant != null) return false;
+ } else if (!gaeCompliant.equals(other.gaeCompliant)) {
+ return false;
+ }
+
+ return true;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/_CoreLogs.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/_CoreLogs.java b/src/main/java/org/apache/freemarker/core/_CoreLogs.java
new file mode 100644
index 0000000..0e16f6c
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/_CoreLogs.java
@@ -0,0 +1,45 @@
+/*
+ * 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't.
+ */
+public final class _CoreLogs {
+
+ public static final Logger RUNTIME = LoggerFactory.getLogger("org.apache.freemarker.core.runtime");
+ public static final Logger ATTEMPT = LoggerFactory.getLogger("org.apache.freemarker.core.runtime.attempt");
+ public static final Logger SECURITY = LoggerFactory.getLogger("org.apache.freemarker.core.security");
+ public static final Logger BEANS_WRAPPER = LoggerFactory.getLogger("org.apache.freemarker.core.model.impl.beans");
+ public static final Logger DOM_WRAPPER = LoggerFactory.getLogger("org.apache.freemarker.core.model.impl.dom");
+ public static final Logger TEMPLATE_RESOLVER = LoggerFactory.getLogger(
+ "org.apache.freemarker.core.templateresolver");
+ public static final Logger DEBUG_SERVER = LoggerFactory.getLogger("org.apache.freemarker.core.debug.server");
+ public static final Logger DEBUG_CLIENT = LoggerFactory.getLogger("org.apache.freemarker.core.debug.client");
+
+ private _CoreLogs() {
+ // Not meant to be instantiated
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/_TemplateAPI.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/_TemplateAPI.java b/src/main/java/org/apache/freemarker/core/_TemplateAPI.java
new file mode 100644
index 0000000..7e8d330
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/_TemplateAPI.java
@@ -0,0 +1,143 @@
+/*
+ * 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.Set;
+
+import org.apache.freemarker.core.ast.Expression;
+import org.apache.freemarker.core.ast.OutputFormat;
+import org.apache.freemarker.core.ast.TemplateObject;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.util.NullArgumentException;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't.
+ */
+public class _TemplateAPI {
+
+ public static final int VERSION_INT_2_3_0 = Configuration.VERSION_2_3_0.intValue();
+ public static final int VERSION_INT_2_3_19 = Configuration.VERSION_2_3_19.intValue();
+ public static final int VERSION_INT_2_3_20 = Configuration.VERSION_2_3_20.intValue();
+ public static final int VERSION_INT_2_3_21 = Configuration.VERSION_2_3_21.intValue();
+ public static final int VERSION_INT_2_3_22 = Configuration.VERSION_2_3_22.intValue();
+ public static final int VERSION_INT_2_3_23 = Configuration.VERSION_2_3_23.intValue();
+ public static final int VERSION_INT_2_3_24 = Configuration.VERSION_2_3_24.intValue();
+ public static final int VERSION_INT_2_3_25 = Configuration.VERSION_2_3_25.intValue();
+ public static final int VERSION_INT_2_3_26 = Configuration.VERSION_2_3_26.intValue();
+ public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0);
+
+ public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
+ NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
+ int iciV = incompatibleImprovements.intValue();
+ if (iciV > Configuration.getVersion().intValue()) {
+ throw new IllegalArgumentException("The FreeMarker version requested by \"incompatibleImprovements\" was "
+ + incompatibleImprovements + ", but the installed FreeMarker version is only "
+ + Configuration.getVersion() + ". You may need to upgrade FreeMarker in your project.");
+ }
+ if (iciV < VERSION_INT_2_3_0) {
+ throw new IllegalArgumentException("\"incompatibleImprovements\" must be at least 2.3.0.");
+ }
+ }
+
+ public static int getTemplateLanguageVersionAsInt(TemplateObject to) {
+ return getTemplateLanguageVersionAsInt(to.getTemplate());
+ }
+
+ public static int getTemplateLanguageVersionAsInt(Template t) {
+ return t.getTemplateLanguageVersion().intValue();
+ }
+
+ public static TemplateExceptionHandler getDefaultTemplateExceptionHandler(
+ Version incompatibleImprovements) {
+ return Configuration.getDefaultTemplateExceptionHandler(incompatibleImprovements);
+ }
+
+ public static boolean getDefaultLogTemplateExceptions(Version incompatibleImprovements) {
+ return Configuration.getDefaultLogTemplateExceptions(incompatibleImprovements);
+ }
+
+ public static CacheStorage createDefaultCacheStorage(Version incompatibleImprovements) {
+ return Configuration.createDefaultCacheStorage(incompatibleImprovements);
+ }
+
+ public static TemplateLookupStrategy getDefaultTemplateLookupStrategy(Version incompatibleImprovements) {
+ return Configuration.getDefaultTemplateLookupStrategy(incompatibleImprovements);
+ }
+
+ public static TemplateNameFormat getDefaultTemplateNameFormat(Version incompatibleImprovements) {
+ return Configuration.getDefaultTemplateNameFormat(incompatibleImprovements);
+ }
+
+ /**
+ * [2.4] getSettingNames() becomes to public; remove this.
+ */
+ public static Set/*<String>*/ getConfigurationSettingNames(Configuration cfg, boolean camelCase) {
+ return cfg.getSettingNames(camelCase);
+ }
+
+ public static void setAutoEscaping(Template t, boolean autoEscaping) {
+ t.setAutoEscaping(autoEscaping);
+ }
+
+ public static void setOutputFormat(Template t, OutputFormat outputFormat) {
+ t.setOutputFormat(outputFormat);
+ }
+
+ public static void validateAutoEscapingPolicyValue(int autoEscaping) {
+ if (autoEscaping != Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY
+ && autoEscaping != Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY
+ && autoEscaping != Configuration.DISABLE_AUTO_ESCAPING_POLICY) {
+ throw new IllegalArgumentException("\"auto_escaping\" can only be set to one of these: "
+ + "Configuration.ENABLE_AUTO_ESCAPING_IF_DEFAULT, "
+ + "or Configuration.ENABLE_AUTO_ESCAPING_IF_SUPPORTED"
+ + "or Configuration.DISABLE_AUTO_ESCAPING");
+ }
+ }
+
+ public static void validateNamingConventionValue(int namingConvention) {
+ if (namingConvention != Configuration.AUTO_DETECT_NAMING_CONVENTION
+ && namingConvention != Configuration.LEGACY_NAMING_CONVENTION
+ && namingConvention != Configuration.CAMEL_CASE_NAMING_CONVENTION) {
+ throw new IllegalArgumentException("\"naming_convention\" can only be set to one of these: "
+ + "Configuration.AUTO_DETECT_NAMING_CONVENTION, "
+ + "or Configuration.LEGACY_NAMING_CONVENTION"
+ + "or Configuration.CAMEL_CASE_NAMING_CONVENTION");
+ }
+ }
+
+ public static void valideTagSyntaxValue(int tagSyntax) {
+ if (tagSyntax != Configuration.AUTO_DETECT_TAG_SYNTAX
+ && tagSyntax != Configuration.SQUARE_BRACKET_TAG_SYNTAX
+ && tagSyntax != Configuration.ANGLE_BRACKET_TAG_SYNTAX) {
+ throw new IllegalArgumentException("\"tag_syntax\" can only be set to one of these: "
+ + "Configuration.AUTO_DETECT_TAG_SYNTAX, Configuration.ANGLE_BRACKET_SYNTAX, "
+ + "or Configuration.SQAUARE_BRACKET_SYNTAX");
+ }
+ }
+
+ public static Expression getBlamedExpression(TemplateException e) {
+ return e.getBlamedExpression();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/ast/APINotSupportedTemplateException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/APINotSupportedTemplateException.java b/src/main/java/org/apache/freemarker/core/ast/APINotSupportedTemplateException.java
new file mode 100644
index 0000000..a34ee92
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/APINotSupportedTemplateException.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.ast;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core._TemplateAPI;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleHash;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+
+/**
+ * Thrown when {@code ?api} is not supported by a value.
+ */
+class APINotSupportedTemplateException extends TemplateException {
+
+ APINotSupportedTemplateException(Environment env, Expression blamedExpr, TemplateModel model) {
+ super(null, env, blamedExpr, buildDescription(env, blamedExpr, model));
+ }
+
+ protected static _ErrorDescriptionBuilder buildDescription(Environment env, Expression blamedExpr,
+ TemplateModel tm) {
+ final _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+ "The value doesn't support ?api. See requirements in the FreeMarker Manual. ("
+ + "FTL type: ", new _DelayedFTLTypeDescription(tm),
+ ", TemplateModel class: ", new _DelayedShortClassName(tm.getClass()),
+ ", ObjectWapper: ", new _DelayedToString(env.getObjectWrapper()), ")"
+ ).blame(blamedExpr);
+
+ if (blamedExpr.isLiteral()) {
+ desc.tip("Only adapted Java objects can possibly have API, not values created inside templates.");
+ } else {
+ ObjectWrapper ow = env.getObjectWrapper();
+ if (ow instanceof DefaultObjectWrapper
+ && (tm instanceof SimpleHash || tm instanceof SimpleSequence)) {
+ DefaultObjectWrapper dow = (DefaultObjectWrapper) ow;
+ if (!dow.getUseAdaptersForContainers()) {
+ desc.tip("In the FreeMarker configuration, \"", Configurable.OBJECT_WRAPPER_KEY,
+ "\" is a DefaultObjectWrapper with its \"useAdaptersForContainers\" property set to "
+ + "false. Setting it to true might solves this problem.");
+ if (dow.getIncompatibleImprovements().intValue() < _TemplateAPI.VERSION_INT_2_3_22) {
+ desc.tip("Setting DefaultObjectWrapper's \"incompatibleImprovements\" to 2.3.22 or higher will "
+ + "change the default value of \"useAdaptersForContainers\" to true.");
+ }
+ } else if (tm instanceof SimpleSequence && dow.getForceLegacyNonListCollections()) {
+ desc.tip("In the FreeMarker configuration, \"",
+ Configurable.OBJECT_WRAPPER_KEY,
+ "\" is a DefaultObjectWrapper with its \"forceLegacyNonListCollections\" property set "
+ + "to true. If you are trying to access the API of a non-List Collection, setting the "
+ + "\"forceLegacyNonListCollections\" property to false might solves this problem.");
+ }
+ }
+ }
+
+ return desc;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/ast/AddConcatExpression.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/AddConcatExpression.java b/src/main/java/org/apache/freemarker/core/ast/AddConcatExpression.java
new file mode 100644
index 0000000..cc126e0
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/AddConcatExpression.java
@@ -0,0 +1,306 @@
+/*
+ * 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;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.TemplateException;
+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.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.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+
+/**
+ * An operator for the + operator. Note that this is treated
+ * separately from the other 4 arithmetic operators,
+ * since + is overloaded to mean string concatenation.
+ */
+final class AddConcatExpression extends Expression {
+
+ private final Expression left;
+ private final Expression right;
+
+ AddConcatExpression(Expression left, Expression 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,
+ TemplateObject parent,
+ Expression leftExp, TemplateModel leftModel,
+ Expression rightExp, TemplateModel rightModel)
+ throws TemplateModelException, TemplateException, NonStringException {
+ 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, (String) 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, (String) 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, TemplateObject 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 Expression deepCloneWithIdentifierReplaced_inner(
+ String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
+ return new AddConcatExpression(
+ 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;
+ }
+
+ public int size()
+ throws TemplateModelException {
+ return left.size() + right.size();
+ }
+
+ 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;
+ }
+
+ public TemplateModel get(String key)
+ throws TemplateModelException {
+ TemplateModel model = right.get(key);
+ return (model != null) ? model : left.get(key);
+ }
+
+ 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);
+ }
+
+ public int size() throws TemplateModelException {
+ initKeys();
+ return size;
+ }
+
+ public TemplateCollectionModel keys()
+ throws TemplateModelException {
+ initKeys();
+ return keys;
+ }
+
+ public TemplateCollectionModel values()
+ throws TemplateModelException {
+ initValues();
+ return values;
+ }
+
+ private void initKeys()
+ throws TemplateModelException {
+ if (keys == null) {
+ HashSet keySet = new HashSet();
+ SimpleSequence keySeq = new SimpleSequence(32);
+ addKeys(keySet, keySeq, (TemplateHashModelEx) this.left);
+ addKeys(keySet, keySeq, (TemplateHashModelEx) this.right);
+ size = keySet.size();
+ keys = new CollectionAndSequence(keySeq);
+ }
+ }
+
+ private static void addKeys(Set set, SimpleSequence 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) {
+ SimpleSequence seq = new SimpleSequence(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/ecb4e230/src/main/java/org/apache/freemarker/core/ast/AliasTargetTemplateValueFormatException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/AliasTargetTemplateValueFormatException.java b/src/main/java/org/apache/freemarker/core/ast/AliasTargetTemplateValueFormatException.java
new file mode 100644
index 0000000..0a4bd02
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/AliasTargetTemplateValueFormatException.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Can't create a template format that the template format refers to (typically thrown by alias template formats).
+ *
+ * @since 2.3.24
+ */
+class AliasTargetTemplateValueFormatException extends TemplateValueFormatException {
+
+ public AliasTargetTemplateValueFormatException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AliasTargetTemplateValueFormatException(String message) {
+ super(message);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/ast/AliasTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/AliasTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/ast/AliasTemplateDateFormatFactory.java
new file mode 100644
index 0000000..0ae7ccd
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/AliasTemplateDateFormatFactory.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core.ast;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.util.StringUtil;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ *
+ * @since 2.3.24
+ */
+public final class AliasTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+ private final String defaultTargetFormatString;
+ private final Map<Locale, String> localizedTargetFormatStrings;
+
+ /**
+ * @param targetFormatString
+ * The format string this format will be an alias to.
+ */
+ public AliasTemplateDateFormatFactory(String targetFormatString) {
+ this.defaultTargetFormatString = targetFormatString;
+ localizedTargetFormatStrings = null;
+ }
+
+ /**
+ * @param defaultTargetFormatString
+ * The format string this format will be an alias to if there's no locale-specific format string for the
+ * requested locale in {@code localizedTargetFormatStrings}
+ * @param localizedTargetFormatStrings
+ * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+ * specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+ * {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+ * this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+ * {@code new Locale("en")}. If there's still no matching key, the value of the
+ * {@code targetFormatString} will be used.
+ */
+ public AliasTemplateDateFormatFactory(
+ String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+ this.defaultTargetFormatString = defaultTargetFormatString;
+ this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+ }
+
+ @Override
+ public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+ Environment env) throws TemplateValueFormatException {
+ TemplateFormatUtil.checkHasNoParameters(params);
+ try {
+ String targetFormatString;
+ if (localizedTargetFormatStrings != null) {
+ Locale lookupLocale = locale;
+ targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+ while (targetFormatString == null
+ && (lookupLocale = _CoreLocaleUtils.getLessSpecificLocale(lookupLocale)) != null) {
+ targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+ }
+ } else {
+ targetFormatString = null;
+ }
+ if (targetFormatString == null) {
+ targetFormatString = this.defaultTargetFormatString;
+ }
+ return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput);
+ } catch (TemplateValueFormatException e) {
+ throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string, "
+ + StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/ast/AliasTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/AliasTemplateNumberFormatFactory.java b/src/main/java/org/apache/freemarker/core/ast/AliasTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..93cd083
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/AliasTemplateNumberFormatFactory.java
@@ -0,0 +1,90 @@
+/*
+ * 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;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.freemarker.core.util.StringUtil;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ *
+ * @since 2.3.24
+ */
+public final class AliasTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+ private final String defaultTargetFormatString;
+ private final Map<Locale, String> localizedTargetFormatStrings;
+
+ /**
+ * @param targetFormatString
+ * The format string this format will be an alias to
+ */
+ public AliasTemplateNumberFormatFactory(String targetFormatString) {
+ this.defaultTargetFormatString = targetFormatString;
+ localizedTargetFormatStrings = null;
+ }
+
+ /**
+ * @param defaultTargetFormatString
+ * The format string this format will be an alias to if there's no locale-specific format string for the
+ * requested locale in {@code localizedTargetFormatStrings}
+ * @param localizedTargetFormatStrings
+ * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+ * specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+ * {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+ * this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+ * {@code new Locale("en")}. If there's still no matching key, the value of the
+ * {@code targetFormatString} will be used.
+ */
+ public AliasTemplateNumberFormatFactory(
+ String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+ this.defaultTargetFormatString = defaultTargetFormatString;
+ this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+ }
+
+ @Override
+ public TemplateNumberFormat get(String params, Locale locale, Environment env)
+ throws TemplateValueFormatException {
+ TemplateFormatUtil.checkHasNoParameters(params);
+ try {
+ String targetFormatString;
+ if (localizedTargetFormatStrings != null) {
+ Locale lookupLocale = locale;
+ targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+ while (targetFormatString == null
+ && (lookupLocale = _CoreLocaleUtils.getLessSpecificLocale(lookupLocale)) != null) {
+ targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+ }
+ } else {
+ targetFormatString = null;
+ }
+ if (targetFormatString == null) {
+ targetFormatString = this.defaultTargetFormatString;
+ }
+ return env.getTemplateNumberFormat(targetFormatString, locale);
+ } catch (TemplateValueFormatException e) {
+ throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string, "
+ + StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/ast/AndExpression.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/AndExpression.java b/src/main/java/org/apache/freemarker/core/ast/AndExpression.java
new file mode 100644
index 0000000..16a3cd3
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/AndExpression.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.ast;
+
+import org.apache.freemarker.core.TemplateException;
+
+final class AndExpression extends BooleanExpression {
+
+ private final Expression lho;
+ private final Expression rho;
+
+ AndExpression(Expression lho, Expression 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 Expression deepCloneWithIdentifierReplaced_inner(
+ String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
+ return new AndExpression(
+ 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/ecb4e230/src/main/java/org/apache/freemarker/core/ast/ArithmeticEngine.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/ArithmeticEngine.java b/src/main/java/org/apache/freemarker/core/ast/ArithmeticEngine.java
new file mode 100644
index 0000000..c8d6f37
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/ArithmeticEngine.java
@@ -0,0 +1,550 @@
+/*
+ * 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;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.util.NumberUtil;
+import org.apache.freemarker.core.util.OptimizerUtil;
+import org.apache.freemarker.core.util.StringUtil;
+
+/**
+ * Class to perform arithmetic operations.
+ */
+
+public abstract class ArithmeticEngine {
+
+ /**
+ * Arithmetic engine that converts all numbers to {@link BigDecimal} and
+ * then operates on them. This is FreeMarker's default arithmetic engine.
+ */
+ public static final BigDecimalEngine BIGDECIMAL_ENGINE = new BigDecimalEngine();
+ /**
+ * Arithmetic engine that uses (more-or-less) the widening conversions of
+ * Java language to determine the type of result of operation, instead of
+ * converting everything to BigDecimal up front.
+ */
+ public static final ConservativeEngine CONSERVATIVE_ENGINE = new ConservativeEngine();
+
+ public abstract int compareNumbers(Number first, Number second) throws TemplateException;
+ public abstract Number add(Number first, Number second) throws TemplateException;
+ public abstract Number subtract(Number first, Number second) throws TemplateException;
+ public abstract Number multiply(Number first, Number second) throws TemplateException;
+ public abstract Number divide(Number first, Number second) throws TemplateException;
+ public abstract Number modulus(Number first, Number second) throws TemplateException;
+
+ /**
+ * Should be able to parse all FTL numerical literals, Java Double toString results, and XML Schema numbers.
+ * This means these should be parsed successfully, except if the arithmetical engine
+ * couldn't support the resulting value anyway (such as NaN, infinite, even non-integers):
+ * {@code -123.45}, {@code 1.5e3}, {@code 1.5E3}, {@code 0005}, {@code +0}, {@code -0}, {@code NaN},
+ * {@code INF}, {@code -INF}, {@code Infinity}, {@code -Infinity}.
+ */
+ public abstract Number toNumber(String s);
+
+ protected int minScale = 12;
+ protected int maxScale = 12;
+ protected int roundingPolicy = BigDecimal.ROUND_HALF_UP;
+
+ /**
+ * Sets the minimal scale to use when dividing BigDecimal numbers. Default
+ * value is 12.
+ */
+ public void setMinScale(int minScale) {
+ if (minScale < 0) {
+ throw new IllegalArgumentException("minScale < 0");
+ }
+ this.minScale = minScale;
+ }
+
+ /**
+ * Sets the maximal scale to use when multiplying BigDecimal numbers.
+ * Default value is 100.
+ */
+ public void setMaxScale(int maxScale) {
+ if (maxScale < minScale) {
+ throw new IllegalArgumentException("maxScale < minScale");
+ }
+ this.maxScale = maxScale;
+ }
+
+ public void setRoundingPolicy(int roundingPolicy) {
+ if (roundingPolicy != BigDecimal.ROUND_CEILING
+ && roundingPolicy != BigDecimal.ROUND_DOWN
+ && roundingPolicy != BigDecimal.ROUND_FLOOR
+ && roundingPolicy != BigDecimal.ROUND_HALF_DOWN
+ && roundingPolicy != BigDecimal.ROUND_HALF_EVEN
+ && roundingPolicy != BigDecimal.ROUND_HALF_UP
+ && roundingPolicy != BigDecimal.ROUND_UNNECESSARY
+ && roundingPolicy != BigDecimal.ROUND_UP) {
+ throw new IllegalArgumentException("invalid rounding policy");
+ }
+
+ this.roundingPolicy = roundingPolicy;
+ }
+
+ /**
+ * This is the default arithmetic engine in FreeMarker. It converts every
+ * number it receives into {@link BigDecimal}, then operates on these
+ * converted {@link BigDecimal}s.
+ */
+ public static class BigDecimalEngine
+ extends
+ ArithmeticEngine {
+ @Override
+ public int compareNumbers(Number first, Number second) {
+ // We try to find the result based on the sign (+/-/0) first, because:
+ // - It's much faster than converting to BigDecial, and comparing to 0 is the most common comparison.
+ // - It doesn't require any type conversions, and thus things like "Infinity > 0" won't fail.
+ int firstSignum = NumberUtil.getSignum(first);
+ int secondSignum = NumberUtil.getSignum(second);
+ if (firstSignum != secondSignum) {
+ return firstSignum < secondSignum ? -1 : (firstSignum > secondSignum ? 1 : 0);
+ } else if (firstSignum == 0 && secondSignum == 0) {
+ return 0;
+ } else {
+ BigDecimal left = toBigDecimal(first);
+ BigDecimal right = toBigDecimal(second);
+ return left.compareTo(right);
+ }
+ }
+
+ @Override
+ public Number add(Number first, Number second) {
+ BigDecimal left = toBigDecimal(first);
+ BigDecimal right = toBigDecimal(second);
+ return left.add(right);
+ }
+
+ @Override
+ public Number subtract(Number first, Number second) {
+ BigDecimal left = toBigDecimal(first);
+ BigDecimal right = toBigDecimal(second);
+ return left.subtract(right);
+ }
+
+ @Override
+ public Number multiply(Number first, Number second) {
+ BigDecimal left = toBigDecimal(first);
+ BigDecimal right = toBigDecimal(second);
+ BigDecimal result = left.multiply(right);
+ if (result.scale() > maxScale) {
+ result = result.setScale(maxScale, roundingPolicy);
+ }
+ return result;
+ }
+
+ @Override
+ public Number divide(Number first, Number second) {
+ BigDecimal left = toBigDecimal(first);
+ BigDecimal right = toBigDecimal(second);
+ return divide(left, right);
+ }
+
+ @Override
+ public Number modulus(Number first, Number second) {
+ long left = first.longValue();
+ long right = second.longValue();
+ return Long.valueOf(left % right);
+ }
+
+ @Override
+ public Number toNumber(String s) {
+ return toBigDecimalOrDouble(s);
+ }
+
+ private BigDecimal divide(BigDecimal left, BigDecimal right) {
+ int scale1 = left.scale();
+ int scale2 = right.scale();
+ int scale = Math.max(scale1, scale2);
+ scale = Math.max(minScale, scale);
+ return left.divide(right, scale, roundingPolicy);
+ }
+ }
+
+ /**
+ * An arithmetic engine that conservatively widens the operation arguments
+ * to extent that they can hold the result of the operation. Widening
+ * conversions occur in following situations:
+ * <ul>
+ * <li>byte and short are always widened to int (alike to Java language).</li>
+ * <li>To preserve magnitude: when operands are of different types, the
+ * result type is the type of the wider operand.</li>
+ * <li>to avoid overflows: if add, subtract, or multiply would overflow on
+ * integer types, the result is widened from int to long, or from long to
+ * BigInteger.</li>
+ * <li>to preserve fractional part: if a division of integer types would
+ * have a fractional part, int and long are converted to double, and
+ * BigInteger is converted to BigDecimal. An operation on a float and a
+ * long results in a double. An operation on a float or double and a
+ * BigInteger results in a BigDecimal.</li>
+ * </ul>
+ */
+ public static class ConservativeEngine extends ArithmeticEngine {
+ private static final int INTEGER = 0;
+ private static final int LONG = 1;
+ private static final int FLOAT = 2;
+ private static final int DOUBLE = 3;
+ private static final int BIGINTEGER = 4;
+ private static final int BIGDECIMAL = 5;
+
+ private static final Map classCodes = createClassCodesMap();
+
+ @Override
+ public int compareNumbers(Number first, Number second) throws TemplateException {
+ switch(getCommonClassCode(first, second)) {
+ case INTEGER: {
+ int n1 = first.intValue();
+ int n2 = second.intValue();
+ return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+ }
+ case LONG: {
+ long n1 = first.longValue();
+ long n2 = second.longValue();
+ return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+ }
+ case FLOAT: {
+ float n1 = first.floatValue();
+ float n2 = second.floatValue();
+ return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+ }
+ case DOUBLE: {
+ double n1 = first.doubleValue();
+ double n2 = second.doubleValue();
+ return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+ }
+ case BIGINTEGER: {
+ BigInteger n1 = toBigInteger(first);
+ BigInteger n2 = toBigInteger(second);
+ return n1.compareTo(n2);
+ }
+ case BIGDECIMAL: {
+ BigDecimal n1 = toBigDecimal(first);
+ BigDecimal n2 = toBigDecimal(second);
+ return n1.compareTo(n2);
+ }
+ }
+ // Make the compiler happy. getCommonClassCode() is guaranteed to
+ // return only above codes, or throw an exception.
+ throw new Error();
+ }
+
+ @Override
+ public Number add(Number first, Number second) throws TemplateException {
+ switch(getCommonClassCode(first, second)) {
+ case INTEGER: {
+ int n1 = first.intValue();
+ int n2 = second.intValue();
+ int n = n1 + n2;
+ return
+ ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check
+ ? Long.valueOf(((long) n1) + n2)
+ : Integer.valueOf(n);
+ }
+ case LONG: {
+ long n1 = first.longValue();
+ long n2 = second.longValue();
+ long n = n1 + n2;
+ return
+ ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check
+ ? toBigInteger(first).add(toBigInteger(second))
+ : Long.valueOf(n);
+ }
+ case FLOAT: {
+ return Float.valueOf(first.floatValue() + second.floatValue());
+ }
+ case DOUBLE: {
+ return Double.valueOf(first.doubleValue() + second.doubleValue());
+ }
+ case BIGINTEGER: {
+ BigInteger n1 = toBigInteger(first);
+ BigInteger n2 = toBigInteger(second);
+ return n1.add(n2);
+ }
+ case BIGDECIMAL: {
+ BigDecimal n1 = toBigDecimal(first);
+ BigDecimal n2 = toBigDecimal(second);
+ return n1.add(n2);
+ }
+ }
+ // Make the compiler happy. getCommonClassCode() is guaranteed to
+ // return only above codes, or throw an exception.
+ throw new Error();
+ }
+
+ @Override
+ public Number subtract(Number first, Number second) throws TemplateException {
+ switch(getCommonClassCode(first, second)) {
+ case INTEGER: {
+ int n1 = first.intValue();
+ int n2 = second.intValue();
+ int n = n1 - n2;
+ return
+ ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check
+ ? (Number) Long.valueOf(((long) n1) - n2)
+ : (Number) Integer.valueOf(n);
+ }
+ case LONG: {
+ long n1 = first.longValue();
+ long n2 = second.longValue();
+ long n = n1 - n2;
+ return
+ ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check
+ ? (Number) toBigInteger(first).subtract(toBigInteger(second))
+ : (Number) Long.valueOf(n);
+ }
+ case FLOAT: {
+ return Float.valueOf(first.floatValue() - second.floatValue());
+ }
+ case DOUBLE: {
+ return Double.valueOf(first.doubleValue() - second.doubleValue());
+ }
+ case BIGINTEGER: {
+ BigInteger n1 = toBigInteger(first);
+ BigInteger n2 = toBigInteger(second);
+ return n1.subtract(n2);
+ }
+ case BIGDECIMAL: {
+ BigDecimal n1 = toBigDecimal(first);
+ BigDecimal n2 = toBigDecimal(second);
+ return n1.subtract(n2);
+ }
+ }
+ // Make the compiler happy. getCommonClassCode() is guaranteed to
+ // return only above codes, or throw an exception.
+ throw new Error();
+ }
+
+ @Override
+ public Number multiply(Number first, Number second) throws TemplateException {
+ switch(getCommonClassCode(first, second)) {
+ case INTEGER: {
+ int n1 = first.intValue();
+ int n2 = second.intValue();
+ int n = n1 * n2;
+ return
+ n1 == 0 || n / n1 == n2 // overflow check
+ ? (Number) Integer.valueOf(n)
+ : (Number) Long.valueOf(((long) n1) * n2);
+ }
+ case LONG: {
+ long n1 = first.longValue();
+ long n2 = second.longValue();
+ long n = n1 * n2;
+ return
+ n1 == 0L || n / n1 == n2 // overflow check
+ ? (Number) Long.valueOf(n)
+ : (Number) toBigInteger(first).multiply(toBigInteger(second));
+ }
+ case FLOAT: {
+ return Float.valueOf(first.floatValue() * second.floatValue());
+ }
+ case DOUBLE: {
+ return Double.valueOf(first.doubleValue() * second.doubleValue());
+ }
+ case BIGINTEGER: {
+ BigInteger n1 = toBigInteger(first);
+ BigInteger n2 = toBigInteger(second);
+ return n1.multiply(n2);
+ }
+ case BIGDECIMAL: {
+ BigDecimal n1 = toBigDecimal(first);
+ BigDecimal n2 = toBigDecimal(second);
+ BigDecimal r = n1.multiply(n2);
+ return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r;
+ }
+ }
+ // Make the compiler happy. getCommonClassCode() is guaranteed to
+ // return only above codes, or throw an exception.
+ throw new Error();
+ }
+
+ @Override
+ public Number divide(Number first, Number second) throws TemplateException {
+ switch(getCommonClassCode(first, second)) {
+ case INTEGER: {
+ int n1 = first.intValue();
+ int n2 = second.intValue();
+ if (n1 % n2 == 0) {
+ return Integer.valueOf(n1 / n2);
+ }
+ return Double.valueOf(((double) n1) / n2);
+ }
+ case LONG: {
+ long n1 = first.longValue();
+ long n2 = second.longValue();
+ if (n1 % n2 == 0) {
+ return Long.valueOf(n1 / n2);
+ }
+ return Double.valueOf(((double) n1) / n2);
+ }
+ case FLOAT: {
+ return Float.valueOf(first.floatValue() / second.floatValue());
+ }
+ case DOUBLE: {
+ return Double.valueOf(first.doubleValue() / second.doubleValue());
+ }
+ case BIGINTEGER: {
+ BigInteger n1 = toBigInteger(first);
+ BigInteger n2 = toBigInteger(second);
+ BigInteger[] divmod = n1.divideAndRemainder(n2);
+ if (divmod[1].equals(BigInteger.ZERO)) {
+ return divmod[0];
+ } else {
+ BigDecimal bd1 = new BigDecimal(n1);
+ BigDecimal bd2 = new BigDecimal(n2);
+ return bd1.divide(bd2, minScale, roundingPolicy);
+ }
+ }
+ case BIGDECIMAL: {
+ BigDecimal n1 = toBigDecimal(first);
+ BigDecimal n2 = toBigDecimal(second);
+ int scale1 = n1.scale();
+ int scale2 = n2.scale();
+ int scale = Math.max(scale1, scale2);
+ scale = Math.max(minScale, scale);
+ return n1.divide(n2, scale, roundingPolicy);
+ }
+ }
+ // Make the compiler happy. getCommonClassCode() is guaranteed to
+ // return only above codes, or throw an exception.
+ throw new Error();
+ }
+
+ @Override
+ public Number modulus(Number first, Number second) throws TemplateException {
+ switch(getCommonClassCode(first, second)) {
+ case INTEGER: {
+ return Integer.valueOf(first.intValue() % second.intValue());
+ }
+ case LONG: {
+ return Long.valueOf(first.longValue() % second.longValue());
+ }
+ case FLOAT: {
+ return Float.valueOf(first.floatValue() % second.floatValue());
+ }
+ case DOUBLE: {
+ return Double.valueOf(first.doubleValue() % second.doubleValue());
+ }
+ case BIGINTEGER: {
+ BigInteger n1 = toBigInteger(first);
+ BigInteger n2 = toBigInteger(second);
+ return n1.mod(n2);
+ }
+ case BIGDECIMAL: {
+ throw new _MiscTemplateException("Can't calculate remainder on BigDecimals");
+ }
+ }
+ // Make the compiler happy. getCommonClassCode() is guaranteed to
+ // return only above codes, or throw an exception.
+ throw new BugException();
+ }
+
+ @Override
+ public Number toNumber(String s) {
+ Number n = toBigDecimalOrDouble(s);
+ return n instanceof BigDecimal ? OptimizerUtil.optimizeNumberRepresentation(n) : n;
+ }
+
+ private static Map createClassCodesMap() {
+ Map map = new HashMap(17);
+ Integer intcode = Integer.valueOf(INTEGER);
+ map.put(Byte.class, intcode);
+ map.put(Short.class, intcode);
+ map.put(Integer.class, intcode);
+ map.put(Long.class, Integer.valueOf(LONG));
+ map.put(Float.class, Integer.valueOf(FLOAT));
+ map.put(Double.class, Integer.valueOf(DOUBLE));
+ map.put(BigInteger.class, Integer.valueOf(BIGINTEGER));
+ map.put(BigDecimal.class, Integer.valueOf(BIGDECIMAL));
+ return map;
+ }
+
+ private static int getClassCode(Number num) throws TemplateException {
+ try {
+ return ((Integer) classCodes.get(num.getClass())).intValue();
+ } catch (NullPointerException e) {
+ if (num == null) {
+ throw new _MiscTemplateException("The Number object was null.");
+ } else {
+ throw new _MiscTemplateException("Unknown number type ", num.getClass().getName());
+ }
+ }
+ }
+
+ private static int getCommonClassCode(Number num1, Number num2) throws TemplateException {
+ int c1 = getClassCode(num1);
+ int c2 = getClassCode(num2);
+ int c = c1 > c2 ? c1 : c2;
+ // If BigInteger is combined with a Float or Double, the result is a
+ // BigDecimal instead of BigInteger in order not to lose the
+ // fractional parts. If Float is combined with Long, the result is a
+ // Double instead of Float to preserve the bigger bit width.
+ switch(c) {
+ case FLOAT: {
+ if ((c1 < c2 ? c1 : c2) == LONG) {
+ return DOUBLE;
+ }
+ break;
+ }
+ case BIGINTEGER: {
+ int min = c1 < c2 ? c1 : c2;
+ if (min == DOUBLE || min == FLOAT) {
+ return BIGDECIMAL;
+ }
+ break;
+ }
+ }
+ return c;
+ }
+
+ private static BigInteger toBigInteger(Number num) {
+ return num instanceof BigInteger ? (BigInteger) num : new BigInteger(num.toString());
+ }
+ }
+
+ private static BigDecimal toBigDecimal(Number num) {
+ try {
+ return num instanceof BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString());
+ } catch (NumberFormatException e) {
+ // The exception message is useless, so we add a new one:
+ throw new NumberFormatException("Can't parse this as BigDecimal number: " + StringUtil.jQuote(num));
+ }
+ }
+
+ private static Number toBigDecimalOrDouble(String s) {
+ if (s.length() > 2) {
+ char c = s.charAt(0);
+ if (c == 'I' && (s.equals("INF") || s.equals("Infinity"))) {
+ return Double.valueOf(Double.POSITIVE_INFINITY);
+ } else if (c == 'N' && s.equals("NaN")) {
+ return Double.valueOf(Double.NaN);
+ } else if (c == '-' && s.charAt(1) == 'I' && (s.equals("-INF") || s.equals("-Infinity"))) {
+ return Double.valueOf(Double.NEGATIVE_INFINITY);
+ }
+ }
+ return new BigDecimal(s);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ecb4e230/src/main/java/org/apache/freemarker/core/ast/ArithmeticExpression.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/ArithmeticExpression.java b/src/main/java/org/apache/freemarker/core/ast/ArithmeticExpression.java
new file mode 100644
index 0000000..3b75418
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ast/ArithmeticExpression.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.ast;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * An operator for arithmetic operations. Note that the + operator is in {@link AddConcatExpression}, because its
+ * overloaded (does string concatenation and more).
+ */
+final class ArithmeticExpression extends Expression {
+
+ static final int TYPE_SUBSTRACTION = 0;
+ static final int TYPE_MULTIPLICATION = 1;
+ static final int TYPE_DIVISION = 2;
+ static final int TYPE_MODULO = 3;
+
+ private static final char[] OPERATOR_IMAGES = new char[] { '-', '*', '/', '%' };
+
+ private final Expression lho;
+ private final Expression rho;
+ private final int operator;
+
+ ArithmeticExpression(Expression lho, Expression rho, int operator) {
+ this.lho = lho;
+ this.rho = rho;
+ this.operator = operator;
+ }
+
+ @Override
+ TemplateModel _eval(Environment env) throws TemplateException {
+ return _eval(env, this, lho.evalToNumber(env), operator, rho.evalToNumber(env));
+ }
+
+ static TemplateModel _eval(Environment env, TemplateObject parent, Number lhoNumber, int operator, Number rhoNumber)
+ throws TemplateException, _MiscTemplateException {
+ ArithmeticEngine ae = EvalUtil.getArithmeticEngine(env, parent);
+ switch (operator) {
+ case TYPE_SUBSTRACTION :
+ return new SimpleNumber(ae.subtract(lhoNumber, rhoNumber));
+ case TYPE_MULTIPLICATION :
+ return new SimpleNumber(ae.multiply(lhoNumber, rhoNumber));
+ case TYPE_DIVISION :
+ return new SimpleNumber(ae.divide(lhoNumber, rhoNumber));
+ case TYPE_MODULO :
+ return new SimpleNumber(ae.modulus(lhoNumber, rhoNumber));
+ default:
+ if (parent instanceof Expression) {
+ throw new _MiscTemplateException((Expression) parent,
+ "Unknown operation: ", Integer.valueOf(operator));
+ } else {
+ throw new _MiscTemplateException("Unknown operation: ", Integer.valueOf(operator));
+ }
+ }
+ }
+
+ @Override
+ public String getCanonicalForm() {
+ return lho.getCanonicalForm() + ' ' + getOperatorSymbol(operator) + ' ' + rho.getCanonicalForm();
+ }
+
+ @Override
+ String getNodeTypeSymbol() {
+ return String.valueOf(getOperatorSymbol(operator));
+ }
+
+ static char getOperatorSymbol(int operator) {
+ return OPERATOR_IMAGES[operator];
+ }
+
+ @Override
+ boolean isLiteral() {
+ return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+ }
+
+ @Override
+ protected Expression deepCloneWithIdentifierReplaced_inner(
+ String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
+ return new ArithmeticExpression(
+ lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+ rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+ operator);
+ }
+
+ @Override
+ int getParameterCount() {
+ return 3;
+ }
+
+ @Override
+ Object getParameterValue(int idx) {
+ switch (idx) {
+ case 0: return lho;
+ case 1: return rho;
+ case 2: return Integer.valueOf(operator);
+ 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;
+ case 2: return ParameterRole.AST_NODE_SUBTYPE;
+ default: throw new IndexOutOfBoundsException();
+ }
+ }
+
+}