freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [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...
Date Thu, 16 Feb 2017 23:08:28 GMT
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();
+        }
+    }
+    
+}


Mime
View raw message