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(); + } + } + +}
