Merge branch '2.3-gae' into 2.3-gae-tools
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 3a05af0..77126cc 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -197,7 +197,7 @@
org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
-org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.indentation.size=8
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
diff --git a/ivy.xml b/ivy.xml
index dbea293..994146d 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -125,7 +125,7 @@
<!-- test -->
<!-- Note: Ant doesn't contain junit.jar anymore, so we add it to conf "test" too. -->
- <dependency org="junit" name="junit" rev="3.7" conf="build.test->default; test->default" />
+ <dependency org="junit" name="junit" rev="4.11" conf="build.test->default; test->default" />
<!-- docs -->
diff --git a/src/main/java/freemarker/ext/beans/ArgumentTypes.java b/src/main/java/freemarker/ext/beans/ArgumentTypes.java
new file mode 100644
index 0000000..b13f842
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ArgumentTypes.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (c) 2003 The Visigoth Software Society. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ * any, must include the following acknowledgement:
+ * "This product includes software developed by the
+ * Visigoth Software Society (http://www.visigoths.org/)."
+ * Alternately, this acknowledgement may appear in the software itself,
+ * if and wherever such third-party acknowledgements normally appear.
+ *
+ * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
+ * project contributors may be used to endorse or promote products derived
+ * from this software without prior written permission. For written
+ * permission, please contact visigoths@visigoths.org.
+ *
+ * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
+ * nor may "FreeMarker" or "Visigoth" appear in their names
+ * without prior written permission of the Visigoth Software Society.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Visigoth Software Society. For more
+ * information on the Visigoth Software Society, please see
+ * http://www.visigoths.org/
+ */
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import freemarker.template.Version;
+import freemarker.template.utility.ClassUtil;
+
+
+/**
+ * The argument types of a method call; usable as cache key.
+ *
+ * @author Attila Szegedi
+ */
+final class ArgumentTypes {
+
+ private static final int BIG_MANTISSA_LOSS_PRICE = 40000;
+
+ /**
+ * The types of the arguments; for varags this contains the exploded list (not the array).
+ */
+ private final Class[] types;
+
+ private final boolean bugfixed;
+
+ /**
+ * @param args The actual arguments. A varargs argument should be present exploded, no as an array.
+ * @param bugfixed Introduced in 2.3.21, sets this object to a mode that works well with {@link BeansWrapper}-s
+ * created with {@link Version} 2.3.21 or higher.
+ */
+ ArgumentTypes(Object[] args, boolean bugfixed) {
+ int l = args.length;
+ types = new Class[l];
+ for(int i = 0; i < l; ++i) {
+ Object obj = args[i];
+ types[i] = obj == null
+ ? (bugfixed ? Null.class : Object.class)
+ : obj.getClass();
+ }
+ this.bugfixed = bugfixed;
+ }
+
+ public int hashCode() {
+ int hash = 0;
+ for(int i = 0; i < types.length; ++i) {
+ hash ^= types[i].hashCode();
+ }
+ return hash;
+ }
+
+ public boolean equals(Object o) {
+ if(o instanceof ArgumentTypes) {
+ ArgumentTypes cs = (ArgumentTypes)o;
+ if(cs.types.length != types.length) {
+ return false;
+ }
+ for(int i = 0; i < types.length; ++i) {
+ if(cs.types[i] != types[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or
+ * {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}.
+ */
+ MaybeEmptyCallableMemberDescriptor getMostSpecific(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg)
+ {
+ LinkedList/*<CallableMemberDescriptor>*/ applicables = getApplicables(memberDescs, varArg);
+ if(applicables.isEmpty()) {
+ return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
+ }
+ if(applicables.size() == 1) {
+ return (CallableMemberDescriptor) applicables.getFirst();
+ }
+
+ LinkedList/*<CallableMemberDescriptor>*/ maximals = new LinkedList();
+ for (Iterator applicablesIter = applicables.iterator(); applicablesIter.hasNext();)
+ {
+ CallableMemberDescriptor applicable = (CallableMemberDescriptor) applicablesIter.next();
+ boolean lessSpecific = false;
+ for (Iterator maximalsIter = maximals.iterator();
+ maximalsIter.hasNext();)
+ {
+ CallableMemberDescriptor maximal = (CallableMemberDescriptor) maximalsIter.next();
+ final int cmpRes = compareParameterListPreferability(applicable.paramTypes, maximal.paramTypes, varArg);
+ if (cmpRes > 0) {
+ maximalsIter.remove();
+ } else if (cmpRes < 0) {
+ lessSpecific = true;
+ }
+ }
+ if(!lessSpecific) {
+ maximals.addLast(applicable);
+ }
+ }
+ if(maximals.size() > 1) {
+ return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
+ }
+ return (CallableMemberDescriptor) maximals.getFirst();
+ }
+
+ /**
+ * Tells if among the parameter list of two methods, which one fits this argument list better.
+ * This method assumes that the parameter lists are applicable to this argument lists; if that's not ensured,
+ * what the result will be is undefined.
+ *
+ * <p>This method behaves differently in {@code bugfixed}-mode (used when a {@link BeansWrapper} is created with
+ * incompatible improvements set to 2.3.21 or higher). Below we describe the bugfixed behavior only.
+ *
+ * <p>The decision is made by comparing the preferability of each parameter types of the same position in a loop.
+ * At the end, the parameter list with the more preferred parameters will be the preferred one. If both parameter
+ * lists has the same amount of preferred parameters, the one that has the first (lower index) preferred parameter
+ * is the preferred one. Otherwise the two parameter list are considered to be equal in terms of preferability.
+ *
+ * <p>If there's no numerical conversion involved, the preferability of two parameter types is decided on how
+ * specific their types are. For example, {@code String} is more specific than {@link Object} (because
+ * {@code Object.class.isAssignableFrom(String.class)}-s), and so {@code String} is preferred. Primitive
+ * types are considered to be more specific than the corresponding boxing class (like {@code boolean} is more
+ * specific than {@code Boolean}, because the former can't store {@code null}). The preferability decision gets
+ * trickier when there's a possibility of numerical conversion from the actual argument type to the type of some of
+ * the parameters. If such conversion is only possible for one of the competing parameter types, that parameter
+ * automatically wins. If it's possible for both, {@link OverloadedNumberUtil#getArgumentConversionPrice} will
+ * be used to calculate the conversion "price", and the parameter type with lowest price wins.
+ *
+ * @param paramTypes1 The parameter types of one of the competing methods
+ * @param paramTypes2 The parameter types of the other competing method
+ * @param varArg Whether these competing methods are varargs methods.
+ * @return More than 0 if the first parameter list is preferred, less then 0 if the other is preferred,
+ * 0 if there's no decision
+ */
+ int compareParameterListPreferability(Class[] paramTypes1, Class[] paramTypes2, boolean varArg) {
+ final int argTypesLen = types.length;
+ final int paramTypes1Len = paramTypes1.length;
+ final int paramTypes2Len = paramTypes2.length;
+ //assert varArg || paramTypes1Len == paramTypes2Len;
+
+ if (bugfixed) {
+ int paramList1WeakWinCnt = 0;
+ int paramList2WeakWinCnt = 0;
+ int paramList1WinCnt = 0;
+ int paramList2WinCnt = 0;
+ int paramList1StrongWinCnt = 0;
+ int paramList2StrongWinCnt = 0;
+ int firstWinerParamList = 0;
+ for (int i = 0; i < argTypesLen; i++) {
+ final Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
+ final Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
+
+ final int winerParam; // 1 => paramType1; -1 => paramType2; 0 => draw
+ if (paramType1 == paramType2) {
+ winerParam = 0;
+ } else {
+ final Class argType = types[i];
+ final boolean argIsNum = Number.class.isAssignableFrom(argType);
+
+ final int numConvPrice1;
+ if (argIsNum && ClassUtil.isNumerical(paramType1)) {
+ final Class nonPrimParamType1 = paramType1.isPrimitive()
+ ? ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1;
+ numConvPrice1 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType1);
+ } else {
+ numConvPrice1 = Integer.MAX_VALUE;
+ }
+ // numConvPrice1 is Integer.MAX_VALUE if either:
+ // - argType and paramType1 aren't both numerical
+ // - FM doesn't know some of the numerical types, or the conversion between them is not allowed
+
+ final int numConvPrice2;
+ if (argIsNum && ClassUtil.isNumerical(paramType2)) {
+ final Class nonPrimParamType2 = paramType2.isPrimitive()
+ ? ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+ numConvPrice2 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType2);
+ } else {
+ numConvPrice2 = Integer.MAX_VALUE;
+ }
+
+ if (numConvPrice1 == Integer.MAX_VALUE) {
+ if (numConvPrice2 == Integer.MAX_VALUE) { // No numerical conversions anywhere
+ final int r = compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
+ if (r > 0) {
+ winerParam = 1;
+ if (r > 1) {
+ paramList1WinCnt++;
+ } else {
+ paramList1WeakWinCnt++;
+ }
+ } else if (r < 0) {
+ winerParam = -1;
+ if (r < -1) {
+ paramList2WinCnt++;
+ } else {
+ paramList2WeakWinCnt++;
+ }
+ } else {
+ winerParam = 0;
+ }
+ } else { // No num. conv. of param1, num. conv. of param2
+ winerParam = -1;
+ paramList2WinCnt++;
+ }
+ } else if (numConvPrice2 == Integer.MAX_VALUE) { // Num. conv. of param1, not of param2
+ winerParam = 1;
+ paramList1WinCnt++;
+ } else { // Num. conv. of both param1 and param2
+ if (numConvPrice1 != numConvPrice2) {
+ if (numConvPrice1 < numConvPrice2) {
+ winerParam = 1;
+ if (numConvPrice1 < BIG_MANTISSA_LOSS_PRICE && numConvPrice2 > BIG_MANTISSA_LOSS_PRICE) {
+ paramList1StrongWinCnt++;
+ } else {
+ paramList1WinCnt++;
+ }
+ } else {
+ winerParam = -1;
+ if (numConvPrice2 < BIG_MANTISSA_LOSS_PRICE && numConvPrice1 > BIG_MANTISSA_LOSS_PRICE) {
+ paramList2StrongWinCnt++;
+ } else {
+ paramList2WinCnt++;
+ }
+ }
+ } else {
+ winerParam = (paramType1.isPrimitive() ? 1 : 0) - (paramType2.isPrimitive() ? 1 : 0);
+ if (winerParam == 1) paramList1WeakWinCnt++;
+ else if (winerParam == -1) paramList2WeakWinCnt++;
+ }
+ }
+ } // when paramType1 != paramType2
+
+ if (firstWinerParamList == 0 && winerParam != 0) {
+ firstWinerParamList = winerParam;
+ }
+ } // for each parameter types
+
+ if (paramList1StrongWinCnt != paramList2StrongWinCnt) {
+ return paramList1StrongWinCnt - paramList2StrongWinCnt;
+ } else if (paramList1WinCnt != paramList2WinCnt) {
+ return paramList1WinCnt - paramList2WinCnt;
+ } else if (paramList1WeakWinCnt != paramList2WeakWinCnt) {
+ return paramList1WeakWinCnt - paramList2WeakWinCnt;
+ } else if (firstWinerParamList != 0) { // paramList1WinCnt == paramList2WinCnt
+ return firstWinerParamList;
+ } else { // still undecided
+ if (varArg) {
+ if (paramTypes1Len == paramTypes2Len) {
+ // If we had a 0-length varargs array in both methods, we also compare the types at the
+ // index of the varargs parameter, like if we had a single varargs argument. However, this
+ // time we don't have an argument type, so we can only decide based on type specificity:
+ if (argTypesLen == paramTypes1Len - 1) {
+ Class paramType1 = getParamType(paramTypes1, paramTypes1Len, argTypesLen, true);
+ Class paramType2 = getParamType(paramTypes2, paramTypes2Len, argTypesLen, true);
+ if (ClassUtil.isNumerical(paramType1) && ClassUtil.isNumerical(paramType2)) {
+ int r = OverloadedNumberUtil.compareNumberTypeSpecificity(paramType1, paramType2);
+ if (r != 0) return r;
+ // falls through
+ }
+ return compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
+ } else {
+ return 0;
+ }
+ } else {
+ // The method with more fixed parameters wins:
+ return paramTypes1Len - paramTypes2Len;
+ }
+ } else {
+ return 0;
+ }
+ }
+ } else { // non-bugfixed (backward-compatible) mode
+ boolean paramTypes1HasAMoreSpecific = false;
+ boolean paramTypes2HasAMoreSpecific = false;
+ for(int i = 0; i < paramTypes1Len; ++i) {
+ Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
+ Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
+ if(paramType1 != paramType2) {
+ paramTypes1HasAMoreSpecific =
+ paramTypes1HasAMoreSpecific
+ || MethodUtilities.isMoreOrSameSpecificParameterType(paramType1, paramType2, false, 0) != 0;
+ paramTypes2HasAMoreSpecific =
+ paramTypes2HasAMoreSpecific
+ || MethodUtilities.isMoreOrSameSpecificParameterType(paramType2, paramType1, false, 0) != 0;
+ }
+ }
+
+ if(paramTypes1HasAMoreSpecific) {
+ return paramTypes2HasAMoreSpecific ? 0 : 1;
+ } else if(paramTypes2HasAMoreSpecific) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ /**
+ * Trivial comparison type specificities; unaware of numerical conversions.
+ *
+ * @return Less-than-0, 0, or more-than-0 depending on which side is more specific. The absolute value is 1 if
+ * the difference is only in primitive VS non-primitive, more otherwise.
+ */
+ private int compareParameterListPreferability_cmpTypeSpecificty(final Class paramType1, final Class paramType2) {
+ // The more specific (smaller) type wins.
+
+ final Class nonPrimParamType1 = paramType1.isPrimitive()
+ ? ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1;
+ final Class nonPrimParamType2 = paramType2.isPrimitive()
+ ? ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+
+ if (nonPrimParamType1 == nonPrimParamType2) {
+ if (nonPrimParamType1 != paramType1) {
+ if (nonPrimParamType2 != paramType2) {
+ return 0; // identical prim. types; shouldn't ever be reached
+ } else {
+ return 1; // param1 is prim., param2 is non prim.
+ }
+ } else if (nonPrimParamType2 != paramType2) {
+ return -1; // param1 is non-prim., param2 is prim.
+ } else {
+ return 0; // identical non-prim. types
+ }
+ } else if (nonPrimParamType2.isAssignableFrom(nonPrimParamType1)) {
+ return 2;
+ } else if (nonPrimParamType1.isAssignableFrom(nonPrimParamType2)) {
+ return -2;
+ } else {
+ return 0; // unrelated types
+ }
+ }
+
+ private static Class getParamType(Class[] paramTypes, int paramTypesLen, int i, boolean varArg) {
+ return varArg && i >= paramTypesLen - 1
+ ? paramTypes[paramTypesLen - 1].getComponentType()
+ : paramTypes[i];
+ }
+
+ /**
+ * Returns all methods that are applicable to actual
+ * parameter types represented by this ArgumentTypes object.
+ */
+ LinkedList/*<CallableMemberDescriptor>*/ getApplicables(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg) {
+ LinkedList/*<CallableMemberDescriptor>*/ list = new LinkedList();
+ for (Iterator it = memberDescs.iterator(); it.hasNext();) {
+ CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) it.next();
+ if(isApplicable(memberDesc, varArg)) {
+ list.add(memberDesc);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Returns true if the supplied method is applicable to actual
+ * parameter types represented by this ArgumentTypes object.
+ */
+ private boolean isApplicable(CallableMemberDescriptor memberDesc, boolean varArg) {
+ final Class[] paramTypes = memberDesc.paramTypes;
+ final int cl = types.length;
+ final int fl = paramTypes.length - (varArg ? 1 : 0);
+ if(varArg) {
+ if(cl < fl) {
+ return false;
+ }
+ } else {
+ if(cl != fl) {
+ return false;
+ }
+ }
+ for(int i = 0; i < fl; ++i) {
+ if(!isMethodInvocationConvertible(paramTypes[i], types[i])) {
+ return false;
+ }
+ }
+ if(varArg) {
+ Class varArgParamType = paramTypes[fl].getComponentType();
+ for(int i = fl; i < cl; ++i) {
+ if(!isMethodInvocationConvertible(varArgParamType, types[i])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determines whether a type represented by a class object is
+ * convertible to another type represented by a class object using a
+ * method invocation conversion, treating object types of primitive
+ * types as if they were primitive types (that is, a Boolean actual
+ * parameter type matches boolean primitive formal type). This behavior
+ * is because this method is used to determine applicable methods for
+ * an actual parameter list, and primitive types are represented by
+ * their object duals in reflective method calls.
+ * @param formal the parameter type to which the actual
+ * parameter type should be convertible; possibly a primitive type
+ * @param actual the argument type; not a primitive type, maybe {@link Null}.
+ * @return true if either formal type is assignable from actual type,
+ * or the formal type is a primitive type and the actual type is its corresponding object
+ * type or an object type of a primitive type that can be converted to
+ * the formal type.
+ */
+ private boolean isMethodInvocationConvertible(final Class formal, final Class actual) {
+ // Check for identity or widening reference conversion
+ if(formal.isAssignableFrom(actual)) {
+ return true;
+ } else if (actual == Null.class && bugfixed) {
+ return !formal.isPrimitive();
+ } else if (bugfixed) {
+ final Class formalNP;
+ if (formal.isPrimitive()) {
+ formalNP = ClassUtil.primitiveClassToBoxingClass(formal);
+ if (actual == formalNP) {
+ // Character and char, etc.
+ return true;
+ }
+ } else {
+ formalNP = formal;
+ }
+ if (Number.class.isAssignableFrom(actual) && Number.class.isAssignableFrom(formalNP)) {
+ return OverloadedNumberUtil.getArgumentConversionPrice(actual, formalNP) != Integer.MAX_VALUE;
+ } else {
+ return false;
+ }
+ } else { // if !bugfixed
+ // This non-bugfixed (backward-compatibile) branch:
+ // - Doesn't convert *to* non-primitive numerical types (unless the argument is a BigDecimal).
+ // (This is like in Java language, which also doesn't coerce to non-primitive numerical types.)
+ // - Doesn't support BigInteger conversions
+ // - Doesn't support NumberWithFallbackType-s. Those are only produced in bugfixed mode anyway.
+ if(formal.isPrimitive()) {
+ // Check for boxing with widening primitive conversion. Note that
+ // actual parameters are never primitives.
+ // It doesn't do the same with boxing types... that was a bug.
+ if(formal == Boolean.TYPE) {
+ return actual == Boolean.class;
+ } else if (formal == Double.TYPE &&
+ (actual == Double.class || actual == Float.class ||
+ actual == Long.class || actual == Integer.class ||
+ actual == Short.class || actual == Byte.class)) {
+ return true;
+ } else if (formal == Integer.TYPE &&
+ (actual == Integer.class || actual == Short.class ||
+ actual == Byte.class)) {
+ return true;
+ } else if (formal == Long.TYPE &&
+ (actual == Long.class || actual == Integer.class ||
+ actual == Short.class || actual == Byte.class)) {
+ return true;
+ } else if (formal == Float.TYPE &&
+ (actual == Float.class || actual == Long.class ||
+ actual == Integer.class || actual == Short.class ||
+ actual == Byte.class)) {
+ return true;
+ } else if (formal == Character.TYPE) {
+ return actual == Character.class;
+ } else if(formal == Byte.TYPE && actual == Byte.class) {
+ return true;
+ } else if(formal == Short.TYPE &&
+ (actual == Short.class || actual == Byte.class)) {
+ return true;
+ } else if (BigDecimal.class.isAssignableFrom(actual) && ClassUtil.isNumerical(formal)) {
+ // Special case for BigDecimals as we deem BigDecimal to be
+ // convertible to any numeric type - either object or primitive.
+ // This can actually cause us trouble as this is a narrowing
+ // conversion, not widening.
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Symbolizes the class of null (it's missing from Java).
+ */
+ private static class Null {
+
+ // Can't be instantiated
+ private Null() { }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index 4463756..a9e7a90 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -514,7 +514,11 @@
*/
public Version getIncompatibleImprovements() {
return incompatibleImprovements;
- }
+ }
+
+ boolean is2321Bugfixed() {
+ return getIncompatibleImprovements().intValue() >= 2003021;
+ }
/**
* Returns the default instance of the wrapper. This instance is used
@@ -652,29 +656,52 @@
public Object unwrap(TemplateModel model, Class hint)
throws TemplateModelException
{
- final Object obj = unwrapInternal(model, hint);
+ final Object obj = tryUnwrap(model, hint);
if(obj == CAN_NOT_UNWRAP) {
throw new TemplateModelException("Can not unwrap model of type " +
model.getClass().getName() + " to type " + hint.getName());
}
return obj;
}
+
+ /**
+ * Same as {@link #tryUnwrap(TemplateModel, Class, int)} with 0 last argument.
+ */
+ Object tryUnwrap(TemplateModel model, Class hint) throws TemplateModelException
+ {
+ return tryUnwrap(model, hint, 0);
+ }
- Object unwrapInternal(TemplateModel model, Class hint)
+ /**
+ * @param targetNumTypes Used when unwrapping for overloaded methods and so the hint is too generic; 0 otherwise.
+ * This will be ignored if the hint is already a concrete numerical type. (With overloaded methods the hint
+ * is often {@link Number} or {@link Object}, because the unwrapping has to happen before choosing the
+ * concrete overloaded method.)
+ * @return {@link #CAN_NOT_UNWRAP} or the unwrapped object.
+ */
+ Object tryUnwrap(TemplateModel model, Class hint, int targetNumTypes)
throws TemplateModelException
{
- return unwrap(model, hint, null);
+ Object res = tryUnwrap(model, hint, null);
+ if (targetNumTypes != 0
+ && (targetNumTypes & OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT) != 0
+ && res instanceof Number) {
+ return OverloadedNumberUtil.addFallbackType((Number) res, targetNumTypes);
+ } else {
+ return res;
+ }
}
- private Object unwrap(TemplateModel model, Class hint, Map recursionStops)
+ private Object tryUnwrap(TemplateModel model, Class hint, Map recursionStops)
throws TemplateModelException
{
if(model == null || model == nullModel) {
return null;
}
- boolean isBoolean = Boolean.TYPE == hint;
- boolean isChar = Character.TYPE == hint;
+ if (is2321Bugfixed() && hint.isPrimitive()) {
+ hint = ClassUtil.primitiveClassToBoxingClass(hint);
+ }
// This is for transparent interop with other wrappers (and ourselves)
// Passing the hint allows i.e. a Jython-aware method that declares a
@@ -687,13 +714,9 @@
return adapted;
}
// Attempt numeric conversion
- if(adapted instanceof Number && ((hint.isPrimitive() && !isChar &&
- !isBoolean) || NUMBER_CLASS.isAssignableFrom(hint))) {
- Number number = convertUnwrappedNumber(hint,
- (Number)adapted);
- if(number != null) {
- return number;
- }
+ if(adapted instanceof Number && ClassUtil.isNumerical(hint)) {
+ Number number = forceUnwrappedNumberToType((Number) adapted, hint, is2321Bugfixed());
+ if(number != null) return number;
}
}
@@ -703,10 +726,8 @@
return wrapped;
}
// Attempt numeric conversion
- if(wrapped instanceof Number && ((hint.isPrimitive() && !isChar &&
- !isBoolean) || NUMBER_CLASS.isAssignableFrom(hint))) {
- Number number = convertUnwrappedNumber(hint,
- (Number)wrapped);
+ if(wrapped instanceof Number && ClassUtil.isNumerical(hint)) {
+ Number number = forceUnwrappedNumberToType((Number) wrapped, hint, is2321Bugfixed());
if(number != null) {
return number;
}
@@ -718,6 +739,7 @@
// select the appropriate interface in multi-interface models when we
// know what is expected as the return type.
+ // Java 5: Also should check for CharSequence at the end
if(STRING_CLASS == hint) {
if(model instanceof TemplateScalarModel) {
return ((TemplateScalarModel)model).getAsString();
@@ -727,18 +749,17 @@
}
// Primitive numeric types & Number.class and its subclasses
- if((hint.isPrimitive() && !isChar && !isBoolean)
- || NUMBER_CLASS.isAssignableFrom(hint)) {
+ if(ClassUtil.isNumerical(hint)) {
if(model instanceof TemplateNumberModel) {
- Number number = convertUnwrappedNumber(hint,
- ((TemplateNumberModel)model).getAsNumber());
+ Number number = forceUnwrappedNumberToType(
+ ((TemplateNumberModel)model).getAsNumber(), hint, is2321Bugfixed());
if(number != null) {
return number;
}
}
}
- if(isBoolean || BOOLEAN_CLASS == hint) {
+ if(Boolean.TYPE == hint || BOOLEAN_CLASS == hint) {
if(model instanceof TemplateBooleanModel) {
return ((TemplateBooleanModel)model).getAsBoolean()
? Boolean.TRUE : Boolean.FALSE;
@@ -795,8 +816,7 @@
try {
int size = seq.size();
for (int i = 0; i < size; i++) {
- Object val = unwrap(seq.get(i), componentType,
- recursionStops);
+ Object val = tryUnwrap(seq.get(i), componentType, recursionStops);
if(val == CAN_NOT_UNWRAP) {
return CAN_NOT_UNWRAP;
}
@@ -812,7 +832,7 @@
}
// Allow one-char strings to be coerced to characters
- if(isChar || hint == CHARACTER_CLASS) {
+ if(Character.TYPE == hint || hint == CHARACTER_CLASS) {
if(model instanceof TemplateScalarModel) {
String s = ((TemplateScalarModel)model).getAsString();
if(s.length() == 1) {
@@ -879,57 +899,64 @@
return CAN_NOT_UNWRAP;
}
- private static Number convertUnwrappedNumber(Class hint, Number number)
- {
- if(hint == Integer.TYPE || hint == Integer.class) {
- return number instanceof Integer ? (Integer)number :
- new Integer(number.intValue());
- }
- if(hint == Long.TYPE || hint == Long.class) {
- return number instanceof Long ? (Long)number :
- new Long(number.longValue());
- }
- if(hint == Float.TYPE || hint == Float.class) {
- return number instanceof Float ? (Float)number :
- new Float(number.floatValue());
- }
- if(hint == Double.TYPE
- || hint == Double.class) {
- return number instanceof Double ? (Double)number :
- new Double(number.doubleValue());
- }
- if(hint == Byte.TYPE || hint == Byte.class) {
- return number instanceof Byte ? (Byte)number :
- new Byte(number.byteValue());
- }
- if(hint == Short.TYPE || hint == Short.class) {
- return number instanceof Short ? (Short)number :
- new Short(number.shortValue());
- }
- if(hint == BigInteger.class) {
- return number instanceof BigInteger ? number :
- new BigInteger(number.toString());
- }
- if(hint == BigDecimal.class) {
- if(number instanceof BigDecimal) {
- return number;
+ /**
+ * Converts a number to the target type aggressively (possibly with overflow or significant loss of precision).
+ * @param n Non-{@code null}
+ * @return {@code null} if the conversion has failed.
+ */
+ static Number forceUnwrappedNumberToType(final Number n, final Class targetType, boolean bugfixed) {
+ // We try to order the conditions by decreasing probability.
+ if (targetType == n.getClass()) {
+ return n;
+ } else if (targetType == Integer.TYPE || targetType == Integer.class) {
+ return n instanceof Integer ? (Integer) n : new Integer(n.intValue());
+ } else if (targetType == Long.TYPE || targetType == Long.class) {
+ return n instanceof Long ? (Long) n : new Long(n.longValue());
+ } else if (targetType == Double.TYPE || targetType == Double.class) {
+ return n instanceof Double ? (Double) n : new Double(n.doubleValue());
+ } else if(targetType == BigDecimal.class) {
+ if(n instanceof BigDecimal) {
+ return n;
+ } else if (n instanceof BigInteger) {
+ return new BigDecimal((BigInteger) n);
+ } else if (n instanceof Long) {
+ // Because we can't represent long accurately as double
+ return BigDecimal.valueOf(n.longValue());
+ } else {
+ return new BigDecimal(n.doubleValue());
}
- if(number instanceof BigInteger) {
- return new BigDecimal((BigInteger)number);
+ } else if (targetType == Float.TYPE || targetType == Float.class) {
+ return n instanceof Float ? (Float) n : new Float(n.floatValue());
+ } else if (targetType == Byte.TYPE || targetType == Byte.class) {
+ return n instanceof Byte ? (Byte) n : new Byte(n.byteValue());
+ } else if (targetType == Short.TYPE || targetType == Short.class) {
+ return n instanceof Short ? (Short) n : new Short(n.shortValue());
+ } else if (targetType == BigInteger.class) {
+ if (n instanceof BigInteger) {
+ return n;
+ } else if (bugfixed) {
+ if (n instanceof OverloadedNumberUtil.IntegerBigDecimal) {
+ return ((OverloadedNumberUtil.IntegerBigDecimal) n).bigIntegerValue();
+ } else if (n instanceof BigDecimal) {
+ return ((BigDecimal) n).toBigInteger();
+ } else {
+ return BigInteger.valueOf(n.longValue());
+ }
+ } else {
+ // This is wrong, because something like "123.4" will cause NumberFormatException instead of flooring.
+ return new BigInteger(n.toString());
}
- if(number instanceof Long) {
- // Because we can't represent long accurately as a
- // double
- return new BigDecimal(number.toString());
+ } else {
+ final Number oriN = n instanceof OverloadedNumberUtil.NumberWithFallbackType
+ ? ((OverloadedNumberUtil.NumberWithFallbackType) n).getSourceNumber() : n;
+ if (targetType.isInstance(oriN)) {
+ // Handle nonstandard Number subclasses as well as directly java.lang.Number.
+ return oriN;
+ } else {
+ // Fails
+ return null;
}
- return new BigDecimal(number.doubleValue());
}
- // Handle nonstandard Number subclasses as well as directly
- // java.lang.Number too
- if(hint.isInstance(number)) {
- return number;
- }
- return null;
}
/**
@@ -1726,6 +1753,7 @@
* Converts any {@link BigDecimal}s in the passed array to the type of
* the corresponding formal argument of the method.
*/
+ // Unused?
public static void coerceBigDecimals(AccessibleObject callable, Object[] args)
{
Class[] formalTypes = null;
@@ -1775,7 +1803,7 @@
}
}
}
-
+
public static Object coerceBigDecimal(BigDecimal bd, Class formalType) {
// int is expected in most situations, so we check it first
if(formalType == Integer.TYPE || formalType == Integer.class) {
@@ -1798,8 +1826,9 @@
}
else if(BIGINTEGER_CLASS.isAssignableFrom(formalType)) {
return bd.toBigInteger();
+ } else {
+ return bd;
}
- return bd;
}
private static ClassBasedModelFactory createEnumModels(BeansWrapper wrapper) {
diff --git a/src/main/java/freemarker/ext/beans/ClassString.java b/src/main/java/freemarker/ext/beans/ClassString.java
deleted file mode 100644
index 305238f..0000000
--- a/src/main/java/freemarker/ext/beans/ClassString.java
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * Copyright (c) 2003 The Visigoth Software Society. All rights
- * reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * 3. The end-user documentation included with the redistribution, if
- * any, must include the following acknowledgement:
- * "This product includes software developed by the
- * Visigoth Software Society (http://www.visigoths.org/)."
- * Alternately, this acknowledgement may appear in the software itself,
- * if and wherever such third-party acknowledgements normally appear.
- *
- * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
- * project contributors may be used to endorse or promote products derived
- * from this software without prior written permission. For written
- * permission, please contact visigoths@visigoths.org.
- *
- * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
- * nor may "FreeMarker" or "Visigoth" appear in their names
- * without prior written permission of the Visigoth Software Society.
- *
- * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
- * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
- * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
- * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Visigoth Software Society. For more
- * information on the Visigoth Software Society, please see
- * http://www.visigoths.org/
- */
-package freemarker.ext.beans;
-
-import java.math.BigDecimal;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-
-
-/**
- * @author Attila Szegedi
- */
-final class ClassString
-{
- private final Class[] classes;
- private final boolean bugfixed;
-
- ClassString(Object[] objects, boolean bugfixed) {
- int l = objects.length;
- classes = new Class[l];
- for(int i = 0; i < l; ++i) {
- Object obj = objects[i];
- classes[i] = obj == null
- ? (bugfixed ? Null.class : Object.class)
- : obj.getClass();
- }
- this.bugfixed = bugfixed;
- }
-
- public int hashCode() {
- int hash = 0;
- for(int i = 0; i < classes.length; ++i) {
- hash ^= classes[i].hashCode();
- }
- return hash;
- }
-
- public boolean equals(Object o) {
- if(o instanceof ClassString) {
- ClassString cs = (ClassString)o;
- if(cs.classes.length != classes.length) {
- return false;
- }
- for(int i = 0; i < classes.length; ++i) {
- if(cs.classes[i] != classes[i]) {
- return false;
- }
- }
- return true;
- }
- return false;
- }
-
- /**
- * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}.
- */
- MaybeEmptyCallableMemberDescriptor getMostSpecific(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg)
- {
- LinkedList/*<CallableMemberDescriptor>*/ applicables = getApplicables(memberDescs, varArg);
- if(applicables.isEmpty()) {
- return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
- }
- if(applicables.size() == 1) {
- return (CallableMemberDescriptor) applicables.getFirst();
- }
-
- LinkedList/*<CallableMemberDescriptor>*/ maximals = new LinkedList();
- for (Iterator applicablesIter = applicables.iterator(); applicablesIter.hasNext();)
- {
- CallableMemberDescriptor applicable = (CallableMemberDescriptor) applicablesIter.next();
- boolean lessSpecific = false;
- for (Iterator maximalsIter = maximals.iterator();
- maximalsIter.hasNext();)
- {
- CallableMemberDescriptor maximal = (CallableMemberDescriptor) maximalsIter.next();
- switch(compareParameterTypesSpecificity(applicable.paramTypes, maximal.paramTypes, varArg)) {
- case 1: {
- maximalsIter.remove();
- break;
- }
- case -1: {
- lessSpecific = true;
- break;
- }
- }
- }
- if(!lessSpecific) {
- maximals.addLast(applicable);
- }
- }
- if(maximals.size() > 1) {
- return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
- }
- return (CallableMemberDescriptor) maximals.getFirst();
- }
-
- private int compareParameterTypesSpecificity(Class[] paramTypes1, Class[] paramTypes2, boolean varArg) {
- final int paramTypes1Len = paramTypes1.length;
- final int paramTypes2Len = paramTypes2.length;
- //assert varArg || cl1 == cl2;
-
- if (bugfixed) {
- int currentWinner = 0;
- int currentHighScore = 0;
- for(int i = 0; i < paramTypes1Len; ++i) {
- Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
- Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
- if(paramType1 != paramType2) {
- int score;
-
- score = MethodUtilities.isMoreOrSameSpecificParameterType(
- paramType1, paramType2, true,
- currentWinner == -1 ? currentHighScore - 1 : currentHighScore);
- if (score > currentHighScore) {
- currentHighScore = score;
- currentWinner = 1;
- } else if (score == currentHighScore && currentWinner == -1) {
- currentWinner = 0;
- }
-
- score = MethodUtilities.isMoreOrSameSpecificParameterType(
- paramType2, paramType1, true,
- currentWinner == 1 ? currentHighScore - 1 : currentHighScore);
- if (score > currentHighScore) {
- currentHighScore = score;
- currentWinner = -1;
- } else if (score == currentHighScore && currentWinner == 1) {
- currentWinner = 0;
- }
- }
- }
- return currentWinner;
- } else {
- boolean paramTypes1HasAMoreSpecific = false;
- boolean paramTypes2HasAMoreSpecific = false;
- for(int i = 0; i < paramTypes1Len; ++i) {
- Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
- Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
- if(paramType1 != paramType2) {
- paramTypes1HasAMoreSpecific =
- paramTypes1HasAMoreSpecific
- || MethodUtilities.isMoreOrSameSpecificParameterType(paramType1, paramType2, false, 0) != 0;
- paramTypes2HasAMoreSpecific =
- paramTypes2HasAMoreSpecific
- || MethodUtilities.isMoreOrSameSpecificParameterType(paramType2, paramType1, false, 0) != 0;
- }
- }
-
- if(paramTypes1HasAMoreSpecific) {
- return paramTypes2HasAMoreSpecific ? 0 : 1;
- } else if(paramTypes2HasAMoreSpecific) {
- return -1;
- } else {
- return 0;
- }
- }
- }
-
- private static Class getParamType(Class[] paramTypes, int paramTypesLen, int i, boolean varArg) {
- return varArg && i >= paramTypesLen - 1
- ? paramTypes[paramTypesLen - 1].getComponentType()
- : paramTypes[i];
- }
-
- /**
- * Returns all methods that are applicable to actual
- * parameter classes represented by this ClassString object.
- */
- LinkedList/*<CallableMemberDescriptor>*/ getApplicables(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg) {
- LinkedList/*<CallableMemberDescriptor>*/ list = new LinkedList();
- for (Iterator it = memberDescs.iterator(); it.hasNext();) {
- CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) it.next();
- if(isApplicable(memberDesc, varArg)) {
- list.add(memberDesc);
- }
- }
- return list;
- }
-
- /**
- * Returns true if the supplied method is applicable to actual
- * parameter classes represented by this ClassString object.
- */
- private boolean isApplicable(CallableMemberDescriptor memberDesc, boolean varArg) {
- final Class[] formalTypes = memberDesc.paramTypes;
- final int cl = classes.length;
- final int fl = formalTypes.length - (varArg ? 1 : 0);
- if(varArg) {
- if(cl < fl) {
- return false;
- }
- } else {
- if(cl != fl) {
- return false;
- }
- }
- for(int i = 0; i < fl; ++i) {
- if(!isMethodInvocationConvertible(formalTypes[i], classes[i])) {
- return false;
- }
- }
- if(varArg) {
- Class varArgType = formalTypes[fl].getComponentType();
- for(int i = fl; i < cl; ++i) {
- if(!isMethodInvocationConvertible(varArgType, classes[i])) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * Determines whether a type represented by a class object is
- * convertible to another type represented by a class object using a
- * method invocation conversion, treating object types of primitive
- * types as if they were primitive types (that is, a Boolean actual
- * parameter type matches boolean primitive formal type). This behavior
- * is because this method is used to determine applicable methods for
- * an actual parameter list, and primitive types are represented by
- * their object duals in reflective method calls.
- * @param formal the formal parameter type to which the actual
- * parameter type should be convertible
- * @param actual the actual parameter type.
- * @return true if either formal type is assignable from actual type,
- * or formal is a primitive type and actual is its corresponding object
- * type or an object type of a primitive type that can be converted to
- * the formal type.
- */
- private boolean isMethodInvocationConvertible(Class formal, Class actual) {
- // Check for identity or widening reference conversion
- if(formal.isAssignableFrom(actual)) {
- return true;
- }
-
- if (actual == Null.class && bugfixed) {
- return !formal.isPrimitive();
- }
-
- // Check for boxing with widening primitive conversion. Note that
- // actual parameters are never primitives.
- // FIXME: Why we don't do the same with boxing types too?
- if(formal.isPrimitive()) {
- if(formal == Boolean.TYPE) {
- return actual == Boolean.class;
- } else if (formal == Double.TYPE &&
- (actual == Double.class || actual == Float.class ||
- actual == Long.class || actual == Integer.class ||
- actual == Short.class || actual == Byte.class)) {
- return true;
- } else if (formal == Integer.TYPE &&
- (actual == Integer.class || actual == Short.class ||
- actual == Byte.class)) {
- return true;
- } else if (formal == Long.TYPE &&
- (actual == Long.class || actual == Integer.class ||
- actual == Short.class || actual == Byte.class)) {
- return true;
- } else if (formal == Float.TYPE &&
- (actual == Float.class || actual == Long.class ||
- actual == Integer.class || actual == Short.class ||
- actual == Byte.class)) {
- return true;
- } else if (formal == Character.TYPE) {
- return actual == Character.class;
- } else if(formal == Byte.TYPE && actual == Byte.class) {
- return true;
- } else if(formal == Short.TYPE &&
- (actual == Short.class || actual == Byte.class)) {
- return true;
- } else if (BigDecimal.class.isAssignableFrom(actual) && isNumerical(formal)) {
- // Special case for BigDecimals as we deem BigDecimal to be
- // convertible to any numeric type - either object or primitive.
- // This can actually cause us trouble as this is a narrowing
- // conversion, not widening.
- return true;
- } else {
- return false;
- }
- }
- return false;
- }
-
- private static boolean isNumerical(Class type) {
- return Number.class.isAssignableFrom(type)
- || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE;
- }
-
- /**
- * Symbolizes the class of null (it's missing from Java).
- */
- private static class Null {
-
- // Can't be instantiated
- private Null() { }
-
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/beans/MethodUtilities.java b/src/main/java/freemarker/ext/beans/MethodUtilities.java
index abcf4e6..3c07324 100644
--- a/src/main/java/freemarker/ext/beans/MethodUtilities.java
+++ b/src/main/java/freemarker/ext/beans/MethodUtilities.java
@@ -69,7 +69,7 @@
/**
* Determines whether the type given as the 1st argument is convertible to the type given as the 2nd argument
* for method call argument conversion. This follows the rules of the Java reflection-based method call, except
- * that since we don't have the value here, a boxed calls is never seens as convertable to a primtivie type.
+ * that since we don't have the value here, a boxed class is never seen as convertible to a primitive type.
*
* @return 0 means {@code false}, non-0 means {@code true}.
* That is, 0 is returned less specificity or incomparable specificity, also when if
diff --git a/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java b/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java
index 7b55ee7..2aa493a 100644
--- a/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java
+++ b/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java
@@ -51,7 +51,6 @@
*/
package freemarker.ext.beans;
-import java.lang.reflect.Member;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -63,8 +62,7 @@
* Stores the non-varargs methods for a {@link OverloadedMethods} object.
* @author Attila Szegedi
*/
-class OverloadedFixArgsMethods extends OverloadedMethodsSubset
-{
+class OverloadedFixArgsMethods extends OverloadedMethodsSubset {
OverloadedFixArgsMethods(BeansWrapper beansWrapper) {
super(beansWrapper);
@@ -74,7 +72,7 @@
return memberDesc.paramTypes;
}
- void afterWideningUnwrappingHints(Class[] paramTypes) {
+ void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
// Do nothing
}
@@ -89,15 +87,24 @@
if(unwrappingHintsByParamCount.length <= argCount) {
return EmptyMemberAndArguments.NO_SUCH_METHOD;
}
- Class[] unwarppingArgumentTypes = unwrappingHintsByParamCount[argCount];
- if(unwarppingArgumentTypes == null) {
+ Class[] unwarppingHints = unwrappingHintsByParamCount[argCount];
+ if(unwarppingHints == null) {
return EmptyMemberAndArguments.NO_SUCH_METHOD;
}
Object[] pojoArgs = new Object[argCount];
+
+ int[] possibleNumericalTypes = getPossibleNumericalTypes(argCount);
+ if (possibleNumericalTypes == ALL_ZEROS_ARRAY) {
+ possibleNumericalTypes = null;
+ }
+
Iterator it = tmArgs.iterator();
for(int i = 0; i < argCount; ++i) {
- Object pojo = w.unwrapInternal((TemplateModel)it.next(), unwarppingArgumentTypes[i]);
+ Object pojo = w.tryUnwrap(
+ (TemplateModel) it.next(),
+ unwarppingHints[i],
+ possibleNumericalTypes != null ? possibleNumericalTypes[i] : 0);
if(pojo == BeansWrapper.CAN_NOT_UNWRAP) {
return EmptyMemberAndArguments.NO_SUCH_METHOD;
}
@@ -107,7 +114,16 @@
MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, false);
if(maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
- BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+ if (bugfixed) {
+ if (possibleNumericalTypes != null) {
+ // Note that overloaded method selection has already accounted for overflow errors when the method
+ // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+ // BigDecimal is allowed to overflow for backward-compatibility.
+ forceNumberArgumentsToParameterTypes(pojoArgs, memberDesc.paramTypes, possibleNumericalTypes);
+ }
+ } else {
+ BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+ }
return new MemberAndArguments(memberDesc.member, pojoArgs);
} else {
return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc);
diff --git a/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java b/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java
index 2cb282b..ccc18e7 100644
--- a/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java
+++ b/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java
@@ -62,18 +62,32 @@
import freemarker.template.TemplateModelException;
import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
/**
* @author Attila Szegedi
*/
abstract class OverloadedMethodsSubset {
+
+ /**
+ * Used for an optimization trick to substitute an array of whatever size that contains only 0-s. Since this array
+ * is 0 long, this means that the code that reads the int[] always have to check if the int[] has this value, and
+ * then it has to act like if was all 0-s.
+ */
+ static final int[] ALL_ZEROS_ARRAY = new int[0];
private Class[/*number of args*/][/*arg index*/] unwrappingHintsByParamCount;
+ /**
+ * Tells what numerical types occur at a given parameter position with a bit field, also if the
+ * unwrapping will do proper conversion. See {@link OverloadedNumberUtil#FLAG_INTEGER}, etc.
+ */
+ private int[/*number of args*/][/*arg index*/] possibleNumericalTypesByParamCount;
+
// TODO: This can cause memory-leak when classes are re-loaded. However, first the genericClassIntrospectionCache
// and such need to be fixed in this regard.
// Java 5: Use ConcurrentHashMap:
- private final Map/*<ClassString, MaybeEmptyCallableMemberDescriptor>*/ argTypesToMemberDescCache = new HashMap();
+ private final Map/*<ArgumentTypes, MaybeEmptyCallableMemberDescriptor>*/ argTypesToMemberDescCache = new HashMap();
private final List/*<CallableMemberDescriptor>*/ memberDescs = new LinkedList();
@@ -81,56 +95,73 @@
protected final boolean bugfixed;
OverloadedMethodsSubset(BeansWrapper beansWrapper) {
- bugfixed = beansWrapper.getIncompatibleImprovements().intValue() >= 2003021;
+ bugfixed = beansWrapper.is2321Bugfixed();
}
void addCallableMemberDescriptor(CallableMemberDescriptor memberDesc) {
- final int paramCount = memberDesc.paramTypes.length;
-
memberDescs.add(memberDesc);
- final Class[] preprocessedParamTypes = preprocessParameterTypes(memberDesc);
+ // Warning: Do not modify this array, or put it into unwrappingHintsByParamCount by reference, as the arrays
+ // inside that are modified!
+ final Class[] prepedParamTypes = preprocessParameterTypes(memberDesc);
+ final int paramCount = prepedParamTypes.length; // Must be the same as the length of the original param list
- final Class[] unwrappingHints = (Class[]) preprocessedParamTypes.clone();
// Merge these unwrapping hints with the existing table of hints:
if (unwrappingHintsByParamCount == null) {
unwrappingHintsByParamCount = new Class[paramCount + 1][];
- unwrappingHintsByParamCount[paramCount] = unwrappingHints;
+ unwrappingHintsByParamCount[paramCount] = (Class[]) prepedParamTypes.clone();
} else if (unwrappingHintsByParamCount.length <= paramCount) {
Class[][] newUnwrappingHintsByParamCount = new Class[paramCount + 1][];
System.arraycopy(unwrappingHintsByParamCount, 0, newUnwrappingHintsByParamCount, 0,
unwrappingHintsByParamCount.length);
unwrappingHintsByParamCount = newUnwrappingHintsByParamCount;
- unwrappingHintsByParamCount[paramCount] = unwrappingHints;
+ unwrappingHintsByParamCount[paramCount] = (Class[]) prepedParamTypes.clone();
} else {
- Class[] prevUnwrappingHints = unwrappingHintsByParamCount[paramCount];
- if (prevUnwrappingHints == null) {
- unwrappingHintsByParamCount[paramCount] = unwrappingHints;
+ Class[] unwrappingHints = unwrappingHintsByParamCount[paramCount];
+ if (unwrappingHints == null) {
+ unwrappingHintsByParamCount[paramCount] = (Class[]) prepedParamTypes.clone();
} else {
- for(int i = 0; i < prevUnwrappingHints.length; ++i) {
+ for (int paramIdx = 0; paramIdx < prepedParamTypes.length; paramIdx++) {
// For each parameter list length, we merge the argument type arrays into a single Class[] that
- // stores the most specific common types of each position. Hence we will possibly use a too generic
+ // stores the most specific common types for each position. Hence we will possibly use a too generic
// hint for the unwrapping. For correct behavior, for each overloaded methods its own parameter
// types should be used as a hint. But without unwrapping the arguments, we couldn't select the
// overloaded method. So this is a circular reference problem. We could try selecting the
// method based on the wrapped value, but that's quite tricky, and the result of such selection
- // is not cacheable (the TM types are not enough as cache key then. So we just use this
+ // is not cacheable (the TM types are not enough as cache key then). So we just use this
// compromise.
- prevUnwrappingHints[i] = getCommonSupertypeForUnwrappingHint(
- prevUnwrappingHints[i], unwrappingHints[i]);
+ unwrappingHints[paramIdx] = getCommonSupertypeForUnwrappingHint(
+ unwrappingHints[paramIdx], prepedParamTypes[paramIdx]);
}
}
}
+
+ int[] paramNumericalTypes = ALL_ZEROS_ARRAY;
+ if (bugfixed) {
+ // Fill possibleNumericalTypesByParamCount (if necessary)
+ for (int paramIdx = 0; paramIdx < paramCount; paramIdx++) {
+ final int numType = OverloadedNumberUtil.classToTypeFlags(prepedParamTypes[paramIdx]);
+ if (numType != 0) { // It's a numerical type
+ if (paramNumericalTypes == ALL_ZEROS_ARRAY) {
+ paramNumericalTypes = new int[paramCount];
+ }
+ paramNumericalTypes[paramIdx] = numType;
+ }
+ }
+ mergeInNumericalTypes(paramCount, paramNumericalTypes);
+ }
- afterWideningUnwrappingHints(bugfixed ? preprocessedParamTypes : unwrappingHints);
+ afterWideningUnwrappingHints(
+ bugfixed ? prepedParamTypes : unwrappingHintsByParamCount[paramCount],
+ paramNumericalTypes);
}
Class[][] getUnwrappingHintsByParamCount() {
return unwrappingHintsByParamCount;
}
- MaybeEmptyCallableMemberDescriptor getMemberDescriptorForArgs(Object[] args, boolean varArg) {
- ClassString argTypes = new ClassString(args, bugfixed);
+ final MaybeEmptyCallableMemberDescriptor getMemberDescriptorForArgs(Object[] args, boolean varArg) {
+ ArgumentTypes argTypes = new ArgumentTypes(args, bugfixed);
MaybeEmptyCallableMemberDescriptor memberDesc;
synchronized(argTypesToMemberDescCache) {
memberDesc = (MaybeEmptyCallableMemberDescriptor) argTypesToMemberDescCache.get(argTypes);
@@ -147,19 +178,18 @@
}
abstract Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc);
- abstract void afterWideningUnwrappingHints(Class[] paramTypes);
+ abstract void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes);
abstract MaybeEmptyMemberAndArguments getMemberAndArguments(List/*<TemplateModel>*/ tmArgs,
BeansWrapper w) throws TemplateModelException;
/**
* Returns the most specific common class (or interface) of two parameter types for the purpose of unwrapping.
- * This is trickier then finding the most specific overlapping superclass of two classes, because:
+ * This is trickier than finding the most specific overlapping superclass of two classes, because:
* <ul>
* <li>It considers primitive classes as the subclasses of the boxing classes.</li>
- * <li>It considers widening numerical conversion as if the narrower type is subclass.</li>
* <li>If the only common class is {@link Object}, it will try to find a common interface. If there are more
- * of them, it will start removing those that are known to be uninteresting as unwrapping hint.</li>
+ * of them, it will start removing those that are known to be uninteresting as unwrapping hints.</li>
* </ul>
*
* @param c1 Parameter type 1
@@ -180,7 +210,7 @@
c1WasPrim = false;
}
- // c1 primitive class to boxing class:
+ // c2 primitive class to boxing class:
final boolean c2WasPrim;
if (c2.isPrimitive()) {
c2 = ClassUtil.primitiveClassToBoxingClass(c2);
@@ -190,13 +220,15 @@
}
if (c1 == c2) {
- // If it was like int and Integer, boolean and Boolean, etc.
+ // If it was like int and Integer, boolean and Boolean, etc., we return the boxing type (as that's the
+ // less specific, because it allows null.)
// (If it was two equivalent primitives, we don't get here, because of the 1st line of the method.)
return c1;
} else if (Number.class.isAssignableFrom(c1) && Number.class.isAssignableFrom(c2)) {
- // We don't want the unwrapper to convert to a numerical super-type as it's not yet known what the
- // actual number type of the chosen method will be. (Especially as fixed point to floating point can be
- // lossy.)
+ // We don't want the unwrapper to convert to a numerical super-type [*] as it's not yet known what the
+ // actual number type of the chosen method will be. We will postpone the actual numerical conversion
+ // until that, especially as some conversions (like fixed point to floating point) can be lossy.
+ // * Numerical super-type: Like long > int > short > byte.
return Number.class;
} else if (c1WasPrim || c2WasPrim) {
// At this point these all stand:
@@ -218,7 +250,9 @@
}
}
- // If buxfixed is true and any of the classes was a primitive type, we never reach this point.
+ // We never get to this point if buxfixed is true and any of these stands:
+ // - One of classes was a primitive type
+ // - One of classes was a numerical type (either boxing type or primitive)
Set commonTypes = MethodUtilities.getAssignables(c1, c2);
commonTypes.retainAll(MethodUtilities.getAssignables(c2, c1));
@@ -237,15 +271,18 @@
Class clazz = (Class)commonTypesIter.next();
for (Iterator maxIter = max.iterator(); maxIter.hasNext();) {
Class maxClazz = (Class)maxIter.next();
- if(MethodUtilities.isMoreOrSameSpecificParameterType(maxClazz, clazz, bugfixed, 0) != 0) {
+ if(MethodUtilities.isMoreOrSameSpecificParameterType(maxClazz, clazz, false /*bugfixed [1]*/, 0) != 0) {
// clazz can't be maximal, if there's already a more specific or equal maximal than it.
continue listCommonTypes;
}
- if(MethodUtilities.isMoreOrSameSpecificParameterType(clazz, maxClazz, bugfixed, 0) != 0) {
+ if(MethodUtilities.isMoreOrSameSpecificParameterType(clazz, maxClazz, false /*bugfixed [1]*/, 0) != 0) {
// If it's more specific than a currently maximal element,
// that currently maximal is no longer a maximal.
maxIter.remove();
}
+ // 1: We don't use bugfixed at the "[1]"-marked points because it's slower and doesn't make any
+ // difference here as it's ensured that nor c1 nor c2 is primitive or numerical. The bugfix has only
+ // affected the treatment of primitives and numerical types.
}
// If we get here, no current maximal is more specific than the
// current class, so clazz is a new maximal so far.
@@ -288,4 +325,112 @@
return (Class) max.get(0);
}
+ /**
+ * Gets the numerical type "flags" of each parameter positions, or {@code null} if there's no method with this
+ * parameter count or if we aren't in pre-2.3.21 mode, {@link #ALL_ZEROS_ARRAY} if there were no numerical
+ * parameters. The returned {@code int}-s are on or more {@link OverloadedNumberUtil}} {@code FLAG_...} constants
+ * binary "or"-ed together.
+ */
+ final protected int[] getPossibleNumericalTypes(int paramCount) {
+ return possibleNumericalTypesByParamCount != null && possibleNumericalTypesByParamCount.length > paramCount
+ ? possibleNumericalTypesByParamCount[paramCount]
+ : null;
+ }
+
+ /**
+ * @param dstParamCount The parameter count for which we want to merge in the possible numerical types
+ * @param srcNumTypesByParamIdx If shorter than {@code dstParamCount}, it's last item will be repeated until
+ * dstParamCount length is reached. If longer, the excessive items will be ignored.
+ * Maybe {@link #ALL_ZEROS_ARRAY}. Cant'be a 0-length array. Can't be {@code null}.
+ */
+ final protected void mergeInNumericalTypes(int dstParamCount, int[] srcNumTypesByParamIdx) {
+ NullArgumentException.check("srcNumTypesByParamIdx", srcNumTypesByParamIdx);
+ if (dstParamCount == 0) return;
+
+ // Ensure that possibleNumericalTypesByParamCount[dstParamCount] exists:
+ if (possibleNumericalTypesByParamCount == null) {
+ possibleNumericalTypesByParamCount = new int[dstParamCount + 1][];
+ } else if (possibleNumericalTypesByParamCount.length <= dstParamCount) {
+ if (srcNumTypesByParamIdx == null) return;
+
+ int[][] newNumericalTypeByParamCount = new int[dstParamCount + 1][];
+ System.arraycopy(possibleNumericalTypesByParamCount, 0, newNumericalTypeByParamCount, 0,
+ possibleNumericalTypesByParamCount.length);
+ possibleNumericalTypesByParamCount = newNumericalTypeByParamCount;
+ }
+
+ final int srcParamCount = srcNumTypesByParamIdx.length;
+
+ int[] dstNumTypesByParamIdx = possibleNumericalTypesByParamCount[dstParamCount];
+ if (dstNumTypesByParamIdx == null) {
+ // This is the first method added with this number of params => no merging
+
+ if (srcNumTypesByParamIdx != ALL_ZEROS_ARRAY) {
+ dstNumTypesByParamIdx = new int[dstParamCount];
+ for (int paramIdx = 0; paramIdx < dstParamCount; paramIdx++) {
+ dstNumTypesByParamIdx[paramIdx]
+ = srcNumTypesByParamIdx[paramIdx < srcParamCount ? paramIdx : srcParamCount - 1];
+ }
+ } else {
+ dstNumTypesByParamIdx = ALL_ZEROS_ARRAY;
+ }
+
+ possibleNumericalTypesByParamCount[dstParamCount] = dstNumTypesByParamIdx;
+ } else {
+ // dstNumTypesByParamIdx != null, so we need to merge into it.
+
+ if (srcNumTypesByParamIdx == dstNumTypesByParamIdx) {
+ // Used to occur when both are ALL_ZEROS_ARRAY
+ return;
+ }
+
+ // As we will write dstNumTypesByParamIdx, it can't remain ALL_ZEROS_ARRAY anymore.
+ if (dstNumTypesByParamIdx == ALL_ZEROS_ARRAY && dstParamCount > 0) {
+ dstNumTypesByParamIdx = new int[dstParamCount];
+ possibleNumericalTypesByParamCount[dstParamCount] = dstNumTypesByParamIdx;
+ }
+
+ for (int paramIdx = 0; paramIdx < dstParamCount; paramIdx++) {
+ final int srcParamNumTypes
+ = srcNumTypesByParamIdx != ALL_ZEROS_ARRAY
+ ? srcNumTypesByParamIdx[paramIdx < srcParamCount ? paramIdx : srcParamCount - 1]
+ : 0;
+ final int dstParamNumTypes = dstNumTypesByParamIdx[paramIdx];
+ if (dstParamNumTypes != srcParamNumTypes) {
+ dstNumTypesByParamIdx[paramIdx]
+ = dstParamNumTypes | srcParamNumTypes | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT;
+ }
+ // Note that if a parameter is non-numerical, its paraNumTypes is 0. So if we have a non-number and
+ // a some kind of number (non-0), the merged result will be marked with FLAG_WIDENED_UNWRAPPING_HINT.
+ // (I.e., the same happens as with two different numerical types.)
+ }
+ }
+ }
+
+ protected void forceNumberArgumentsToParameterTypes(
+ Object[] args, Class[] paramTypes, int[] unwrappingNumTypesByParamIndex) {
+ final int paramTypesLen = paramTypes.length;
+ final int argsLen = args.length;
+ for (int argIdx = 0; argIdx < argsLen; argIdx++) {
+ final int paramTypeIdx = argIdx < paramTypesLen ? argIdx : paramTypesLen - 1;
+ final int unwrappingNumTypes = unwrappingNumTypesByParamIndex[paramTypeIdx];
+
+ // Forcing the number type can only be interesting if there are numerical parameter types on that index,
+ // and the unwrapping was not to an exact numerical type.
+ if (unwrappingNumTypes != 0
+ && (unwrappingNumTypes & OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT) != 0) {
+ final Object arg = args[argIdx];
+ // If arg isn't a number, we can't do any conversions anyway, regardless of the param type.
+ if (arg instanceof Number) {
+ final Class targetType = paramTypes[paramTypeIdx];
+ final Number convertedArg = BeansWrapper.forceUnwrappedNumberToType(
+ (Number) arg, targetType, bugfixed);
+ if (convertedArg != null) {
+ args[argIdx] = convertedArg;
+ }
+ }
+ }
+ }
+ }
+
}
diff --git a/src/main/java/freemarker/ext/beans/OverloadedNumberUtil.java b/src/main/java/freemarker/ext/beans/OverloadedNumberUtil.java
new file mode 100644
index 0000000..77c673e
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/OverloadedNumberUtil.java
@@ -0,0 +1,1248 @@
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NumberUtil;
+
+/**
+ * Everything related to coercion to ambiguous numerical types.
+ */
+class OverloadedNumberUtil {
+
+ // Can't be instantiated
+ private OverloadedNumberUtil() { }
+
+ /** The unwrapping hint will not be a specific numerical type. */
+ static final int FLAG_WIDENED_UNWRAPPING_HINT = 1;
+
+ static final int FLAG_BYTE = 4;
+ static final int FLAG_SHORT = 8;
+ static final int FLAG_INTEGER = 16;
+ static final int FLAG_LONG = 32;
+ static final int FLAG_FLOAT = 64;
+ static final int FLAG_DOUBLE = 128;
+ static final int FLAG_BIG_INTEGER = 256;
+ static final int FLAG_BIG_DECIMAL = 512;
+ static final int FLAG_UNKNOWN_TYPE = 1024;
+
+ static final int MASK_KNOWN_INTEGERS = FLAG_BYTE | FLAG_SHORT | FLAG_INTEGER | FLAG_LONG | FLAG_BIG_INTEGER;
+ static final int MASK_KNOWN_NONINTEGERS = FLAG_FLOAT | FLAG_DOUBLE | FLAG_BIG_DECIMAL;
+ static final int MASK_ALL_KNOWN_TYPES = MASK_KNOWN_INTEGERS | MASK_KNOWN_NONINTEGERS;
+ static final int MASK_ALL_TYPES = MASK_ALL_KNOWN_TYPES | FLAG_UNKNOWN_TYPE;
+
+ /** The highest long that can be stored in double without precision loss: 2**53. */
+ private static final long MAX_DOUBLE_OR_LONG = 9007199254740992L;
+ /** The lowest long that can be stored in double without precision loss: -(2**53). */
+ private static final long MIN_DOUBLE_OR_LONG = -9007199254740992L;
+ private static final int MAX_DOUBLE_OR_LONG_LOG_2 = 53;
+
+ /** The highest long that can be stored in float without precision loss: 2**24. */
+ private static final int MAX_FLOAT_OR_INT = 16777216;
+ /** The lowest long that can be stored in float without precision loss: -(2**24). */
+ private static final int MIN_FLOAT_OR_INT = -16777216;
+ private static final int MAX_FLOAT_OR_INT_LOG_2 = 24;
+
+ static boolean hasCommonFlags(Class pClass, int numberTypeFlags) {
+ return (OverloadedNumberUtil.classToTypeFlags(pClass) & numberTypeFlags) != 0;
+ }
+
+ static int classToTypeFlags(Class pClass) {
+ if (pClass.isPrimitive()) {
+ if (pClass == Integer.TYPE) return FLAG_INTEGER;
+ else if (pClass == Long.TYPE) return FLAG_LONG;
+ else if (pClass == Double.TYPE) return FLAG_DOUBLE;
+ else if (pClass == Float.TYPE) return FLAG_FLOAT;
+ else if (pClass == Byte.TYPE) return FLAG_BYTE;
+ else if (pClass == Short.TYPE) return FLAG_SHORT;
+ else return 0;
+ } else if (Number.class.isAssignableFrom(pClass)) {
+ if (pClass == Integer.class) return FLAG_INTEGER;
+ else if (pClass == Long.class) return FLAG_LONG;
+ else if (pClass == Double.class) return FLAG_DOUBLE;
+ else if (pClass == Float.class) return FLAG_FLOAT;
+ else if (pClass == Byte.class) return FLAG_BYTE;
+ else if (pClass == Short.class) return FLAG_SHORT;
+ else if (BigDecimal.class.isAssignableFrom(pClass)) return FLAG_BIG_DECIMAL;
+ else if (BigInteger.class.isAssignableFrom(pClass)) return FLAG_BIG_INTEGER;
+ else return FLAG_UNKNOWN_TYPE;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Attaches the lowest alternative number type to the parameter number via {@link NumberWithFallbackType}, if
+ * that's useful according the possible target number types. This transformation is applied on the method call
+ * argument list before overloaded method selection.
+ *
+ * <p>Note that as of this writing, this method is only used when
+ * {@link BeansWrapper#getIncompatibleImprovements()} >= 2.3.21.
+ *
+ * <p>Why's this needed, how it works: Overloaded methods selection only selects methods where the <em>type</em>
+ * (not the value!) of the argument is "smaller" or the same as the parameter type. This is similar to how it's in
+ * the Java language. That it only decides based on the parameter type is important because this way
+ * {@link OverloadedMethodsSubset} can cache method lookup decisions using the types as the cache key. Problem is,
+ * since you don't declare the exact numerical types in FTL, and FTL has only a single generic numeric type
+ * anyway, what Java type a {@link TemplateNumberModel} uses internally is often seen as a technical detail of which
+ * the template author can't always keep track of. So we investigate the <em>value</em> of the number too,
+ * then coerce it down without overflow to a type that will match the most overloaded methods. (This
+ * is especially important as FTL often stores numbers in {@link BigDecimal}-s, which will hardly ever match any
+ * method parameters.) We could simply return that number, like {@code Byte(0)} for an {@code Integer(0)},
+ * however, then we would lose the information about what the original type was. The original type is sometimes
+ * important, as in ambiguous situations the method where the there's an exact type match should be selected (like,
+ * when someone wants to select an overload explicitly with {@code m(x?int)}). Also, if an overload wins where
+ * the parameter type at the position of the number is {@code Number} or {@code Object} (or {@code Comparable}
+ * etc.), it's expected that we pass in the original value (an {@code Integer} in this example), especially if that
+ * value is the return value of another Java method. That's why we use
+ * {@link NumberWithFallbackType} numerical classes like {@link IntegerOrByte}, which represents both the original
+ * type and the coerced type, all encoded into the class of the value, which is used as the overloaded method lookup
+ * cache key.
+ *
+ * <p>See also: <tt>src\main\misc\overloadedNumberRules\prices.ods</tt>.
+ *
+ * @param num the number to coerce
+ * @param targetNumTypes a bit-set of {@link #FLAG_INTEGER} and such.
+ *
+ * @returns The original number or a {@link NumberWithFallbackType}, depending on the actual value and the types
+ * indicated in the {@code targetNumTypes} parameter.
+ */
+ static Number addFallbackType(Number num, int targetNumTypes) {
+ // Java 5: use valueOf where possible
+ final Class numClass = num.getClass();
+ if (numClass == BigDecimal.class) {
+ // For now we only support the backward-compatible mode that doesn't prevent roll overs and magnitude loss.
+ // However, we push the overloaded selection to the right direction, so we will at least indicate if the
+ // number has decimals.
+ BigDecimal n = (BigDecimal) num;
+ if ((targetNumTypes & MASK_KNOWN_INTEGERS) != 0 && (targetNumTypes & MASK_KNOWN_NONINTEGERS) != 0
+ && NumberUtil.isBigDecimalInteger(n) /* <- can be expensive */) {
+ return new IntegerBigDecimal(n);
+ } else {
+ // Either it was a non-integer, or it didn't mater what it was, as we don't have both integer and
+ // non-integer target types.
+ return n;
+ }
+ } else if (numClass == Integer.class) {
+ int pn = num.intValue();
+ // Note that we try to return the most specific type (i.e., the numerical type with the smallest range), but
+ // only among the types that are possible targets. Like if the only target is int and the value is 1, we
+ // will return Integer 1, not Byte 1, even though byte is automatically converted to int so it would
+ // work too. Why we avoid unnecessarily specific types is that they generate more overloaded method lookup
+ // cache entries, since the cache key is the array of types of the argument values. So we want as few
+ // permutations as possible.
+ if ((targetNumTypes & FLAG_BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+ return new IntegerOrByte((Integer) num, (byte) pn);
+ } else if ((targetNumTypes & FLAG_SHORT) != 0 && pn <= Short.MAX_VALUE && pn >= Short.MIN_VALUE) {
+ return new IntegerOrShort((Integer) num, (short) pn);
+ } else {
+ return num;
+ }
+ } else if (numClass == Long.class) {
+ final long pn = num.longValue();
+ if ((targetNumTypes & FLAG_BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+ return new LongOrByte((Long) num, (byte) pn);
+ } else if ((targetNumTypes & FLAG_SHORT) != 0 && pn <= Short.MAX_VALUE && pn >= Short.MIN_VALUE) {
+ return new LongOrShort((Long) num, (short) pn);
+ } else if ((targetNumTypes & FLAG_INTEGER) != 0 && pn <= Integer.MAX_VALUE && pn >= Integer.MIN_VALUE) {
+ return new LongOrInteger((Long) num, (int) pn);
+ } else {
+ return num;
+ }
+ } else if (numClass == Double.class) {
+ final double doubleN = num.doubleValue();
+
+ // Can we store it in an integer type?
+ checkIfWholeNumber: do {
+ if ((targetNumTypes & MASK_KNOWN_INTEGERS) == 0) break checkIfWholeNumber;
+
+ // There's no hope to be 1-precise outside this region. (Although problems can occur even inside it...)
+ if (doubleN > MAX_DOUBLE_OR_LONG || doubleN < MIN_DOUBLE_OR_LONG) break checkIfWholeNumber;
+
+ long longN = num.longValue();
+ double diff = doubleN - longN;
+ boolean exact; // We will try to ignore precision glitches (like 0.3 - 0.2 - 0.1 = -2.7E-17)
+ if (diff == 0) {
+ exact = true;
+ } else if (diff > 0) {
+ if (diff < 0.000001) {
+ exact = false;
+ } else if (diff > 0.999999) {
+ exact = false;
+ longN++;
+ } else {
+ break checkIfWholeNumber;
+ }
+ } else { // => diff < 0
+ if (diff > -0.000001) {
+ exact = false;
+ } else if (diff < -0.999999) {
+ exact = false;
+ longN--;
+ } else {
+ break checkIfWholeNumber;
+ }
+ }
+
+ // If we reach this, it can be treated as a whole number.
+
+ if ((targetNumTypes & FLAG_BYTE) != 0 && longN <= Byte.MAX_VALUE && longN >= Byte.MIN_VALUE) {
+ return new DoubleOrByte((Double) num, (byte) longN);
+ } else if ((targetNumTypes & FLAG_SHORT) != 0 && longN <= Short.MAX_VALUE && longN >= Short.MIN_VALUE) {
+ return new DoubleOrShort((Double) num, (short) longN);
+ } else if ((targetNumTypes & FLAG_INTEGER) != 0 && longN <= Integer.MAX_VALUE && longN >= Integer.MIN_VALUE) {
+ final int intN = (int) longN;
+ // Java 5: remove the "? (Number)" and ": (Number)" casts
+ return (targetNumTypes & FLAG_FLOAT) != 0 && intN >= MIN_FLOAT_OR_INT && intN <= MAX_FLOAT_OR_INT
+ ? (Number) new DoubleOrIntegerOrFloat((Double) num, intN)
+ : (Number) new DoubleOrInteger((Double) num, intN);
+ } else if ((targetNumTypes & FLAG_LONG) != 0) {
+ if (exact) {
+ return new DoubleOrLong((Double) num, longN);
+ } else {
+ // We don't deal with non-exact numbers outside the range of int, as we already reach
+ // ULP 2.384185791015625E-7 there.
+ if (longN >= Integer.MIN_VALUE && longN <= Integer.MAX_VALUE) {
+ return new DoubleOrLong((Double) num, longN);
+ } else {
+ break checkIfWholeNumber;
+ }
+ }
+ }
+ // This point is reached if the double value was out of the range of target integer type(s).
+ // Falls through!
+ } while (false);
+ // If we reach this that means that it can't be treated as a whole number.
+
+ if ((targetNumTypes & FLAG_FLOAT) != 0 && doubleN >= -Float.MAX_VALUE && doubleN <= Float.MAX_VALUE) {
+ return new DoubleOrFloat((Double) num);
+ } else {
+ // Simply Double:
+ return num;
+ }
+ } else if (numClass == Float.class) {
+ final float floatN = num.floatValue();
+
+ // Can we store it in an integer type?
+ checkIfWholeNumber: do {
+ if ((targetNumTypes & MASK_KNOWN_INTEGERS) == 0) break checkIfWholeNumber;
+
+ // There's no hope to be 1-precise outside this region. (Although problems can occur even inside it...)
+ if (floatN > MAX_FLOAT_OR_INT || floatN < MIN_FLOAT_OR_INT) break checkIfWholeNumber;
+
+ int intN = num.intValue();
+ double diff = floatN - intN;
+ boolean exact; // We will try to ignore precision glitches (like 0.3 - 0.2 - 0.1 = -2.7E-17)
+ if (diff == 0) {
+ exact = true;
+ // We already reach ULP 7.6293945E-6 with bytes, so we don't continue with shorts.
+ } else if (intN >= Byte.MIN_VALUE && intN <= Byte.MAX_VALUE) {
+ if (diff > 0) {
+ if (diff < 0.00001) {
+ exact = false;
+ } else if (diff > 0.99999) {
+ exact = false;
+ intN++;
+ } else {
+ break checkIfWholeNumber;
+ }
+ } else { // => diff < 0
+ if (diff > -0.00001) {
+ exact = false;
+ } else if (diff < -0.99999) {
+ exact = false;
+ intN--;
+ } else {
+ break checkIfWholeNumber;
+ }
+ }
+ } else {
+ break checkIfWholeNumber;
+ }
+
+ // If we reach this, it can be treated as a whole number.
+
+ if ((targetNumTypes & FLAG_BYTE) != 0 && intN <= Byte.MAX_VALUE && intN >= Byte.MIN_VALUE) {
+ return new FloatOrByte((Float) num, (byte) intN);
+ } else if ((targetNumTypes & FLAG_SHORT) != 0 && intN <= Short.MAX_VALUE && intN >= Short.MIN_VALUE) {
+ return new FloatOrShort((Float) num, (short) intN);
+ } else if ((targetNumTypes & FLAG_INTEGER) != 0) {
+ return new FloatOrInteger((Float) num, intN);
+ } else if ((targetNumTypes & FLAG_LONG) != 0) {
+ // We can't even go outside the range of integers, so we don't need Long variation:
+ return exact
+ ? (Number) new FloatOrInteger((Float) num, intN)
+ : (Number) new FloatOrByte((Float) num, (byte) intN); // as !exact implies (-128..127)
+ }
+ // This point is reached if the float value was out of the range of target integer type(s).
+ // Falls through!
+ } while (false);
+ // If we reach this that means that it can't be treated as a whole number. So it's simply a Float:
+ return num;
+ } else if (numClass == Byte.class) {
+ return num;
+ } else if (numClass == Short.class) {
+ short pn = num.shortValue();
+ if ((targetNumTypes & FLAG_BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+ return new ShortOrByte((Short) num, (byte) pn);
+ } else {
+ return num;
+ }
+ } else if (numClass == BigInteger.class) {
+ if ((targetNumTypes
+ & ((MASK_KNOWN_INTEGERS | MASK_KNOWN_NONINTEGERS) ^ (FLAG_BIG_INTEGER | FLAG_BIG_DECIMAL))) != 0) {
+ BigInteger biNum = (BigInteger) num;
+ final int bitLength = biNum.bitLength(); // Doesn't include sign bit, so it's one less than expected
+ if ((targetNumTypes & FLAG_BYTE) != 0 && bitLength <= 7) {
+ return new BigIntegerOrByte(biNum);
+ } else if ((targetNumTypes & FLAG_SHORT) != 0 && bitLength <= 15) {
+ return new BigIntegerOrShort(biNum);
+ } else if ((targetNumTypes & FLAG_INTEGER) != 0 && bitLength <= 31) {
+ return new BigIntegerOrInteger(biNum);
+ } else if ((targetNumTypes & FLAG_LONG) != 0 && bitLength <= 63) {
+ return new BigIntegerOrLong(biNum);
+ } else if ((targetNumTypes & FLAG_FLOAT) != 0
+ && (bitLength <= MAX_FLOAT_OR_INT_LOG_2
+ || bitLength == MAX_FLOAT_OR_INT_LOG_2 + 1
+ && biNum.getLowestSetBit() >= MAX_FLOAT_OR_INT_LOG_2)) {
+ return new BigIntegerOrFloat(biNum);
+ } else if ((targetNumTypes & FLAG_DOUBLE) != 0
+ && (bitLength <= MAX_DOUBLE_OR_LONG_LOG_2
+ || bitLength == MAX_DOUBLE_OR_LONG_LOG_2 + 1
+ && biNum.getLowestSetBit() >= MAX_DOUBLE_OR_LONG_LOG_2)) {
+ return new BigIntegerOrDouble(biNum);
+ } else {
+ return num;
+ }
+ } else {
+ // No relevant coercion target types; return the BigInteger as is:
+ return num;
+ }
+ } else {
+ // Unknown number type:
+ return num;
+ }
+ }
+
+ static interface ByteSource { Byte byteValue(); }
+ static interface ShortSource { Short shortValue(); }
+ static interface IntegerSource { Integer integerValue(); }
+ static interface LongSource { Long longValue(); }
+ static interface FloatSource { Float floatValue(); }
+ static interface DoubleSource { Double doubleValue(); }
+ static interface BigIntegerSource { BigInteger bigIntegerValue(); }
+ static interface BigDecimalSource { BigDecimal bigDecimalValue(); }
+
+ /**
+ * Superclass of "Or"-ed numerical types. With an example, a {@code int} 1 has the fallback type {@code byte}, as
+ * that's the smallest type that can store the value, so it can be represented as an {@link IntegerOrByte}.
+ * This is useful as overloaded method selection only examines the type of the arguments, not the value of them,
+ * but with "Or"-ed types we can encode this value-related information into the argument type, hence influencing the
+ * method selection.
+ */
+ abstract static class NumberWithFallbackType extends Number implements Comparable {
+
+ protected abstract Number getSourceNumber();
+
+ public int intValue() {
+ return getSourceNumber().intValue();
+ }
+
+ public long longValue() {
+ return getSourceNumber().longValue();
+ }
+
+ public float floatValue() {
+ return getSourceNumber().floatValue();
+ }
+
+ public double doubleValue() {
+ return getSourceNumber().doubleValue();
+ }
+
+ public byte byteValue() {
+ return getSourceNumber().byteValue();
+ }
+
+ public short shortValue() {
+ return getSourceNumber().shortValue();
+ }
+
+ public int hashCode() {
+ return getSourceNumber().hashCode();
+ }
+
+ public boolean equals(Object obj) {
+ if (obj != null && this.getClass() == obj.getClass()) {
+ return getSourceNumber().equals(((NumberWithFallbackType) obj).getSourceNumber());
+ } else {
+ return false;
+ }
+ }
+
+ public String toString() {
+ return getSourceNumber().toString();
+ }
+
+ // We have to implement this, so that if a potential matching method expects a Comparable, which is implemented
+ // by all the supported numerical types, the "Or" type will be a match.
+ public int compareTo(Object o) {
+ Number n = getSourceNumber();
+ if (n instanceof Comparable) {
+ return ((Comparable) n).compareTo(o);
+ } else {
+ throw new ClassCastException(n.getClass().getName() + " is not Comparable.");
+ }
+ }
+
+ }
+
+ /**
+ * Holds a {@link BigDecimal} that stores a whole number. When selecting a overloaded method, FreeMarker tries to
+ * associate {@link BigDecimal} values to parameters of types that can hold non-whole numbers, unless the
+ * {@link BigDecimal} is wrapped into this class, in which case it does the opposite. This mechanism is, however,
+ * too rough to prevent roll overs or magnitude losses. Those are not yet handled for backward compatibility (they
+ * were suppressed earlier too).
+ */
+ static final class IntegerBigDecimal extends NumberWithFallbackType {
+
+ private final BigDecimal n;
+
+ IntegerBigDecimal(BigDecimal n) {
+ this.n = n;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ public BigInteger bigIntegerValue() {
+ return n.toBigInteger();
+ }
+
+ }
+
+ static abstract class LongOrSmallerInteger extends NumberWithFallbackType {
+
+ private final Long n;
+
+ protected LongOrSmallerInteger(Long n) {
+ this.n = n;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ public long longValue() {
+ return n.longValue();
+ }
+
+ }
+
+ static class LongOrByte extends LongOrSmallerInteger {
+
+ private final byte w;
+
+ LongOrByte(Long n, byte w) {
+ super(n);
+ this.w = w;
+ }
+
+ public byte byteValue() {
+ return w;
+ }
+
+ }
+
+ static class LongOrShort extends LongOrSmallerInteger {
+
+ private final short w;
+
+ LongOrShort(Long n, short w) {
+ super(n);
+ this.w = w;
+ }
+
+ public short shortValue() {
+ return w;
+ }
+
+ }
+
+ static class LongOrInteger extends LongOrSmallerInteger {
+
+ private final int w;
+
+ LongOrInteger(Long n, int w) {
+ super(n);
+ this.w = w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ }
+
+ static abstract class IntegerOrSmallerInteger extends NumberWithFallbackType {
+
+ private final Integer n;
+
+ protected IntegerOrSmallerInteger(Integer n) {
+ this.n = n;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ public int intValue() {
+ return n.intValue();
+ }
+
+ }
+
+ static class IntegerOrByte extends IntegerOrSmallerInteger {
+
+ private final byte w;
+
+ IntegerOrByte(Integer n, byte w) {
+ super(n);
+ this.w = w;
+ }
+
+ public byte byteValue() {
+ return w;
+ }
+
+ }
+
+ static class IntegerOrShort extends IntegerOrSmallerInteger {
+
+ private final short w;
+
+ IntegerOrShort(Integer n, short w) {
+ super(n);
+ this.w = w;
+ }
+
+ public short shortValue() {
+ return w;
+ }
+
+ }
+
+ static class ShortOrByte extends NumberWithFallbackType {
+
+ private final Short n;
+ private final byte w;
+
+ protected ShortOrByte(Short n, byte w) {
+ this.n = n;
+ this.w = w;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ public short shortValue() {
+ return n.shortValue();
+ }
+
+ public byte byteValue() {
+ return w;
+ }
+
+ }
+
+ static abstract class DoubleOrWholeNumber extends NumberWithFallbackType {
+
+ private final Double n;
+
+ protected DoubleOrWholeNumber(Double n) {
+ this.n = n;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ public double doubleValue() {
+ return n.doubleValue();
+ }
+
+ }
+
+ static final class DoubleOrByte extends DoubleOrWholeNumber {
+
+ private final byte w;
+
+ DoubleOrByte(Double n, byte w) {
+ super(n);
+ this.w = w;
+ }
+
+ public byte byteValue() {
+ return w;
+ }
+
+ public short shortValue() {
+ return w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class DoubleOrShort extends DoubleOrWholeNumber {
+
+ private final short w;
+
+ DoubleOrShort(Double n, short w) {
+ super(n);
+ this.w = w;
+ }
+
+ public short shortValue() {
+ return w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class DoubleOrIntegerOrFloat extends DoubleOrWholeNumber {
+
+ private final int w;
+
+ DoubleOrIntegerOrFloat(Double n, int w) {
+ super(n);
+ this.w = w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class DoubleOrInteger extends DoubleOrWholeNumber {
+
+ private final int w;
+
+ DoubleOrInteger(Double n, int w) {
+ super(n);
+ this.w = w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class DoubleOrLong extends DoubleOrWholeNumber {
+
+ private final long w;
+
+ DoubleOrLong(Double n, long w) {
+ super(n);
+ this.w = w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class DoubleOrFloat extends NumberWithFallbackType {
+
+ private final Double n;
+
+ DoubleOrFloat(Double n) {
+ this.n = n;
+ }
+
+ public float floatValue() {
+ return n.floatValue();
+ }
+
+ public double doubleValue() {
+ return n.doubleValue();
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ }
+
+ static abstract class FloatOrWholeNumber extends NumberWithFallbackType {
+
+ private final Float n;
+
+ FloatOrWholeNumber(Float n) {
+ this.n = n;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ public float floatValue() {
+ return n.floatValue();
+ }
+
+ }
+
+ static final class FloatOrByte extends FloatOrWholeNumber {
+
+ private final byte w;
+
+ FloatOrByte(Float n, byte w) {
+ super(n);
+ this.w = w;
+ }
+
+ public byte byteValue() {
+ return w;
+ }
+
+ public short shortValue() {
+ return w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class FloatOrShort extends FloatOrWholeNumber {
+
+ private final short w;
+
+ FloatOrShort(Float n, short w) {
+ super(n);
+ this.w = w;
+ }
+
+ public short shortValue() {
+ return w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ static final class FloatOrInteger extends FloatOrWholeNumber {
+
+ private final int w;
+
+ FloatOrInteger(Float n, int w) {
+ super(n);
+ this.w = w;
+ }
+
+ public int intValue() {
+ return w;
+ }
+
+ public long longValue() {
+ return w;
+ }
+
+ }
+
+ abstract static class BigIntegerOrPrimitive extends NumberWithFallbackType {
+
+ protected final BigInteger n;
+
+ BigIntegerOrPrimitive(BigInteger n) {
+ this.n = n;
+ }
+
+ protected Number getSourceNumber() {
+ return n;
+ }
+
+ }
+
+ final static class BigIntegerOrByte extends BigIntegerOrPrimitive {
+
+ BigIntegerOrByte(BigInteger n) {
+ super(n);
+ }
+
+ }
+
+ final static class BigIntegerOrShort extends BigIntegerOrPrimitive {
+
+ BigIntegerOrShort(BigInteger n) {
+ super(n);
+ }
+
+ }
+
+ final static class BigIntegerOrInteger extends BigIntegerOrPrimitive {
+
+ BigIntegerOrInteger(BigInteger n) {
+ super(n);
+ }
+
+ }
+
+ final static class BigIntegerOrLong extends BigIntegerOrPrimitive {
+
+ BigIntegerOrLong(BigInteger n) {
+ super(n);
+ }
+
+ }
+
+ abstract static class BigIntegerOrFPPrimitive extends BigIntegerOrPrimitive {
+
+ BigIntegerOrFPPrimitive(BigInteger n) {
+ super(n);
+ }
+
+ /** Faster version of {@link BigDecimal#floatValue()}, utilizes that the number known to fit into a long. */
+ public float floatValue() {
+ return n.longValue();
+ }
+
+ /** Faster version of {@link BigDecimal#doubleValue()}, utilizes that the number known to fit into a long. */
+ public double doubleValue() {
+ return n.longValue();
+ }
+
+ }
+
+ final static class BigIntegerOrFloat extends BigIntegerOrFPPrimitive {
+
+ BigIntegerOrFloat(BigInteger n) {
+ super(n);
+ }
+
+ }
+
+ final static class BigIntegerOrDouble extends BigIntegerOrFPPrimitive {
+
+ BigIntegerOrDouble(BigInteger n) {
+ super(n);
+ }
+
+ }
+
+ /**
+ * Returns a non-negative number that indicates how much we want to avoid a given numerical type conversion. Since
+ * we only consider the types here, not the actual value, we always consider the worst case scenario. Like it will
+ * say that converting int to short is not allowed, although int 1 can be converted to byte without loss. To account
+ * for such situations, "Or"-ed types, like {@link IntegerOrByte} has to be used.
+ *
+ * @param fromC the non-primitive type of the argument (with other words, the actual type).
+ * Must be {@link Number} or its subclass. This is possibly an {@link NumberWithFallbackType} subclass.
+ * @param toC the <em>non-primitive</em> type of the target parameter (with other words, the format type).
+ * Must be a {@link Number} subclass, not {@link Number} itself.
+ * Must <em>not</em> be {@link NumberWithFallbackType} or its subclass.
+ *
+ * @return
+ * <p>The possible values are:
+ * <ul>
+ * <li>0: No conversion is needed
+ * <li>[0, 30000): Lossless conversion
+ * <li>[30000, 40000): Smaller precision loss in mantissa is possible.
+ * <li>[40000, 50000): Bigger precision loss in mantissa is possible.
+ * <li>{@link Integer#MAX_VALUE}: Conversion not allowed due to the possibility of magnitude loss or overflow</li>
+ * </ul>
+ *
+ * <p>At some places, we only care if the conversion is possible, i.e., whether the return value is
+ * {@link Integer#MAX_VALUE} or not. But when multiple overloaded methods have an argument type to which we
+ * could convert to, this number will influence which of those will be chosen.
+ */
+ static int getArgumentConversionPrice(Class fromC, Class toC) {
+ // DO NOT EDIT, generated code!
+ // See: src\main\misc\overloadedNumberRules\README.txt
+ if (toC == fromC) {
+ return 0;
+ } else if (toC == Integer.class) {
+ if (fromC == IntegerBigDecimal.class) return 31003;
+ else if (fromC == BigDecimal.class) return 41003;
+ else if (fromC == Long.class) return Integer.MAX_VALUE;
+ else if (fromC == Double.class) return Integer.MAX_VALUE;
+ else if (fromC == Float.class) return Integer.MAX_VALUE;
+ else if (fromC == Byte.class) return 10003;
+ else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrInteger.class) return 21003;
+ else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return 22003;
+ else if (fromC == DoubleOrInteger.class) return 22003;
+ else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+ else if (fromC == IntegerOrByte.class) return 0;
+ else if (fromC == DoubleOrByte.class) return 22003;
+ else if (fromC == LongOrByte.class) return 21003;
+ else if (fromC == Short.class) return 10003;
+ else if (fromC == LongOrShort.class) return 21003;
+ else if (fromC == ShortOrByte.class) return 10003;
+ else if (fromC == FloatOrInteger.class) return 21003;
+ else if (fromC == FloatOrByte.class) return 21003;
+ else if (fromC == FloatOrShort.class) return 21003;
+ else if (fromC == BigIntegerOrInteger.class) return 16003;
+ else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrByte.class) return 16003;
+ else if (fromC == IntegerOrShort.class) return 0;
+ else if (fromC == DoubleOrShort.class) return 22003;
+ else if (fromC == BigIntegerOrShort.class) return 16003;
+ else return Integer.MAX_VALUE;
+ } else if (toC == Long.class) {
+ if (fromC == Integer.class) return 10004;
+ else if (fromC == IntegerBigDecimal.class) return 31004;
+ else if (fromC == BigDecimal.class) return 41004;
+ else if (fromC == Double.class) return Integer.MAX_VALUE;
+ else if (fromC == Float.class) return Integer.MAX_VALUE;
+ else if (fromC == Byte.class) return 10004;
+ else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrInteger.class) return 0;
+ else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return 21004;
+ else if (fromC == DoubleOrInteger.class) return 21004;
+ else if (fromC == DoubleOrLong.class) return 21004;
+ else if (fromC == IntegerOrByte.class) return 10004;
+ else if (fromC == DoubleOrByte.class) return 21004;
+ else if (fromC == LongOrByte.class) return 0;
+ else if (fromC == Short.class) return 10004;
+ else if (fromC == LongOrShort.class) return 0;
+ else if (fromC == ShortOrByte.class) return 10004;
+ else if (fromC == FloatOrInteger.class) return 21004;
+ else if (fromC == FloatOrByte.class) return 21004;
+ else if (fromC == FloatOrShort.class) return 21004;
+ else if (fromC == BigIntegerOrInteger.class) return 15004;
+ else if (fromC == BigIntegerOrLong.class) return 15004;
+ else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrByte.class) return 15004;
+ else if (fromC == IntegerOrShort.class) return 10004;
+ else if (fromC == DoubleOrShort.class) return 21004;
+ else if (fromC == BigIntegerOrShort.class) return 15004;
+ else return Integer.MAX_VALUE;
+ } else if (toC == Double.class) {
+ if (fromC == Integer.class) return 20007;
+ else if (fromC == IntegerBigDecimal.class) return 32007;
+ else if (fromC == BigDecimal.class) return 32007;
+ else if (fromC == Long.class) return 30007;
+ else if (fromC == Float.class) return 10007;
+ else if (fromC == Byte.class) return 20007;
+ else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrInteger.class) return 21007;
+ else if (fromC == DoubleOrFloat.class) return 0;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return 0;
+ else if (fromC == DoubleOrInteger.class) return 0;
+ else if (fromC == DoubleOrLong.class) return 0;
+ else if (fromC == IntegerOrByte.class) return 20007;
+ else if (fromC == DoubleOrByte.class) return 0;
+ else if (fromC == LongOrByte.class) return 21007;
+ else if (fromC == Short.class) return 20007;
+ else if (fromC == LongOrShort.class) return 21007;
+ else if (fromC == ShortOrByte.class) return 20007;
+ else if (fromC == FloatOrInteger.class) return 10007;
+ else if (fromC == FloatOrByte.class) return 10007;
+ else if (fromC == FloatOrShort.class) return 10007;
+ else if (fromC == BigIntegerOrInteger.class) return 20007;
+ else if (fromC == BigIntegerOrLong.class) return 30007;
+ else if (fromC == BigIntegerOrDouble.class) return 20007;
+ else if (fromC == BigIntegerOrFloat.class) return 20007;
+ else if (fromC == BigIntegerOrByte.class) return 20007;
+ else if (fromC == IntegerOrShort.class) return 20007;
+ else if (fromC == DoubleOrShort.class) return 0;
+ else if (fromC == BigIntegerOrShort.class) return 20007;
+ else return Integer.MAX_VALUE;
+ } else if (toC == Float.class) {
+ if (fromC == Integer.class) return 30006;
+ else if (fromC == IntegerBigDecimal.class) return 33006;
+ else if (fromC == BigDecimal.class) return 33006;
+ else if (fromC == Long.class) return 40006;
+ else if (fromC == Double.class) return Integer.MAX_VALUE;
+ else if (fromC == Byte.class) return 20006;
+ else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrInteger.class) return 30006;
+ else if (fromC == DoubleOrFloat.class) return 30006;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return 23006;
+ else if (fromC == DoubleOrInteger.class) return 30006;
+ else if (fromC == DoubleOrLong.class) return 40006;
+ else if (fromC == IntegerOrByte.class) return 24006;
+ else if (fromC == DoubleOrByte.class) return 23006;
+ else if (fromC == LongOrByte.class) return 24006;
+ else if (fromC == Short.class) return 20006;
+ else if (fromC == LongOrShort.class) return 24006;
+ else if (fromC == ShortOrByte.class) return 20006;
+ else if (fromC == FloatOrInteger.class) return 0;
+ else if (fromC == FloatOrByte.class) return 0;
+ else if (fromC == FloatOrShort.class) return 0;
+ else if (fromC == BigIntegerOrInteger.class) return 30006;
+ else if (fromC == BigIntegerOrLong.class) return 40006;
+ else if (fromC == BigIntegerOrDouble.class) return 40006;
+ else if (fromC == BigIntegerOrFloat.class) return 24006;
+ else if (fromC == BigIntegerOrByte.class) return 24006;
+ else if (fromC == IntegerOrShort.class) return 24006;
+ else if (fromC == DoubleOrShort.class) return 23006;
+ else if (fromC == BigIntegerOrShort.class) return 24006;
+ else return Integer.MAX_VALUE;
+ } else if (toC == Byte.class) {
+ if (fromC == Integer.class) return Integer.MAX_VALUE;
+ else if (fromC == IntegerBigDecimal.class) return 35001;
+ else if (fromC == BigDecimal.class) return 45001;
+ else if (fromC == Long.class) return Integer.MAX_VALUE;
+ else if (fromC == Double.class) return Integer.MAX_VALUE;
+ else if (fromC == Float.class) return Integer.MAX_VALUE;
+ else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+ else if (fromC == IntegerOrByte.class) return 22001;
+ else if (fromC == DoubleOrByte.class) return 25001;
+ else if (fromC == LongOrByte.class) return 23001;
+ else if (fromC == Short.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrShort.class) return Integer.MAX_VALUE;
+ else if (fromC == ShortOrByte.class) return 21001;
+ else if (fromC == FloatOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == FloatOrByte.class) return 23001;
+ else if (fromC == FloatOrShort.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrByte.class) return 18001;
+ else if (fromC == IntegerOrShort.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrShort.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrShort.class) return Integer.MAX_VALUE;
+ else return Integer.MAX_VALUE;
+ } else if (toC == Short.class) {
+ if (fromC == Integer.class) return Integer.MAX_VALUE;
+ else if (fromC == IntegerBigDecimal.class) return 34002;
+ else if (fromC == BigDecimal.class) return 44002;
+ else if (fromC == Long.class) return Integer.MAX_VALUE;
+ else if (fromC == Double.class) return Integer.MAX_VALUE;
+ else if (fromC == Float.class) return Integer.MAX_VALUE;
+ else if (fromC == Byte.class) return 10002;
+ else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == LongOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+ else if (fromC == IntegerOrByte.class) return 21002;
+ else if (fromC == DoubleOrByte.class) return 24002;
+ else if (fromC == LongOrByte.class) return 22002;
+ else if (fromC == LongOrShort.class) return 22002;
+ else if (fromC == ShortOrByte.class) return 0;
+ else if (fromC == FloatOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == FloatOrByte.class) return 22002;
+ else if (fromC == FloatOrShort.class) return 22002;
+ else if (fromC == BigIntegerOrInteger.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == BigIntegerOrByte.class) return 17002;
+ else if (fromC == IntegerOrShort.class) return 21002;
+ else if (fromC == DoubleOrShort.class) return 24002;
+ else if (fromC == BigIntegerOrShort.class) return 17002;
+ else return Integer.MAX_VALUE;
+ } else if (toC == BigDecimal.class) {
+ if (fromC == Integer.class) return 20008;
+ else if (fromC == IntegerBigDecimal.class) return 0;
+ else if (fromC == Long.class) return 20008;
+ else if (fromC == Double.class) return 20008;
+ else if (fromC == Float.class) return 20008;
+ else if (fromC == Byte.class) return 20008;
+ else if (fromC == BigInteger.class) return 10008;
+ else if (fromC == LongOrInteger.class) return 20008;
+ else if (fromC == DoubleOrFloat.class) return 20008;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return 20008;
+ else if (fromC == DoubleOrInteger.class) return 20008;
+ else if (fromC == DoubleOrLong.class) return 20008;
+ else if (fromC == IntegerOrByte.class) return 20008;
+ else if (fromC == DoubleOrByte.class) return 20008;
+ else if (fromC == LongOrByte.class) return 20008;
+ else if (fromC == Short.class) return 20008;
+ else if (fromC == LongOrShort.class) return 20008;
+ else if (fromC == ShortOrByte.class) return 20008;
+ else if (fromC == FloatOrInteger.class) return 20008;
+ else if (fromC == FloatOrByte.class) return 20008;
+ else if (fromC == FloatOrShort.class) return 20008;
+ else if (fromC == BigIntegerOrInteger.class) return 10008;
+ else if (fromC == BigIntegerOrLong.class) return 10008;
+ else if (fromC == BigIntegerOrDouble.class) return 10008;
+ else if (fromC == BigIntegerOrFloat.class) return 10008;
+ else if (fromC == BigIntegerOrByte.class) return 10008;
+ else if (fromC == IntegerOrShort.class) return 20008;
+ else if (fromC == DoubleOrShort.class) return 20008;
+ else if (fromC == BigIntegerOrShort.class) return 10008;
+ else return Integer.MAX_VALUE;
+ } else if (toC == BigInteger.class) {
+ if (fromC == Integer.class) return 10005;
+ else if (fromC == IntegerBigDecimal.class) return 10005;
+ else if (fromC == BigDecimal.class) return 40005;
+ else if (fromC == Long.class) return 10005;
+ else if (fromC == Double.class) return Integer.MAX_VALUE;
+ else if (fromC == Float.class) return Integer.MAX_VALUE;
+ else if (fromC == Byte.class) return 10005;
+ else if (fromC == LongOrInteger.class) return 10005;
+ else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+ else if (fromC == DoubleOrIntegerOrFloat.class) return 21005;
+ else if (fromC == DoubleOrInteger.class) return 21005;
+ else if (fromC == DoubleOrLong.class) return 21005;
+ else if (fromC == IntegerOrByte.class) return 10005;
+ else if (fromC == DoubleOrByte.class) return 21005;
+ else if (fromC == LongOrByte.class) return 10005;
+ else if (fromC == Short.class) return 10005;
+ else if (fromC == LongOrShort.class) return 10005;
+ else if (fromC == ShortOrByte.class) return 10005;
+ else if (fromC == FloatOrInteger.class) return 25005;
+ else if (fromC == FloatOrByte.class) return 25005;
+ else if (fromC == FloatOrShort.class) return 25005;
+ else if (fromC == BigIntegerOrInteger.class) return 0;
+ else if (fromC == BigIntegerOrLong.class) return 0;
+ else if (fromC == BigIntegerOrDouble.class) return 0;
+ else if (fromC == BigIntegerOrFloat.class) return 0;
+ else if (fromC == BigIntegerOrByte.class) return 0;
+ else if (fromC == IntegerOrShort.class) return 10005;
+ else if (fromC == DoubleOrShort.class) return 21005;
+ else if (fromC == BigIntegerOrShort.class) return 0;
+ else return Integer.MAX_VALUE;
+ } else {
+ // Unknown toC; we don't know how to convert to it:
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ static int compareNumberTypeSpecificity(Class c1, Class c2) {
+ // DO NOT EDIT, generated code!
+ // See: src\main\misc\overloadedNumberRules\README.txt
+ c1 = ClassUtil.primitiveClassToBoxingClass(c1);
+ c2 = ClassUtil.primitiveClassToBoxingClass(c2);
+
+ if (c1 == c2) return 0;
+
+ if (c1 == Integer.class) {
+ if (c2 == Long.class) return 4 - 3;
+ if (c2 == Double.class) return 7 - 3;
+ if (c2 == Float.class) return 6 - 3;
+ if (c2 == Byte.class) return 1 - 3;
+ if (c2 == Short.class) return 2 - 3;
+ if (c2 == BigDecimal.class) return 8 - 3;
+ if (c2 == BigInteger.class) return 5 - 3;
+ return 0;
+ }
+ if (c1 == Long.class) {
+ if (c2 == Integer.class) return 3 - 4;
+ if (c2 == Double.class) return 7 - 4;
+ if (c2 == Float.class) return 6 - 4;
+ if (c2 == Byte.class) return 1 - 4;
+ if (c2 == Short.class) return 2 - 4;
+ if (c2 == BigDecimal.class) return 8 - 4;
+ if (c2 == BigInteger.class) return 5 - 4;
+ return 0;
+ }
+ if (c1 == Double.class) {
+ if (c2 == Integer.class) return 3 - 7;
+ if (c2 == Long.class) return 4 - 7;
+ if (c2 == Float.class) return 6 - 7;
+ if (c2 == Byte.class) return 1 - 7;
+ if (c2 == Short.class) return 2 - 7;
+ if (c2 == BigDecimal.class) return 8 - 7;
+ if (c2 == BigInteger.class) return 5 - 7;
+ return 0;
+ }
+ if (c1 == Float.class) {
+ if (c2 == Integer.class) return 3 - 6;
+ if (c2 == Long.class) return 4 - 6;
+ if (c2 == Double.class) return 7 - 6;
+ if (c2 == Byte.class) return 1 - 6;
+ if (c2 == Short.class) return 2 - 6;
+ if (c2 == BigDecimal.class) return 8 - 6;
+ if (c2 == BigInteger.class) return 5 - 6;
+ return 0;
+ }
+ if (c1 == Byte.class) {
+ if (c2 == Integer.class) return 3 - 1;
+ if (c2 == Long.class) return 4 - 1;
+ if (c2 == Double.class) return 7 - 1;
+ if (c2 == Float.class) return 6 - 1;
+ if (c2 == Short.class) return 2 - 1;
+ if (c2 == BigDecimal.class) return 8 - 1;
+ if (c2 == BigInteger.class) return 5 - 1;
+ return 0;
+ }
+ if (c1 == Short.class) {
+ if (c2 == Integer.class) return 3 - 2;
+ if (c2 == Long.class) return 4 - 2;
+ if (c2 == Double.class) return 7 - 2;
+ if (c2 == Float.class) return 6 - 2;
+ if (c2 == Byte.class) return 1 - 2;
+ if (c2 == BigDecimal.class) return 8 - 2;
+ if (c2 == BigInteger.class) return 5 - 2;
+ return 0;
+ }
+ if (c1 == BigDecimal.class) {
+ if (c2 == Integer.class) return 3 - 8;
+ if (c2 == Long.class) return 4 - 8;
+ if (c2 == Double.class) return 7 - 8;
+ if (c2 == Float.class) return 6 - 8;
+ if (c2 == Byte.class) return 1 - 8;
+ if (c2 == Short.class) return 2 - 8;
+ if (c2 == BigInteger.class) return 5 - 8;
+ return 0;
+ }
+ if (c1 == BigInteger.class) {
+ if (c2 == Integer.class) return 3 - 5;
+ if (c2 == Long.class) return 4 - 5;
+ if (c2 == Double.class) return 7 - 5;
+ if (c2 == Float.class) return 6 - 5;
+ if (c2 == Byte.class) return 1 - 5;
+ if (c2 == Short.class) return 2 - 5;
+ if (c2 == BigDecimal.class) return 8 - 5;
+ return 0;
+ }
+ return 0;
+ }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java b/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java
index 3b49a5d..ce566bb 100644
--- a/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java
+++ b/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java
@@ -93,7 +93,7 @@
System.arraycopy(args, 0, packedArgs, 0, fixArgCount);
Object varargs = Array.newInstance(varArgsCompType, totalArgCount - fixArgCount);
for(int i = fixArgCount; i < totalArgCount; ++i) {
- Object val = w.unwrapInternal((TemplateModel)modelArgs.get(i), varArgsCompType);
+ Object val = w.tryUnwrap((TemplateModel)modelArgs.get(i), varArgsCompType);
if(val == BeansWrapper.CAN_NOT_UNWRAP) {
return null;
}
@@ -103,7 +103,7 @@
return packedArgs;
}
else {
- Object val = w.unwrapInternal((TemplateModel)modelArgs.get(fixArgCount), varArgsCompType);
+ Object val = w.tryUnwrap((TemplateModel)modelArgs.get(fixArgCount), varArgsCompType);
if(val == BeansWrapper.CAN_NOT_UNWRAP) {
return null;
}
@@ -142,73 +142,79 @@
else {
argPacker = canonical;
}
- }
argPackers.put(memberDesc.member, argPacker);
+ }
return preprocessedParamTypes;
}
- void afterWideningUnwrappingHints(Class[] paramTypes) {
- final int paramCount = paramTypes.length;
- final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
- final Class[] hints = unwrappingHintsByParamCount[paramCount];
-
+ void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
// Overview
// --------
//
// So far, m(t1, t2...) was treated by the hint widening like m(t1, t2). So now we have to continue hint
// widening like if we had further methods:
- // - m(t1), because a vararg array can be 0 long
- // - m(t1, t2), m(t1, t2, t2), , m(t1, t2, t2, t2), ...
+ // - m(t1, t2, t2), m(t1, t2, t2, t2), ...
+ // - m(t1), because a varargs array can be 0 long
//
// But we can't do that for real, because we had to add infinite number of methods. Also, for efficiency we
// don't want to create unwrappingHintsByParamCount entries at the indices which are still unused.
// So we only update the already existing hints. Remember that we already have m(t1, t2) there.
- // The case of m(t1, t2), m(t1, t2, t2), , m(t1, t2, t2, t2), ..., where m is an *earlier* added method.
+ final int paramCount = paramTypes.length;
+ final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
+
+ // The case of e(t1, t2), e(t1, t2, t2), e(t1, t2, t2, t2), ..., where e is an *earlier* added method.
// When that was added, this method wasn't added yet, so it had no chance updating the hints of this method,
// so we do that now:
// FIXME: Only needed if m(t1, t2) was filled an empty slot, otherwise whatever was there was already
// widened by the preceding hints, so this will be a no-op.
for(int i = paramCount - 1; i >= 0; i--) {
- Class[] previousHints = unwrappingHintsByParamCount[i];
+ final Class[] previousHints = unwrappingHintsByParamCount[i];
if(previousHints != null) {
- widenToCommonSupertypesForUnwrappingHint(hints, previousHints);
- break;
+ widenHintsToCommonSupertypes(
+ paramCount,
+ previousHints, getPossibleNumericalTypes(i));
+ break; // we only do this for the first hit, as the methods before that has already widened it.
}
}
- // The case of m(t1), where m is an *earlier* added method.
+ // The case of e(t1), where e is an *earlier* added method.
// When that was added, this method wasn't added yet, so it had no chance updating the hints of this method,
// so we do that now:
// FIXME: Same as above; it's often unnecessary.
if(paramCount + 1 < unwrappingHintsByParamCount.length) {
Class[] oneLongerHints = unwrappingHintsByParamCount[paramCount + 1];
if(oneLongerHints != null) {
- widenToCommonSupertypesForUnwrappingHint(hints, oneLongerHints);
+ widenHintsToCommonSupertypes(
+ paramCount,
+ oneLongerHints, getPossibleNumericalTypes(paramCount + 1));
}
}
- // The case of m(t1, t2), m(t1, t2, t2), , m(t1, t2, t2, t2), ..., where m is the currently added method.
+ // The case of m(t1, t2, t2), m(t1, t2, t2, t2), ..., where m is the currently added method.
// Update the longer hints-arrays:
- // FIXME Shouldn't these use the original hint array instead of the widened one?
- for(int i = paramCount + 1; i < unwrappingHintsByParamCount.length; ++i) {
- Class[] longerHints = unwrappingHintsByParamCount[i];
- if(longerHints != null) {
- widenToCommonSupertypesForUnwrappingHint(longerHints, paramTypes);
- }
+ for(int i = paramCount + 1; i < unwrappingHintsByParamCount.length; i++) {
+ widenHintsToCommonSupertypes(
+ i,
+ paramTypes, paramNumericalTypes);
}
// The case of m(t1) where m is the currently added method.
// update the one-shorter hints-array:
if(paramCount > 0) { // (should be always true, or else it wasn't a varags method)
- Class[] oneShorterUnwrappingHints = unwrappingHintsByParamCount[paramCount - 1];
- if(oneShorterUnwrappingHints != null) {
- widenToCommonSupertypesForUnwrappingHint(oneShorterUnwrappingHints, paramTypes);
- }
+ widenHintsToCommonSupertypes(
+ paramCount - 1,
+ paramTypes, paramNumericalTypes);
}
}
- private void widenToCommonSupertypesForUnwrappingHint(Class[] typesToWiden, Class[] wideningTypes) {
+ private void widenHintsToCommonSupertypes(
+ int paramCountOfWidened, Class[] wideningTypes, int[] wideningNumTypes) {
+ final Class[] typesToWiden = getUnwrappingHintsByParamCount()[paramCountOfWidened];
+ if (typesToWiden == null) {
+ return; // no such overload exists; nothing to widen
+ }
+
final int typesToWidenLen = typesToWiden.length;
final int wideningTypesLen = wideningTypes.length;
int min = Math.min(wideningTypesLen, typesToWidenLen);
@@ -221,6 +227,10 @@
typesToWiden[i] = getCommonSupertypeForUnwrappingHint(typesToWiden[i], varargsComponentType);
}
}
+
+ if (bugfixed) {
+ mergeInNumericalTypes(paramCountOfWidened, wideningNumTypes);
+ }
}
MaybeEmptyMemberAndArguments getMemberAndArguments(List tmArgs, BeansWrapper w)
@@ -229,30 +239,39 @@
// null is treated as empty args
tmArgs = Collections.EMPTY_LIST;
}
- int l = tmArgs.size();
+ int argsLen = tmArgs.size();
Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
- Object[] pojoArgs = new Object[l];
+ Object[] pojoArgs = new Object[argsLen];
+ int[] possibleNumericalTypes = null;
// Starting from args.length + 1 as we must try to match against a case
// where all specified args are fixargs, and the vararg portion
// contains zero args
- outer: for(int j = Math.min(l + 1, unwrappingHintsByParamCount.length - 1); j >= 0; --j) {
- Class[] unwarappingArgTypes = unwrappingHintsByParamCount[j];
- if(unwarappingArgTypes == null) {
- if(j == 0) {
+ outer: for(int paramCount = Math.min(argsLen + 1, unwrappingHintsByParamCount.length - 1); paramCount >= 0; --paramCount) {
+ Class[] unwarappingHints = unwrappingHintsByParamCount[paramCount];
+ if(unwarappingHints == null) {
+ if (paramCount == 0) {
return EmptyMemberAndArguments.NO_SUCH_METHOD;
}
continue;
}
- // Try to marshal the arguments
+
+ possibleNumericalTypes = getPossibleNumericalTypes(paramCount);
+ if (possibleNumericalTypes == ALL_ZEROS_ARRAY) {
+ possibleNumericalTypes = null;
+ }
+
+ // Try to unwrap the arguments
Iterator it = tmArgs.iterator();
- for(int i = 0; i < l; ++i) {
- Object pojo = w.unwrapInternal((TemplateModel)it.next(), i < j ? unwarappingArgTypes[i] : unwarappingArgTypes[j - 1]);
+ for(int i = 0; i < argsLen; ++i) {
+ int paramIdx = i < paramCount ? i : paramCount - 1;
+ Object pojo = w.tryUnwrap(
+ (TemplateModel)it.next(),
+ unwarappingHints[paramIdx],
+ possibleNumericalTypes != null ? possibleNumericalTypes[paramIdx] : 0);
if(pojo == BeansWrapper.CAN_NOT_UNWRAP) {
continue outer;
}
- if(pojo != pojoArgs[i]) {
- pojoArgs[i] = pojo;
- }
+ pojoArgs[i] = pojo;
}
break;
}
@@ -260,11 +279,20 @@
MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, true);
if(maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
- pojoArgs = ((ArgumentPacker)argPackers.get(memberDesc.member)).packArgs(pojoArgs, tmArgs, w);
+ pojoArgs = ((ArgumentPacker) argPackers.get(memberDesc.member)).packArgs(pojoArgs, tmArgs, w);
if(pojoArgs == null) {
return EmptyMemberAndArguments.NO_SUCH_METHOD;
}
- BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+ if (bugfixed) {
+ if (possibleNumericalTypes != null) {
+ // Note that overloaded method selection has already accounted for overflow errors when the method
+ // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+ // BigDecimal is allowed to overflow for backward-compatibility.
+ forceNumberArgumentsToParameterTypes(pojoArgs, memberDesc.paramTypes, possibleNumericalTypes);
+ }
+ } else {
+ BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+ }
return new MemberAndArguments(memberDesc.member, pojoArgs);
} else {
return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc); // either NOT_FOUND or AMBIGUOUS
diff --git a/src/main/java/freemarker/ext/beans/SimpleMemberModel.java b/src/main/java/freemarker/ext/beans/SimpleMemberModel.java
index b5696ee..5903847 100644
--- a/src/main/java/freemarker/ext/beans/SimpleMemberModel.java
+++ b/src/main/java/freemarker/ext/beans/SimpleMemberModel.java
@@ -119,7 +119,7 @@
while (argIdx < normalArgCnt) {
Class argType = argTypes[argIdx];
TemplateModel argVal = (TemplateModel) it.next();
- Object unwrappedArgVal = w.unwrapInternal(argVal, argType);
+ Object unwrappedArgVal = w.tryUnwrap(argVal, argType);
if(unwrappedArgVal == BeansWrapper.CAN_NOT_UNWRAP) {
throw createArgumentTypeMismarchException(argIdx, argVal, argType);
}
@@ -143,7 +143,7 @@
// We first try to treat the last argument as a vararg *array*.
// This is consistent to what OverloadedVarArgMethod does.
if (argsLen - argIdx == 1
- && (unwrappedArgVal = w.unwrapInternal(argVal, varargType))
+ && (unwrappedArgVal = w.tryUnwrap(argVal, varargType))
!= BeansWrapper.CAN_NOT_UNWRAP) {
// It was a vararg array.
unwrappedArgs[argIdx++] = unwrappedArgVal;
@@ -154,7 +154,7 @@
Object varargArray = Array.newInstance(varargItemType, varargArrayLen);
for (int varargIdx = 0; varargIdx < varargArrayLen; varargIdx++) {
TemplateModel varargVal = (TemplateModel) (varargIdx == 0 ? argVal : it.next());
- Object unwrappedVarargVal = w.unwrapInternal(varargVal, varargItemType);
+ Object unwrappedVarargVal = w.tryUnwrap(varargVal, varargItemType);
if(unwrappedVarargVal == BeansWrapper.CAN_NOT_UNWRAP) {
throw createArgumentTypeMismarchException(
argIdx + varargIdx, varargVal, varargItemType);
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index b7d9cb6..cd5deee 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -391,5 +391,17 @@
if (boxingClass == Void.class) return void.class; // not really a primitive, but we normalize to it
return boxingClass;
}
+
+ /**
+ * Tells if a type is numerical; works both for primitive types and classes.
+ *
+ * @param type can't be {@code null}
+ *
+ * @since. 2.3.21
+ */
+ public static boolean isNumerical(Class type) {
+ return Number.class.isAssignableFrom(type)
+ || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE && type != Void.TYPE;
+ }
}
diff --git a/src/main/java/freemarker/template/utility/NumberUtil.java b/src/main/java/freemarker/template/utility/NumberUtil.java
index 0d80cc0..0f9bd64 100644
--- a/src/main/java/freemarker/template/utility/NumberUtil.java
+++ b/src/main/java/freemarker/template/utility/NumberUtil.java
@@ -76,6 +76,18 @@
}
}
+ /**
+ * Tells if a {@link BigDecimal} stores a whole number. For example, it returns {@code true} for {@code 1.0000},
+ * but {@code false} for {@code 1.0001}.
+ * @since 2.3.21
+ */
+ static public boolean isBigDecimalInteger(BigDecimal bd) {
+ return bd.scale() <= 0 // A fast check that whole numbers usually (not always) match
+ || bd.setScale(0, BigDecimal.ROUND_DOWN).compareTo(bd) == 0; // This is rather slow
+ // Note that `bd.signum() == 0 || bd.stripTrailingZeros().scale() <= 0` was also tried for the last
+ // condition, but stripTrailingZeros was slower than setScale + compareTo.
+ }
+
private static boolean isNonFPNumberOfSupportedClass(Number num) {
return num instanceof Integer || num instanceof BigDecimal || num instanceof Long
|| num instanceof Short || num instanceof Byte || num instanceof BigInteger;
diff --git a/src/main/misc/overloadedNumberRules/README.txt b/src/main/misc/overloadedNumberRules/README.txt
new file mode 100644
index 0000000..20a40e7
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/README.txt
@@ -0,0 +1,12 @@
+This FMPP project is used for generating the source code of some
+`freemarker.ext.beans.OverloadedNumberUtil` methods based on the
+content of `prices.ods` (LibreOffice spreadsheet).
+
+Usage:
+1. Edit `prices.ods`
+3. If you have introduced new types in it, also update `toCsFreqSorted` and
+ `toCsCostBoosts` and `toCsContCosts` in `config.fmpp`.
+4. Save it into `prices.csv` (use comma as field separator)
+5. Run FMPP from this directory. It will generate
+ `<freemarkerProjectDir>/build/getArgumentConversionPrice.java`.
+6. Copy-pase its content into `OverloadedNumberUtil.java`.
\ No newline at end of file
diff --git a/src/main/misc/overloadedNumberRules/config.fmpp b/src/main/misc/overloadedNumberRules/config.fmpp
new file mode 100644
index 0000000..1017c30
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/config.fmpp
@@ -0,0 +1,56 @@
+sources: generator.ftl
+outputFile: ../../../../build/overloadedNumberRules.java
+data: {
+ t: csv(prices.csv, { separator: ',' })
+
+ # Conversion target types sorted by decreasing probablity of occurence
+ toCsFreqSorted: [ Integer, Long, Double, Float, Byte, Short, BigDecimal, BigInteger ]
+
+ # Conversion target types associated to conversion price boost. The prices from the spreadsheet
+ # will be multipied by 10000 and the boost will be added to it. Thus, if the price of two possible
+ # targets are the same according the spreadsheet (and only then!), the choice will depend on
+ # this boost.
+ # The more specific the (the smaller) type is, the lower the boost should be. This is improtant,
+ # because this number is also used for comparing the specificity of numerical types where
+ # there's no argument type available.
+ # Note where the price from the spreadsheet is 0 or "-" or "N/A", the boost is not used.
+ toCsCostBoosts: {
+ 'Byte': 1, 'Short': 2, 'Integer': 3, 'Long': 4, 'BigInteger': 5,
+ 'Float': 6, 'Double': 7,
+ 'BigDecimal': 8
+ }
+
+ # Conversion source types sorted by decreasing probablity of occurence
+ fromCsFreqSorted: [
+ Integer,
+ IntegerBigDecimal,
+ BigDecimal,
+ Long,
+ Double,
+ Float,
+ Byte,
+ BigInteger,
+ LongOrInteger
+ DoubleOrFloat,
+ DoubleOrIntegerOrFloat,
+ DoubleOrInteger,
+ DoubleOrLong,
+ IntegerOrByte,
+ DoubleOrByte,
+ LongOrByte
+ Short,
+ LongOrShort
+ ShortOrByte
+ FloatOrInteger,
+ FloatOrByte,
+ FloatOrShort,
+ BigIntegerOrInteger,
+ BigIntegerOrLong,
+ BigIntegerOrDouble,
+ BigIntegerOrFloat,
+ BigIntegerOrByte,
+ IntegerOrShort,
+ DoubleOrShort,
+ BigIntegerOrShort,
+ ]
+}
\ No newline at end of file
diff --git a/src/main/misc/overloadedNumberRules/generator.ftl b/src/main/misc/overloadedNumberRules/generator.ftl
new file mode 100644
index 0000000..bbd7463
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/generator.ftl
@@ -0,0 +1,62 @@
+ static int getArgumentConversionPrice(Class fromC, Class toC) {
+ // DO NOT EDIT, generated code!
+ // See: src\main\misc\overloadedNumberRules\README.txt
+ if (toC == fromC) {
+ return 0;
+ <#list toCsFreqSorted as toC><#t>
+ } else if (toC == ${toC}.class) {
+ <#assign firstFromC = true>
+ <#list fromCsFreqSorted as fromC>
+ <#if toC != fromC>
+ <#assign row = []>
+ <#list t as i>
+ <#if i[0] == fromC>
+ <#assign row = i>
+ <#break>
+ </#if>
+ </#list>
+ <#if !row?has_content><#stop "Not found: " + fromC></#if>
+ <#if !firstFromC>else </#if>if (fromC == ${fromC}.class) return ${toPrice(row[toC], toCsCostBoosts[toC])};
+ <#assign firstFromC = false>
+ </#if>
+ </#list>
+ else return Integer.MAX_VALUE;
+ </#list>
+ } else {
+ // Unknown toC; we don't know how to convert to it:
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ static int compareNumberTypeSpecificity(Class c1, Class c2) {
+ // DO NOT EDIT, generated code!
+ // See: src\main\misc\overloadedNumberRules\README.txt
+ c1 = ClassUtil.primitiveClassToBoxingClass(c1);
+ c2 = ClassUtil.primitiveClassToBoxingClass(c2);
+
+ if (c1 == c2) return 0;
+
+ <#list toCsFreqSorted as c1><#t>
+ if (c1 == ${c1}.class) {
+ <#list toCsFreqSorted as c2><#if c1 != c2><#t>
+ if (c2 == ${c2}.class) return ${toCsCostBoosts[c2]} - ${toCsCostBoosts[c1]};
+ </#if></#list>
+ return 0;
+ }
+ </#list>
+ return 0;
+ }
+
+<#function toPrice cellValue, boost>
+ <#if cellValue?starts_with("BC ")>
+ <#local cellValue = cellValue[3..]>
+ <#elseif cellValue == '-' || cellValue == 'N/A'>
+ <#return 'Integer.MAX_VALUE'>
+ </#if>
+ <#local cellValue = cellValue?number>
+ <#if cellValue != 0>
+ <#return cellValue * 10000 + boost>
+ <#else>
+ <#return 0>
+ </#if>
+</#function>
diff --git a/src/main/misc/overloadedNumberRules/prices.csv b/src/main/misc/overloadedNumberRules/prices.csv
new file mode 100644
index 0000000..c12d7ba
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/prices.csv
@@ -0,0 +1,48 @@
+,Byte,Short,Integer,Long,BigInteger,Float,Double,BigDecimal,,Note
+Byte,0,1,1,1,1,2,2,2,,
+ShortOrByte,2.1,0,1,1,1,2,2,2,,
+Short,-,0,1,1,1,2,2,2,,
+IntegerOrByte,2.2,2.1,0,1,1,2.4,2,2,,
+IntegerOrShort,-,2.1,0,1,1,2.4,2,2,,
+Integer,-,-,0,1,1,3,2,2,,
+LongOrByte,2.3,2.2,2.1,0,1,2.4,2.1,2,,
+LongOrShort,-,2.2,2.1,0,1,2.4,2.1,2,,
+LongOrInteger,-,-,2.1,0,1,3,2.1,2,,
+Long,-,-,-,0,1,4,3,2,,
+BigIntegerOrByte,1.8,1.7,1.6,1.5,0,2.4,2,1,,
+BigIntegerOrShort,-,1.7,1.6,1.5,0,2.4,2,1,,
+BigIntegerOrInteger,-,-,1.6,1.5,0,3,2,1,,
+BigIntegerOrLong,-,-,-,1.5,0,4,3,1,,
+BigIntegerOrFloat,N/A,N/A,N/A,N/A,0,2.4,2,1,,"Condition: No whole-number target types exist, and abs(value) <= 2**24"
+BigIntegerOrDouble,N/A,N/A,N/A,N/A,0,4,2,1,,"Condition: No whole-number target types exist, and abs(value) <= 2**53"
+BigInteger,-,-,-,-,0,-,-,1,,
+FloatOrByte,2.3,2.2,2.1,2.1,2.5,0,1,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+FloatOrShort,-,2.2,2.1,2.1,2.5,0,1,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+FloatOrInteger,-,-,2.1,2.1,2.5,0,1,2,,"Condition: fraction == 0, in [-16777216 .. 16777216]"
+Float,-,-,-,-,-,0,1,2,,
+DoubleOrByte,2.5,2.4,2.2,2.1,2.1,2.3,0,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+DoubleOrShort,-,2.4,2.2,2.1,2.1,2.3,0,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+DoubleOrIntegerOrFloat,-,-,2.2,2.1,2.1,2.3,0,2,,"Condition: fraction == 0, in [-16777216 .. 16777216]"
+DoubleOrInteger,-,-,2.2,2.1,2.1,3,0,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+DoubleOrLong,-,-,-,2.1,2.1,4,0,2,,"Condition: fraction == 0, in [-9007199254740992..9007199254740992]"
+DoubleOrFloat,-,-,-,-,-,3,0,2,,Condition: no magnitude loss
+Double,-,-,-,-,-,-,0,2,,
+IntegerBigDecimal,3.5,3.4,3.1,3.1,1,3.3,3.2,0,,Condition: fraction == 0; for BC we convert rather “bravely” here.
+BigDecimal,4.5,4.4,4.1,4.1,4,3.3,3.2,0,,Condition: fraction != 0; for BC we convert rather “bravely” here.
+,,,,,,,,,,
+,Legend:,,,,,,,,,
+,,"The numbers are the „price” of the conversion, and they tell how much we try to avoid it (when selecting among overloaded methods):",,,,,,,,
+,,,,,,,,,,
+,,0,No conversion,,,,,,,
+,,1,Lossless conversion,,,,,,,
+,,2,"Up to but excluding 3, lossless but certainly unintended conversion.",,,,,,,
+,,3,Smaller precision loss in mantissa.,,,,,,,
+,,4,Bigger precision loss in mantissa.,,,,,,,
+,,-,Magnitude loss or overflow => not allowed,,,,,,,
+,,N/A,Doesn't mater (Not Applicable),,,,,,,
+,,,,,,,,,,
+,Notes:,,,,,,,,,
+,-,"FooOrBar types mean that the argument's type was Foo, but the value also fits into the more specific Bar type.",,,,,,,,
+,,"This possibility should only be utilized as a last resort, to keep the method selection stable (always selects the same method) for Foo-s regardless of the actual value.",,,,,,,,
+,,"This is why the column of Bar-s tends to use higher costs than the less fitting columns that are, however, also selectable for higher values of type Foo.",,,,,,,,
+,-,"Between same-price conversions the one in the leftmost column (i.e., the smallest type) wins. This is realized in the Java code generator.",,,,,,,,
diff --git a/src/main/misc/overloadedNumberRules/prices.ods b/src/main/misc/overloadedNumberRules/prices.ods
new file mode 100644
index 0000000..4beabcf
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/prices.ods
Binary files differ
diff --git a/src/manual/book.xml b/src/manual/book.xml
index f79c552..a04c8b7 100644
--- a/src/manual/book.xml
+++ b/src/manual/book.xml
@@ -20372,89 +20372,163 @@
<literal>ObjectWrapper</literal> instead. (As
<literal>ObjectWrapper</literal> objects are often shared among
multiple <literal>Configuration</literal>-s, they can't use that
- setting of the <literal>Configuration</literal>.)</para>
+ setting of the <literal>Configuration</literal>.) In new or
+ actively developed projects it's recommended to use
+ <literal>Version(2, 3, 21)</literal> or higher.</para>
</listitem>
<listitem>
- <para>Fixes in the overloaded method selection rules when
- <literal>BeansWrapper</literal> or
+ <para>Fixes and reworkings in the overloaded method selection
+ rules when <literal>BeansWrapper</literal> or
<literal>DefaultObjectWrapper</literal> is used (almost all
- applications use these), but only if you create
+ applications use these), but only if you create the
<literal>BeansWrapper</literal>/<literal>DefaultObjectWrapper</literal>
with constructor parameter <literal>Version(2, 3, 21)</literal>
- (or higher). There's a very little chance that because of these
- fixes, a different overloaded method will chosen than before
- (which in theory could change the behavior of the application),
- or ambiguity error will be reported where earlier it has only
- seen one choice, hence the fixes aren't automatically active (at
- least not unit FreeMarker 2.4). But the fix mostly only effect
- calls that were failing earlier, so it's recommended to active
- it for project that are still actively developed. The fixes
- are:</para>
+ or higher. There's a little chance that because of these
+ changes, a different overloaded method will chosen than before,
+ or even that ambiguity error will arise where earlier they
+ didn't (although the opposite is much more probable), hence the
+ fixes aren't automatically active, at least not unit FreeMarker
+ 2.4. But the fix mostly only effect calls that were failing or
+ has chosen then wrong method, so it's recommended to activate it
+ for projects that are still actively developed. This fix
+ includes numerous changes:</para>
<itemizedlist>
<listitem>
- <para><literal>null</literal> argument values has only
- matched overloaded methods where the corresponding parameter
- had <literal>Object</literal> type, not a subclass of it.
- This is the most risky fix regarding backward-compatibility,
- as when the argument is <literal>null</literal>, earlier it
- has chosen the overload that has <literal>Object</literal>
- at the given position, while after the fix it also considers
- the other overloads with a non-primitive type at the given
- position. In that case the overload with the
- <literal>Object</literal> parameter will fall out (so far so
- good), but if there are two other candidates, whose
- parameter types at that position aren't in
- <literal>instanceof</literal> relationship at some
- direction, their relative specificity is undecidable and so
- there will be ambiguity exception.</para>
+ <para>Earlier, <literal>null</literal> argument values has
+ only matched overloaded methods where the corresponding
+ parameter had <literal>Object</literal> type, not a subclass
+ of it. Now it considers all overloads where the parameter
+ type is non-primitive, and just like the Java language, it
+ choses the one with the most specific type among them. This
+ is the most important fix, and also the most risky one
+ regarding backward-compatibility. Like if you have
+ <literal>m(Object o)</literal> and <literal>m(String
+ s)</literal> in a Java class, earlier for a
+ <literal>m(null)</literal> call in the template it has
+ chosen <literal>m(Object o)</literal>, but now it will
+ choose <literal>m(String s)</literal> instead (because
+ <literal>String</literal> is also
+ <literal>null</literal>-able and is more specific then
+ <literal>Object</literal>). Furthermore, if you also had
+ <literal>m(File f)</literal> in the same class, now it will
+ cause an ambiguity exception, since the specificity of
+ <literal>File</literal> and <literal>String</literal> can't
+ be compared (same rule as under Java language), while
+ earlier that wasn't a problem as only <literal>m(Object
+ o)</literal> was seen as applicable.</para>
</listitem>
<listitem>
- <para>TODO: number conversions</para>
- </listitem>
-
- <listitem>
- <para>TODO: method order dependence</para>
- </listitem>
-
- <listitem>
- <para>Ambiguity is now resolved in some cases where earlier
- it was an error. Ambiguities was and are resolved by
- selecting the compatible methods then choosing the one with
- the most specific parameter types among them. How the
- specificity of the parameter types is calculated was
- extended:</para>
+ <para>The behavior of numbers with overloaded method
+ selection was heavily reworked:</para>
<itemizedlist>
<listitem>
- <para>When comparing the specificity of two parameters
- types at the same parameter position: The algorithm now
- considers a primitive type as more specific that its
- corresponding boxing class. Also, the now it considers
- widening conversion from primitive numerical types to
- numerical classes, and from numerical class types to
- other numerical class types, and if the target is wider
- than the source, then the source is considered to be
- more specific. (Earlier this has only worked among two
- primitive types). Furthermore, it considers any
- primitive numerical type as more specific than
- <literal>Number</literal>.</para>
+ <para>If possible, it now always choses the overload
+ where overflow and truncation to integer (like 1.5 to 1)
+ is avoided. Among the methods where no such critical
+ loss occurs, it choses the overload with the least risk
+ of precision loss (unless other conditions with higher
+ priority suggest otherwise). Earlier, the method
+ selection was prone to do choices that led to overflow
+ or precision loss, especially when the parameter was a
+ literal with decimals.</para>
</listitem>
<listitem>
+ <para>Overloaded method call can now convert to
+ non-primitive numerical types, like a
+ <literal>Byte</literal> or <literal>byte</literal> value
+ is automatically converted to <literal>Integer</literal>
+ if the parameter type is <literal>Integer</literal>.
+ (This has always worked for non-overloaded methods.)
+ Earlier where such conversion was needed, the method
+ wasn't seen seen applicable.</para>
+ </listitem>
+
+ <listitem>
+ <para>Method selection is now not only based on the type
+ of the wrapped number, but also on its value. For
+ example, a <literal>Long</literal> with value
+ <literal>1</literal> is now seen to be compatible with a
+ method with parameter type <literal>int</literal> or
+ <literal>short</literal> or <literal>byte</literal>, as
+ <literal>1</literal> can be stored in those without
+ loss. This is important as unlike in Java language, in
+ FTL you doesn't have strict control over the numerical
+ types (the type of the wrapped number, actually), as FTL
+ has no type declarations. (If multiple compatible
+ methods are available, it will still try to chose the
+ one with the same or bigger type first.)</para>
+ </listitem>
+
+ <listitem>
+ <para>Conversion from/to <literal>BigInteger</literal>
+ is now supported.</para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
+
+ <listitem>
+ <para>Method choice ambiguity errors now occur much less
+ often. Ambiguities was and are resolved by selecting the
+ compatible methods then choosing the one with the most
+ specific parameter types among them. The changes are:</para>
+
+ <itemizedlist>
+ <listitem>
<para>When comparing the overall specificity of two
parameter lists: Earlier the more specific parameter
list was the one that had some parameters that won in
specificity, and if both had such parameters then it was
- an ambiguity. This rule changes in that now parameter
- specificity wins has a score, and the parameter list
- that contains the parameter with the highest score wins.
- The scores from lowest to highest: primitive wins over
- boxing type, narrower numerical type wins over wider
- numerical type, and subclass wins over
- super-class.</para>
+ an ambiguity. Now it's enough if a method has more such
+ parameters where it's a better match than the other has,
+ or if the two methods are still equal, if it has the
+ first better matching parameter. While this can lead to
+ choices that seem arbitrary (but are still
+ deterministic), as there is no automated way of
+ discovering method selection ambiguities in templates
+ (unlike in Java source code, where they will be detected
+ during compilation), especially as overloaded selection
+ has to rely on the <emphasis>runtime</emphasis> type of
+ the values which makes proper testing hard, this was
+ considered to be a better compromise than throwing an
+ exception whenever the choice of the method is not
+ obvious. Also note that in fact it's more complicated
+ then counting the <quote>winner</quote> parameter
+ positions for each methods, as certain kind of wins are
+ stronger than any number of the others: wins there the
+ other possibility is risking the loss of substantial
+ mantissa precision are the strongest (like dropping
+ decimals versus not to), wins where the primitive type
+ wins over the boxed class is the weakest (like
+ <literal>int</literal> versus
+ <literal>Integer</literal>), subclassing wins (like
+ <literal>String</literal> versus
+ <literal>Object</literal>) are between these two.</para>
+ </listitem>
+
+ <listitem>
+ <para>When comparing the specificity of two parameters
+ types at the same parameter position: The algorithm now
+ considers a primitive type as more specific that its
+ corresponding boxing class (like <literal>int</literal>
+ is considered to be more specific than
+ <literal>Integer</literal>).</para>
+ </listitem>
+
+ <listitem>
+ <para>The was a bug with overloaded varargs methods of
+ different parameter counts, where sometimes the last
+ parameters of the compared methods was ignored, which is
+ taking away a potential deciding factor and thus can
+ lead to ambiguity error. Whether this happened depends
+ on the order in which the Java reflection API has
+ returned the methods, which is undocumented and known to
+ change at least after some Java updates, breaking the
+ application.</para>
</listitem>
</itemizedlist>
</listitem>
@@ -20466,21 +20540,23 @@
unwrapping hint has influence when there's an ambiguity
regarding how to create a Java object form an FTL value. In
the vast majority of the cases, a too generic hint has no
- effect.) (This is a highly technical topic. The limitation
- of the applied algorithm is that, a single common unwrapping
- hint class is chosen for a given argument position shared by
- the overloads that has the same number of parameters. The
- issue with the too generic hints had several instances: (a)
- When the most specific common class/interface of two
- same-position parameter types were searched, if there was
- multiple common class/interfaces that had no relationship
- (this is always at most one class and one or more unrelated
- interfaces), due to the ambiguity it has felt back to using
- <literal>Object</literal> as the unwrapping hint. Now if
- there's a non-<literal>Object</literal> class among them in
- such case, it will be chosen as the hint. Otherwise if only
- a single interface remains by removing
- <literal>Cloneable</literal>,
+ effect.) (This is a highly technical topic. The way it works
+ is that a single common unwrapping hint class is chosen for
+ a given argument position shared by the overloads that has
+ the same number of parameters, and that hint class has to be
+ as specific as possible while it must fit all those
+ parameter types. The issue with the too generic hints had
+ several instances: (a) When the most specific common
+ class/interface of two same-position parameter types were
+ searched, if there was multiple common class/interfaces that
+ had no relationship (this is always at most one class and
+ one or more unrelated interfaces), due to the ambiguity it
+ has felt back to using <literal>Object</literal> as the
+ unwrapping hint. Now if there's a
+ non-<literal>Object</literal> class among them in such case,
+ it will be chosen as the hint (i.e., we ignore the common
+ interfaces). Otherwise if only a single interface remains by
+ removing <literal>Cloneable</literal>,
<literal>Serializable</literal>, and
<literal>Comparable</literal> (in that order), that will be
chosen. Only then it falls back to
@@ -20488,7 +20564,8 @@
class of a primitive type and the corresponding boxing class
was sometimes <literal>Object</literal> instead of the
boxing class. This depended on Java's the internal ordering
- of the methods, and so were quite unpredictable. (c) The
+ of the methods, and so were quite unpredictable, like could
+ change after upgrading Java under the application. (c) The
common superclass of a numerical primitive value and a
numerical non-primitive value was always
<literal>Object</literal>, now if they are primitive-boxing
@@ -20497,12 +20574,38 @@
position was not the same in all the overloaded varargs
methods, sometimes some varargs arguments where unwrapped
with too generic hints. When this happened was unpredictable
- as it depended on the internal method ordering.)</para>
+ as it depended on the internal method ordering
+ again.)</para>
</listitem>
</itemizedlist>
</listitem>
<listitem>
+ <para>Bug fixed, fix is active only if you create the
+ <literal>BeansWrapper</literal>/<literal>DefaultObjectWrapper</literal>
+ with constructor parameter <literal>Version(2, 3, 21)</literal>
+ or higher: When <literal>BeansWrapper</literal> (and so also
+ <literal>DefaultObjectWrapper</literal>) has unwrapped method
+ call arguments before calling a Java method, when the argument
+ was a <literal>AdapterTemplateModel</literal> and the target
+ parameter type was primitive,
+ <literal>AdapterTemplateModel.getAdaptedObject(Class
+ hint)</literal> has received the primitive type of the target
+ parameter (like <literal>int</literal> instead of
+ <literal>Integer</literal>) as the hint. This did not make sense
+ since <literal>getAdaptedObject</literal> can only return
+ <literal>Object</literal>-s, not primitive values. Furthermore,
+ <literal>BeansWrapper</literal> has expected the returned value
+ to be of the primitive type, otherwise it has discarded it.
+ Exactly the same problem occurs with
+ <literal>WrapperTemplateModel</literal>. Thus, ultimately, if
+ the target parameter type was primitive and some of these
+ interfaces were implemented, their return value was always
+ discarded, instead FreeMarker has felt back to other means of
+ unwrapping.</para>
+ </listitem>
+
+ <listitem>
<para><literal>ParseException</literal>-s now also store the
end-location of the error, not just its start-location. This is
useful if you want to underline the error in the source code,
diff --git a/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java b/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java
index de167eb..0aee299 100644
--- a/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java
+++ b/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java
@@ -27,13 +27,12 @@
@Override
MethodDescriptor[] shortMethodDescriptors(MethodDescriptor[] methodDescriptors) {
ArrayList<MethodDescriptor> ls = new ArrayList<MethodDescriptor>(Arrays.asList(methodDescriptors));
- Collections.sort(ls, new Comparator<MethodDescriptor>() {
-
- public int compare(MethodDescriptor o1, MethodDescriptor o2) {
- int res = o1.getMethod().toString().compareTo(o2.getMethod().toString());
- return desc ? -res : res;
- }
- });
+ Collections.sort(ls, new Comparator<MethodDescriptor>() {
+ public int compare(MethodDescriptor o1, MethodDescriptor o2) {
+ int res = o1.getMethod().toString().compareTo(o2.getMethod().toString());
+ return desc ? -res : res;
+ }
+ });
return ls.toArray(new MethodDescriptor[ls.size()]);
}
diff --git a/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java b/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java
index d5b8775..97718da 100644
--- a/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java
+++ b/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java
@@ -118,7 +118,7 @@
}
@Override
- void afterWideningUnwrappingHints(Class[] paramTypes) {
+ void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
// Do nothing
}
diff --git a/src/test/java/freemarker/ext/beans/IsApplicableTest.java b/src/test/java/freemarker/ext/beans/IsApplicableTest.java
new file mode 100644
index 0000000..24b7469
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/IsApplicableTest.java
@@ -0,0 +1,152 @@
+package freemarker.ext.beans;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class IsApplicableTest extends TestCase {
+
+ public IsApplicableTest(String name) {
+ super(name);
+ }
+
+ public void testSingle() {
+ ArgumentTypes ats = new ArgumentTypes(new Object[] { new Object() }, true);
+ assertApplicable(ats, Object.class);
+ assertNotApplicable(ats, String.class);
+ assertNotApplicable(ats, CharSequence.class);
+ assertNotApplicable(ats, Integer.class);
+ assertNotApplicable(ats, Integer.TYPE);
+
+ ats = new ArgumentTypes(new Object[] { "" }, true);
+ assertApplicable(ats, Object.class);
+ assertApplicable(ats, CharSequence.class);
+ assertApplicable(ats, String.class);
+ assertNotApplicable(ats, Integer.class);
+ assertNotApplicable(ats, Integer.TYPE);
+
+ ats = new ArgumentTypes(new Object[] { 1 }, true);
+ assertApplicable(ats, Object.class);
+ assertNotApplicable(ats, CharSequence.class);
+ assertNotApplicable(ats, String.class);
+ assertNotApplicable(ats, Short.class);
+ assertNotApplicable(ats, Short.TYPE);
+ assertApplicable(ats, Integer.class);
+ assertApplicable(ats, Integer.TYPE);
+ assertApplicable(ats, Float.class);
+ assertApplicable(ats, Float.TYPE);
+ assertApplicable(ats, Double.class);
+ assertApplicable(ats, Double.TYPE);
+ assertApplicable(ats, BigDecimal.class);
+ assertApplicable(ats, BigInteger.class);
+
+ ats = new ArgumentTypes(new Object[] { new OverloadedNumberUtil.IntegerOrByte(1, (byte) 1) }, true);
+ assertApplicable(ats, Object.class);
+ assertNotApplicable(ats, CharSequence.class);
+ assertNotApplicable(ats, String.class);
+ assertApplicable(ats, Short.class);
+ assertApplicable(ats, Short.TYPE);
+ assertApplicable(ats, Integer.class);
+ assertApplicable(ats, Integer.TYPE);
+ assertApplicable(ats, Float.class);
+ assertApplicable(ats, Float.TYPE);
+ assertApplicable(ats, Double.class);
+ assertApplicable(ats, Double.TYPE);
+ assertApplicable(ats, BigDecimal.class);
+ assertApplicable(ats, BigInteger.class);
+
+ ats = new ArgumentTypes(new Object[] { 1.0f }, true);
+ assertApplicable(ats, Object.class);
+ assertNotApplicable(ats, CharSequence.class);
+ assertNotApplicable(ats, String.class);
+ assertNotApplicable(ats, Integer.class);
+ assertNotApplicable(ats, Integer.TYPE);
+ assertApplicable(ats, Float.class);
+ assertApplicable(ats, Float.TYPE);
+ assertApplicable(ats, Double.class);
+ assertApplicable(ats, Double.TYPE);
+ assertApplicable(ats, BigDecimal.class);
+ assertNotApplicable(ats, BigInteger.class);
+
+ ats = new ArgumentTypes(new Object[] { null }, true);
+ assertApplicable(ats, Object.class);
+ assertApplicable(ats, String.class);
+ assertApplicable(ats, Integer.class);
+ assertNotApplicable(ats, Integer.TYPE);
+ assertNotApplicable(ats, Boolean.TYPE);
+ assertNotApplicable(ats, Object.class, Object.class);
+ assertNotApplicable(ats);
+ }
+
+ public void testMulti() {
+ ArgumentTypes ats = new ArgumentTypes(new Object[] { new Object(), "", 1, true }, true);
+ assertApplicable(ats, Object.class, Object.class, Object.class, Object.class);
+ assertApplicable(ats, Object.class, String.class, Number.class, Boolean.class);
+ assertApplicable(ats, Object.class, CharSequence.class, Integer.class, Serializable.class);
+ assertApplicable(ats, Object.class, Comparable.class, Integer.TYPE, Serializable.class);
+ assertNotApplicable(ats, Object.class, String.class, Number.class, Number.class);
+ assertNotApplicable(ats, Object.class, StringBuffer.class, Number.class, Boolean.class);
+ assertNotApplicable(ats, int.class, Object.class, Object.class, Object.class);
+ assertNotApplicable(ats, Object.class, Object.class, Object.class);
+ assertNotApplicable(ats, Object.class, Object.class, Object.class, Object.class, Object.class);
+ }
+
+ public void testNoParam() {
+ ArgumentTypes ats = new ArgumentTypes(new Object[] { }, true);
+ assertApplicable(ats);
+ assertNotApplicable(ats, Object.class);
+ }
+
+ public void testVarags() {
+ Object[][] argLists = new Object[][] {
+ new Object[] { "", 1, 2, 3 },
+ new Object[] { "", 1, (byte) 2, 3 },
+ new Object[] { "", 1},
+ new Object[] { "" },
+ };
+ for (Object[] args : argLists) {
+ ArgumentTypes ats = new ArgumentTypes(args, true);
+ assertApplicable(ats, true, String.class, int[].class);
+ assertApplicable(ats, true, String.class, Integer[].class);
+ assertApplicable(ats, true, Object.class, Comparable[].class);
+ assertApplicable(ats, true, Object.class, Object[].class);
+ assertNotApplicable(ats, true, StringBuilder.class, int[].class);
+ if (args.length > 1) {
+ assertNotApplicable(ats, true, String.class, String[].class);
+ } else {
+ assertApplicable(ats, true, String.class, String[].class);
+ }
+ }
+ }
+
+ private void assertNotApplicable(ArgumentTypes ats, Class... paramTypes) {
+ assertNotApplicable(ats, false, paramTypes);
+ }
+
+ private void assertNotApplicable(ArgumentTypes ats, boolean varargs, Class... paramTypes) {
+ List tested = new ArrayList();
+ tested.add(new CallableMemberDescriptor((Method) null, paramTypes));
+ if (ats.getApplicables(tested, varargs).size() != 0) {
+ fail("Parameter types were applicable");
+ }
+ }
+
+ private void assertApplicable(ArgumentTypes ats, Class<?>... paramTypes) {
+ assertApplicable(ats, false, paramTypes);
+ }
+
+ private void assertApplicable(ArgumentTypes ats, boolean varargs, Class<?>... paramTypes) {
+ List tested = new ArrayList();
+ tested.add(new CallableMemberDescriptor((Method) null, paramTypes));
+ if (ats.getApplicables(tested, varargs).size() != 1) {
+ fail("Parameter types weren't applicable");
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MiscNumericalOperationsTest.java b/src/test/java/freemarker/ext/beans/MiscNumericalOperationsTest.java
new file mode 100644
index 0000000..759f4ff
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MiscNumericalOperationsTest.java
@@ -0,0 +1,100 @@
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.junit.Assert;
+
+import freemarker.template.Version;
+
+import junit.framework.TestCase;
+
+public class MiscNumericalOperationsTest extends TestCase {
+
+ public MiscNumericalOperationsTest(String name) {
+ super(name);
+ }
+
+ public void testForceUnwrappedNumberToType() {
+ // Usual type to to all other types:
+ Double n = Double.valueOf(123.75);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Short.class, false), Short.valueOf(n.shortValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Short.TYPE, false), Short.valueOf(n.shortValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Byte.class, false), Byte.valueOf(n.byteValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Byte.TYPE, false), Byte.valueOf(n.byteValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Integer.class, false), Integer.valueOf(n.intValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Integer.TYPE, false), Integer.valueOf(n.intValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Long.class, false), Long.valueOf(n.longValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Long.TYPE, false), Long.valueOf(n.longValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Float.class, false), Float.valueOf(n.floatValue()));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Float.TYPE, false), Float.valueOf(n.floatValue()));
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(n, Double.class, false) == n);
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(n, Double.TYPE, false) == n);
+ try {
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, BigInteger.class, false), new BigInteger("123"));
+ fail();
+ } catch (NumberFormatException e) {
+ // expected
+ }
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, BigInteger.class, true), new BigInteger("123"));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, BigDecimal.class, false), new BigDecimal("123.75"));
+
+ // Cases of conversion to BigDecimal:
+ BigDecimal bd = new BigDecimal("123");
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(new BigInteger("123"), BigDecimal.class, false), bd);
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(bd, BigDecimal.class, false) == bd);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(Long.valueOf(123), BigDecimal.class, false), bd);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(Double.valueOf(123), BigDecimal.class, false), bd);
+
+ // Cases of conversion to BigInteger:
+ BigInteger bi = new BigInteger("123");
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(new BigDecimal("123.6"), BigInteger.class, true), bi);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(
+ new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal("123")), BigInteger.class, true), bi);
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(bi, BigInteger.class, true) == bi);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(Long.valueOf(123), BigInteger.class, true), bi);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(Double.valueOf(123.6), BigInteger.class, true), bi);
+
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(n, Number.class, true) == n);
+ assertNull(BeansWrapper.forceUnwrappedNumberToType(n, RationalNumber.class, true));
+ RationalNumber r = new RationalNumber(1, 2);
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(r, RationalNumber.class, true) == r);
+ assertTrue(BeansWrapper.forceUnwrappedNumberToType(r, Number.class, true) == r);
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(r, Double.class, true), Double.valueOf(0.5));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(r, BigDecimal.class, true), new BigDecimal("0.5"));
+ assertEquals(BeansWrapper.forceUnwrappedNumberToType(r, BigInteger.class, true), BigInteger.ZERO);
+ }
+
+ @SuppressWarnings("boxing")
+ public void testForceNumberArgumentsToParameterTypes() {
+ OverloadedMethodsSubset oms = new OverloadedFixArgsMethods(new BeansWrapper(new Version(2, 3, 21)));
+ Class[] paramTypes = new Class[] { Short.TYPE, Short.class, Double.TYPE, BigDecimal.class, BigInteger.class };
+ Object[] args;
+
+ args = newArgs();
+ oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF });
+ Assert.assertArrayEquals(
+ args,
+ new Object[] { (short) 123, (short) 123, 123.75, new BigDecimal("123.75"), BigInteger.valueOf(123) });
+
+ args = newArgs();
+ oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 0, 0, 0, 0, 0 });
+ Assert.assertArrayEquals(args, newArgs());
+
+ args = newArgs();
+ oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 8, 8, 8, 8, 8 });
+ Assert.assertArrayEquals(args, newArgs());
+
+ args = newArgs();
+ oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 0xFFFF, 0, 0xFFFF, 0, 0xFFFF });
+ Assert.assertArrayEquals(
+ args,
+ new Object[] { (short) 123, 123.75, 123.75, 123.75, BigInteger.valueOf(123) });
+ }
+
+ @SuppressWarnings("boxing")
+ private Object[] newArgs() {
+ return new Object[] { 123.75, 123.75, 123.75, 123.75, 123.75 };
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/OverloadedMethodsNumericalFlagsTest.java b/src/test/java/freemarker/ext/beans/OverloadedMethodsNumericalFlagsTest.java
new file mode 100644
index 0000000..58c5892
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/OverloadedMethodsNumericalFlagsTest.java
@@ -0,0 +1,338 @@
+package freemarker.ext.beans;
+
+import java.beans.MethodDescriptor;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+import freemarker.template.Version;
+
+public class OverloadedMethodsNumericalFlagsTest extends TestCase {
+
+ public OverloadedMethodsNumericalFlagsTest(String name) {
+ super(name);
+ }
+
+ private final BeansWrapper bw = new BeansWrapper(new Version(2, 3, 21));
+
+ public void testSingleNumType() {
+ checkPossibleParamTypes(SingleTypeC.class, "mInt",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT);
+ checkPossibleParamTypes(SingleTypeC.class, "mLong",
+ OverloadedNumberUtil.FLAG_LONG);
+ checkPossibleParamTypes(SingleTypeC.class, "mShort",
+ OverloadedNumberUtil.FLAG_SHORT);
+ checkPossibleParamTypes(SingleTypeC.class, "mByte",
+ OverloadedNumberUtil.FLAG_BYTE,
+ 0);
+ checkPossibleParamTypes(SingleTypeC.class, "mDouble",
+ OverloadedNumberUtil.FLAG_DOUBLE);
+ checkPossibleParamTypes(SingleTypeC.class, "mFloat",
+ OverloadedNumberUtil.FLAG_FLOAT);
+ checkPossibleParamTypes(SingleTypeC.class, "mUnknown",
+ OverloadedNumberUtil.FLAG_UNKNOWN_TYPE);
+
+ checkPossibleParamTypes(SingleTypeC.class, "mVarParamCnt",
+ OverloadedNumberUtil.FLAG_BIG_DECIMAL);
+ checkPossibleParamTypes(SingleTypeC.class, "mVarParamCnt",
+ OverloadedNumberUtil.FLAG_BIG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE);
+ checkPossibleParamTypes(SingleTypeC.class, "mVarParamCnt",
+ OverloadedNumberUtil.FLAG_DOUBLE,
+ OverloadedNumberUtil.FLAG_FLOAT,
+ OverloadedNumberUtil.FLAG_INTEGER);
+ }
+
+ static public class SingleTypeC {
+ public void mInt(int a1, String a2) { }
+ public void mInt(int a1, int a2) { }
+ public void mLong(long a1) { }
+ public void mLong(Long a1) { }
+ public void mShort(short a1) { }
+ public void mByte(byte a1, boolean a2) { }
+ public void mByte(byte a1, String a2) { }
+ public void mByte(byte a1, Object a2) { }
+ public void mDouble(double a1) { }
+ public void mFloat(float a1) { }
+ public void mUnknown(RationalNumber a1) { };
+ public void mVarParamCnt(BigDecimal a1) { }
+ public void mVarParamCnt(BigInteger a1, Double a2) { }
+ public void mVarParamCnt(Double a1, Float a2, Integer a3) { }
+ public void mVarParamCnt(Object a1, char a2, boolean a3, File a4, Map a5, Boolean a6) { }
+ public void mVarParamCnt(Long a1, int a2, short a3, byte a4, double a5, float a6) { }
+ }
+
+ public void testMultipleNumType() {
+ checkPossibleParamTypes(MultiTypeC.class, "m1",
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ | OverloadedNumberUtil.FLAG_BYTE | OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+ );
+
+ checkPossibleParamTypes(MultiTypeC.class, "m2",
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ | OverloadedNumberUtil.FLAG_SHORT | OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_FLOAT
+ );
+
+ checkPossibleParamTypes(MultiTypeC.class, "m3",
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ | OverloadedNumberUtil.FLAG_BIG_DECIMAL| OverloadedNumberUtil.FLAG_BIG_INTEGER,
+ OverloadedNumberUtil.FLAG_BIG_INTEGER,
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ | OverloadedNumberUtil.FLAG_BIG_DECIMAL | OverloadedNumberUtil.FLAG_UNKNOWN_TYPE
+ );
+
+ checkPossibleParamTypes(MultiTypeC.class, "m4",
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT | OverloadedNumberUtil.FLAG_FLOAT
+ );
+
+ checkPossibleParamTypes(MultiTypeC.class, "m5",
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ | OverloadedNumberUtil.FLAG_FLOAT | OverloadedNumberUtil.FLAG_DOUBLE
+ );
+
+ checkPossibleParamTypes(MultiTypeC.class, "m6",
+ OverloadedNumberUtil.FLAG_INTEGER
+ );
+ assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", false, 2), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+ assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", true, 2), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+ assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", false, 3), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+ assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", true, 3), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+ checkPossibleParamTypes(MultiTypeC.class, "m6",
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT | OverloadedNumberUtil.FLAG_DOUBLE,
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT | OverloadedNumberUtil.FLAG_DOUBLE,
+ 0
+ );
+ }
+
+ static public class MultiTypeC {
+ public void m1(byte a1) { };
+ public void m1(int a1) { };
+ public void m1(double a2) { };
+
+ public void m2(short a1) { };
+ public void m2(long a1) { };
+ public void m2(float a1) { };
+ public void m2(char a1) { };
+
+ public void m3(BigInteger a1, BigInteger a2, BigDecimal a3) { };
+ public void m3(BigDecimal a1, BigInteger a2, RationalNumber a3) { };
+
+ public void m4(float a1) { };
+ public void m4(char a1) { };
+
+ public void m5(Float a1) { };
+ public void m5(Double a1) { };
+ public void m5(Enum a1) { };
+
+ public void m6(int a1) { };
+ public void m6(String a1, char a2) { };
+ public void m6(String a1, char a2, boolean a3) { };
+ public void m6(String a1, char a2, char a3) { };
+ public void m6(double a1, int a2, String a3, String a4) { };
+ public void m6(String a1, int a2, double a3, String a4) { };
+ }
+
+ public void testVarArgs() {
+ checkPossibleParamTypes(VarArgsC.class, "m1",
+ OverloadedNumberUtil.FLAG_INTEGER
+ );
+ checkPossibleParamTypes(VarArgsC.class, "m2",
+ OverloadedNumberUtil.FLAG_DOUBLE,
+ OverloadedNumberUtil.FLAG_INTEGER
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m3",
+ OverloadedNumberUtil.FLAG_INTEGER
+ );
+ checkPossibleParamTypes(VarArgsC.class, "m3",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+ checkPossibleParamTypes(VarArgsC.class, "m3",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+ | OverloadedNumberUtil.FLAG_BIG_DECIMAL | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m4",
+ OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_LONG
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m5",
+ OverloadedNumberUtil.FLAG_LONG
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m6",
+ OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m7",
+ OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_BYTE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m8",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m9",
+ OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_BYTE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_DOUBLE
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m10",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE
+ );
+ checkPossibleParamTypes(VarArgsC.class, "m10",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE,
+ OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m11",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_SHORT
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+ checkPossibleParamTypes(VarArgsC.class, "m11",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_SHORT
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m12",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE
+ );
+ checkPossibleParamTypes(VarArgsC.class, "m12",
+ OverloadedNumberUtil.FLAG_INTEGER,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_SHORT
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_BYTE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+
+ checkPossibleParamTypes(VarArgsC.class, "m13",
+ 0,
+ OverloadedNumberUtil.FLAG_DOUBLE);
+ checkPossibleParamTypes(VarArgsC.class, "m13",
+ 0,
+ OverloadedNumberUtil.FLAG_DOUBLE,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_UNKNOWN_TYPE
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_LONG
+ | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+ );
+ }
+
+ static public class VarArgsC {
+ public void m1(int... va) { }
+
+ public void m2(double a1, int... va) { }
+
+ public void m3(int... va) { }
+ public void m3(int a1, double... va) { }
+ public void m3(int a1, double a2, BigDecimal... va) { }
+
+ public void m4(int... va) { }
+ public void m4(long... va) { }
+
+ public void m5(Long... va) { }
+ public void m5(long... va) { }
+
+ public void m6(long... va) { }
+ public void m6(String... va) { }
+
+ public void m7(int a1, double... va) { }
+ public void m7(byte a1, float... va) { }
+
+ public void m8(int a1, double... va) { }
+ public void m8(int a1, float... va) { }
+
+ public void m9(int a1, double... va) { }
+ public void m9(byte a1, double... va) { }
+
+ public void m10(int a1, double a2, long... va) { }
+ public void m10(int a1, double... va) { }
+
+ public void m11(int a1, short a2, long... va) { }
+ public void m11(int a1, double... va) { }
+
+ public void m12(int a1, short a2, byte a3, long... va) { }
+ public void m12(int a1, double... va) { }
+
+ public void m13(char a1, double a2, RationalNumber a3, Long... va) { }
+ public void m13(char a1, Double... va) { }
+ }
+
+ private OverloadedMethodsSubset newOverloadedMethodsSubset(Class cl, String methodName, final boolean desc) {
+ final Method[] ms = cl.getMethods();
+
+ final List<Method> filteredMethods = new ArrayList();
+ for (Method m : ms) {
+ if (m.getName().equals(methodName)) {
+ filteredMethods.add(m);
+ }
+ }
+ // As the order in which getMethods() returns the methods is undefined, we sort them for test predictability:
+ Collections.sort(filteredMethods, new Comparator<Method>() {
+ public int compare(Method o1, Method o2) {
+ int res = o1.toString().compareTo(o2.toString());
+ return desc ? -res : res;
+ }
+ });
+
+ final OverloadedMethodsSubset oms = cl.getName().indexOf("VarArgs") == -1
+ ? new OverloadedFixArgsMethods(bw) : new OverloadedVarArgsMethods(bw);
+ for (Method m : filteredMethods) {
+ oms.addCallableMemberDescriptor(new CallableMemberDescriptor(m, m.getParameterTypes()));
+ }
+ return oms;
+ }
+
+ private void checkPossibleParamTypes(Class cl, String methodName, int... expectedParamTypes) {
+ checkPossibleParamTypes(cl, methodName, false, expectedParamTypes);
+ checkPossibleParamTypes(cl, methodName, true, expectedParamTypes);
+ }
+
+ private void checkPossibleParamTypes(Class cl, String methodName, boolean revMetOrder, int... expectedParamTypes) {
+ int[] actualParamTypes = getPossibleParamTypes(cl, methodName, revMetOrder, expectedParamTypes.length);
+ assertNotNull(actualParamTypes);
+ assertEquals(expectedParamTypes.length, actualParamTypes.length);
+ for (int i = 0; i < expectedParamTypes.length; i++) {
+ assertEquals(expectedParamTypes[i], actualParamTypes[i]);
+ }
+ }
+
+ private int[] getPossibleParamTypes(Class cl, String methodName, boolean revMetOrder, int paramCnt) {
+ OverloadedMethodsSubset oms = newOverloadedMethodsSubset(cl, methodName, revMetOrder);
+ int[] actualParamTypes = oms.getPossibleNumericalTypes(paramCnt);
+ return actualParamTypes;
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/OverloadedNumberUtilTest.java b/src/test/java/freemarker/ext/beans/OverloadedNumberUtilTest.java
new file mode 100644
index 0000000..a51828a
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/OverloadedNumberUtilTest.java
@@ -0,0 +1,566 @@
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class OverloadedNumberUtilTest extends TestCase {
+
+ public OverloadedNumberUtilTest(String name) {
+ super(name);
+ }
+
+ public void testIntegerCoercions() {
+ cipEqu(new Byte(Byte.MAX_VALUE));
+ cipEqu(new Byte((byte) 0));
+ cipEqu(new Byte(Byte.MIN_VALUE));
+
+ cipEqu(new Short(Byte.MAX_VALUE),
+ new OverloadedNumberUtil.ShortOrByte((short) Byte.MAX_VALUE, Byte.MAX_VALUE));
+ cipEqu(new Short((short) 0),
+ new OverloadedNumberUtil.ShortOrByte((short) 0, (byte) 0));
+ cipEqu(new Short(Byte.MIN_VALUE),
+ new OverloadedNumberUtil.ShortOrByte((short) Byte.MIN_VALUE, Byte.MIN_VALUE));
+
+ cipEqu(new Short((short) (Byte.MAX_VALUE + 1)));
+ cipEqu(new Short((short) (Byte.MIN_VALUE - 1)));
+ cipEqu(new Short(Short.MAX_VALUE));
+ cipEqu(new Short(Short.MIN_VALUE));
+
+ cipEqu(new Integer(Byte.MAX_VALUE),
+ new OverloadedNumberUtil.IntegerOrByte((int) Byte.MAX_VALUE, Byte.MAX_VALUE));
+ cipEqu(new Integer(0),
+ new OverloadedNumberUtil.IntegerOrByte(0, (byte) 0));
+ cipEqu(new Integer(Byte.MIN_VALUE),
+ new OverloadedNumberUtil.IntegerOrByte((int) Byte.MIN_VALUE, Byte.MIN_VALUE));
+
+ cipEqu(new Integer(Byte.MAX_VALUE + 1),
+ new OverloadedNumberUtil.IntegerOrShort(Byte.MAX_VALUE + 1, (short) (Byte.MAX_VALUE + 1)));
+ cipEqu(new Integer(Byte.MIN_VALUE - 1),
+ new OverloadedNumberUtil.IntegerOrShort(Byte.MIN_VALUE - 1, (short) (Byte.MIN_VALUE - 1)));
+ cipEqu(new Integer(Short.MAX_VALUE),
+ new OverloadedNumberUtil.IntegerOrShort((int) Short.MAX_VALUE, Short.MAX_VALUE));
+ cipEqu(new Integer(Short.MIN_VALUE),
+ new OverloadedNumberUtil.IntegerOrShort((int) Short.MIN_VALUE, Short.MIN_VALUE));
+
+ cipEqu(new Integer(Short.MAX_VALUE + 1));
+ cipEqu(new Integer(Short.MIN_VALUE - 1));
+ cipEqu(new Integer(Integer.MAX_VALUE));
+ cipEqu(new Integer(Integer.MIN_VALUE));
+
+ cipEqu(new Long(Byte.MAX_VALUE),
+ new OverloadedNumberUtil.LongOrByte((long) Byte.MAX_VALUE, Byte.MAX_VALUE));
+ cipEqu(new Long(0),
+ new OverloadedNumberUtil.LongOrByte((long) 0, (byte) 0));
+ cipEqu(new Long(Byte.MIN_VALUE),
+ new OverloadedNumberUtil.LongOrByte((long) Byte.MIN_VALUE, Byte.MIN_VALUE));
+
+ cipEqu(new Long(Byte.MAX_VALUE + 1),
+ new OverloadedNumberUtil.LongOrShort((long) (Byte.MAX_VALUE + 1), (short) (Byte.MAX_VALUE + 1)));
+ cipEqu(new Long(Byte.MIN_VALUE - 1),
+ new OverloadedNumberUtil.LongOrShort((long) (Byte.MIN_VALUE - 1), (short) (Byte.MIN_VALUE - 1)));
+ cipEqu(new Long(Short.MAX_VALUE),
+ new OverloadedNumberUtil.LongOrShort((long) Short.MAX_VALUE, Short.MAX_VALUE));
+ cipEqu(new Long(Short.MIN_VALUE),
+ new OverloadedNumberUtil.LongOrShort((long) Short.MIN_VALUE, Short.MIN_VALUE));
+
+ cipEqu(new Long(Short.MAX_VALUE + 1),
+ new OverloadedNumberUtil.LongOrInteger((long) Short.MAX_VALUE + 1, Short.MAX_VALUE + 1));
+ cipEqu(new Long(Short.MIN_VALUE - 1),
+ new OverloadedNumberUtil.LongOrInteger((long) Short.MIN_VALUE - 1, Short.MIN_VALUE - 1));
+ cipEqu(new Long(Integer.MAX_VALUE),
+ new OverloadedNumberUtil.LongOrInteger((long) Integer.MAX_VALUE, Integer.MAX_VALUE));
+ cipEqu(new Long(Integer.MIN_VALUE),
+ new OverloadedNumberUtil.LongOrInteger((long) Integer.MIN_VALUE, Integer.MIN_VALUE));
+
+ cipEqu(new Long(Integer.MAX_VALUE + 1L));
+ cipEqu(new Long(Integer.MIN_VALUE - 1L));
+ cipEqu(new Long(Long.MAX_VALUE));
+ cipEqu(new Long(Long.MIN_VALUE));
+ }
+
+ public void testIntegerNoCoercions() {
+ cipEqu(new Integer(Byte.MAX_VALUE), new Integer(Byte.MAX_VALUE), 0);
+ cipEqu(new Integer(0), new Integer(0), 0);
+ cipEqu(new Integer(Byte.MIN_VALUE), new Integer(Byte.MIN_VALUE), 0);
+ }
+
+ public void testIntegerLimitedCoercions() {
+ cipEqu(new Integer(Byte.MAX_VALUE), new Integer(Byte.MAX_VALUE), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(new Integer(0), new Integer(0), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(new Integer(Byte.MIN_VALUE), new Integer(Byte.MIN_VALUE), OverloadedNumberUtil.FLAG_INTEGER);
+
+ cipEqu(new Long(Integer.MAX_VALUE + 1L), new Long(Integer.MAX_VALUE + 1L), OverloadedNumberUtil.FLAG_INTEGER);
+
+ for (int n = -1; n < 2; n++) {
+ final Long longN = new Long(n);
+ cipEqu(longN, new OverloadedNumberUtil.LongOrInteger(longN, n), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(longN, new OverloadedNumberUtil.LongOrShort(longN, (short) n), OverloadedNumberUtil.FLAG_SHORT);
+ cipEqu(longN, new OverloadedNumberUtil.LongOrByte(longN, (byte) n), OverloadedNumberUtil.FLAG_BYTE);
+ cipEqu(longN, new OverloadedNumberUtil.LongOrShort(longN, (short) n),
+ OverloadedNumberUtil.FLAG_SHORT | OverloadedNumberUtil.FLAG_INTEGER);
+ }
+ }
+
+ public void testBigDecimalCoercions() {
+ cipEqu(new BigDecimal(123), new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal(123)));
+ cipEqu(new BigDecimal(123), new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal(123)),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_LONG);
+ cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_DOUBLE);
+ cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+
+ cipEqu(new BigDecimal(123.5));
+ // Not wasting time with check if it's a whole number if we only have integer-only or non-integer-only targets:
+ cipEqu(new BigDecimal(123.5), OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_LONG);
+ cipEqu(new BigDecimal(123.5), OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+
+ cipEqu(new BigDecimal(0.01));
+ cipEqu(new BigDecimal(-0.01));
+ cipEqu(BigDecimal.ZERO, new OverloadedNumberUtil.IntegerBigDecimal(BigDecimal.ZERO));
+ }
+
+ public void testUnknownNumberCoercion() {
+ cipEqu(new RationalNumber(2, 3));
+ }
+
+ @SuppressWarnings("boxing")
+ public void testDoubleCoercion() {
+ cipEqu(Double.valueOf(1.5), new OverloadedNumberUtil.DoubleOrFloat(1.5));
+ cipEqu(Double.valueOf(-0.125), new OverloadedNumberUtil.DoubleOrFloat(-0.125));
+ cipEqu(Double.valueOf(Float.MAX_VALUE), new OverloadedNumberUtil.DoubleOrFloat((double) Float.MAX_VALUE));
+ cipEqu(Double.valueOf(-Float.MAX_VALUE), new OverloadedNumberUtil.DoubleOrFloat((double) -Float.MAX_VALUE));
+ cipEqu(Double.valueOf(Float.MAX_VALUE * 10.0));
+ cipEqu(Double.valueOf(-Float.MAX_VALUE * 10.0));
+
+ cipEqu(Double.valueOf(0), new OverloadedNumberUtil.DoubleOrByte(0.0, (byte) 0));
+ cipEqu(Double.valueOf(Byte.MAX_VALUE), new OverloadedNumberUtil.DoubleOrByte((double) Byte.MAX_VALUE, Byte.MAX_VALUE));
+ cipEqu(Double.valueOf(Byte.MIN_VALUE), new OverloadedNumberUtil.DoubleOrByte((double) Byte.MIN_VALUE, Byte.MIN_VALUE));
+
+ cipEqu(Double.valueOf(Byte.MAX_VALUE + 1), new OverloadedNumberUtil.DoubleOrShort((double)
+ (Byte.MAX_VALUE + 1), (short) (Byte.MAX_VALUE + 1)));
+ cipEqu(Double.valueOf(Byte.MIN_VALUE - 1), new OverloadedNumberUtil.DoubleOrShort((double)
+ (Byte.MIN_VALUE - 1), (short) (Byte.MIN_VALUE - 1)));
+
+ cipEqu(Double.valueOf(Short.MAX_VALUE + 1),
+ new OverloadedNumberUtil.DoubleOrIntegerOrFloat((double) Short.MAX_VALUE + 1, Short.MAX_VALUE + 1));
+ cipEqu(Double.valueOf(Short.MIN_VALUE - 1),
+ new OverloadedNumberUtil.DoubleOrIntegerOrFloat((double) Short.MIN_VALUE - 1, Short.MIN_VALUE- 1));
+ cipEqu(Double.valueOf(16777216), new OverloadedNumberUtil.DoubleOrIntegerOrFloat(16777216.0, 16777216));
+ cipEqu(Double.valueOf(-16777216), new OverloadedNumberUtil.DoubleOrIntegerOrFloat(-16777216.0, -16777216));
+
+ cipEqu(Double.valueOf(Integer.MAX_VALUE),
+ new OverloadedNumberUtil.DoubleOrInteger((double) Integer.MAX_VALUE, Integer.MAX_VALUE));
+ cipEqu(Double.valueOf(Integer.MIN_VALUE),
+ new OverloadedNumberUtil.DoubleOrInteger((double) Integer.MIN_VALUE, Integer.MIN_VALUE));
+
+ cipEqu(Double.valueOf(Integer.MAX_VALUE + 1L),
+ new OverloadedNumberUtil.DoubleOrLong(Double.valueOf(Integer.MAX_VALUE + 1L), Integer.MAX_VALUE + 1L));
+ cipEqu(Double.valueOf(Integer.MIN_VALUE - 1L),
+ new OverloadedNumberUtil.DoubleOrLong(Double.valueOf(Integer.MIN_VALUE - 1L), Integer.MIN_VALUE - 1L));
+ cipEqu(Double.valueOf(Long.MAX_VALUE),
+ new OverloadedNumberUtil.DoubleOrFloat((double) Long.MAX_VALUE));
+ cipEqu(Double.valueOf(Long.MIN_VALUE),
+ new OverloadedNumberUtil.DoubleOrFloat((double) Long.MIN_VALUE));
+
+ // When only certain target types are present:
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrByte(5.0, (byte) 5), OverloadedNumberUtil.FLAG_BYTE);
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrByte(5.0, (byte) 5), OverloadedNumberUtil.FLAG_BYTE | OverloadedNumberUtil.FLAG_SHORT);
+ cipEqu(Double.valueOf(-129), OverloadedNumberUtil.FLAG_BYTE);
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrShort(5.0, (short) 5), OverloadedNumberUtil.FLAG_SHORT);
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrInteger(5.0, 5), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrLong(5.0, 5), OverloadedNumberUtil.FLAG_LONG);
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrFloat(5.0), OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(Double.valueOf(5), Double.valueOf(5), OverloadedNumberUtil.FLAG_DOUBLE);
+ cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrFloat(5.0),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(Double.valueOf(5.9), new OverloadedNumberUtil.DoubleOrFloat(5.9),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(Double.valueOf(5.9),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(Double.valueOf(5.9), new OverloadedNumberUtil.DoubleOrFloat(5.9),
+ OverloadedNumberUtil.FLAG_FLOAT | OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(Double.valueOf(5.9), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(Double.valueOf(Long.MAX_VALUE),
+ new OverloadedNumberUtil.DoubleOrFloat((double) Long.MAX_VALUE),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(Double.valueOf(Long.MAX_VALUE),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_LONG);
+ cipEqu(Double.valueOf(Long.MIN_VALUE),
+ new OverloadedNumberUtil.DoubleOrFloat((double) Long.MIN_VALUE),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(Double.valueOf(Float.MAX_VALUE * 10),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(Double.valueOf(-Float.MAX_VALUE * 10),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+
+ // Rounded values:
+ cipEqu(Double.valueOf(0.0000009),
+ new OverloadedNumberUtil.DoubleOrByte(0.0000009, (byte) 0));
+ cipEqu(Double.valueOf(-0.0000009),
+ new OverloadedNumberUtil.DoubleOrByte(-0.0000009, (byte) 0));
+ cipEqu(Double.valueOf(0.9999991),
+ new OverloadedNumberUtil.DoubleOrByte(0.9999991, (byte) 1));
+ cipEqu(Double.valueOf(-0.9999991),
+ new OverloadedNumberUtil.DoubleOrByte(-0.9999991, (byte) -1));
+ cipEqu(Double.valueOf(0.0000009),
+ new OverloadedNumberUtil.DoubleOrShort(0.0000009, (short) 0),
+ OverloadedNumberUtil.FLAG_SHORT | OverloadedNumberUtil.FLAG_DOUBLE);
+ cipEqu(Double.valueOf(0.0000009), new OverloadedNumberUtil.DoubleOrInteger(0.0000009, 0),
+ OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_DOUBLE);
+ cipEqu(Double.valueOf(0.0000009), new OverloadedNumberUtil.DoubleOrLong(0.0000009, 0),
+ OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE);
+ cipEqu(Double.valueOf(0.0000009),
+ new OverloadedNumberUtil.DoubleOrByte(0.0000009, (byte) 0), OverloadedNumberUtil.FLAG_BYTE);
+ cipEqu(Double.valueOf(0.0000009),
+ new OverloadedNumberUtil.DoubleOrShort(0.0000009, (short) 0), OverloadedNumberUtil.FLAG_SHORT);
+ cipEqu(Double.valueOf(0.0000009),
+ new OverloadedNumberUtil.DoubleOrInteger(0.0000009, 0), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(Double.valueOf(0.0000009),
+ new OverloadedNumberUtil.DoubleOrLong(0.0000009, 0L), OverloadedNumberUtil.FLAG_LONG);
+ cipEqu(Double.valueOf(0.9999999),
+ new OverloadedNumberUtil.DoubleOrInteger(0.9999999, 1), OverloadedNumberUtil.FLAG_INTEGER);
+ cipEqu(Double.valueOf(Byte.MAX_VALUE + 0.9e-6),
+ new OverloadedNumberUtil.DoubleOrByte(Byte.MAX_VALUE + 0.9e-6, Byte.MAX_VALUE));
+ cipEqu(Double.valueOf(Byte.MIN_VALUE - 0.9e-6),
+ new OverloadedNumberUtil.DoubleOrByte(Byte.MIN_VALUE - 0.9e-6, Byte.MIN_VALUE));
+ cipEqu(Double.valueOf(Byte.MAX_VALUE + 1.1e-6),
+ new OverloadedNumberUtil.DoubleOrFloat(Byte.MAX_VALUE + 1.1e-6));
+ cipEqu(Double.valueOf(Byte.MIN_VALUE - 1.1e-6),
+ new OverloadedNumberUtil.DoubleOrFloat(Byte.MIN_VALUE - 1.1e-6));
+ cipEqu(Double.valueOf(Byte.MAX_VALUE + 0.9999991),
+ new OverloadedNumberUtil.DoubleOrShort(
+ Byte.MAX_VALUE + 0.9999991, (short) (Byte.MAX_VALUE + 1)));
+ cipEqu(Double.valueOf(Byte.MIN_VALUE - 0.9999991),
+ new OverloadedNumberUtil.DoubleOrShort(
+ Byte.MIN_VALUE - 0.9999991, (short) (Byte.MIN_VALUE - 1)));
+
+ cipEqu(Double.valueOf(Byte.MAX_VALUE + 1), new OverloadedNumberUtil.DoubleOrShort((double)
+ (Byte.MAX_VALUE + 1), (short) (Byte.MAX_VALUE + 1)));
+ cipEqu(Double.valueOf(Byte.MIN_VALUE - 1), new OverloadedNumberUtil.DoubleOrShort((double)
+ (Byte.MIN_VALUE - 1), (short) (Byte.MIN_VALUE - 1)));
+
+ cipEqu(Short.MAX_VALUE + 0.9999991,
+ new OverloadedNumberUtil.DoubleOrIntegerOrFloat(Short.MAX_VALUE + 0.9999991, Short.MAX_VALUE + 1));
+ cipEqu(Short.MIN_VALUE - 0.9999991,
+ new OverloadedNumberUtil.DoubleOrIntegerOrFloat(Short.MIN_VALUE - 0.9999991, Short.MIN_VALUE - 1));
+ cipEqu(16777216 + 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrIntegerOrFloat(16777216 + 0.9e-6, 16777216));
+ cipEqu(-16777216 - 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrIntegerOrFloat(-16777216 - 0.9e-6, -16777216));
+
+ cipEqu(Integer.MAX_VALUE + 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrInteger(Integer.MAX_VALUE + 0.9e-6, Integer.MAX_VALUE));
+ cipEqu(Integer.MIN_VALUE - 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrInteger(Integer.MIN_VALUE - 0.9e-6, Integer.MIN_VALUE));
+
+ cipEqu(Integer.MAX_VALUE + 1L + 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrFloat(Integer.MAX_VALUE + 1L + 0.9e-6));
+ cipEqu(Integer.MIN_VALUE - 1L - 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrFloat(Integer.MIN_VALUE - 1L - 0.9e-6));
+ cipEqu(Long.MAX_VALUE + 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrFloat(Long.MAX_VALUE + 0.9e-6));
+ cipEqu(Long.MIN_VALUE - 0.9e-6,
+ new OverloadedNumberUtil.DoubleOrFloat(Long.MIN_VALUE - 0.9e-6));
+ }
+
+ @SuppressWarnings("boxing")
+ public void testFloatCoercion() {
+ cipEqu(1.00002f);
+ cipEqu(-1.00002f);
+ cipEqu(1.999989f);
+ cipEqu(-1.999989f);
+ cipEqu(16777218f);
+ cipEqu(-16777218f);
+
+ cipEqu(1f, new OverloadedNumberUtil.FloatOrByte(1f, (byte) 1));
+ cipEqu(-1f, new OverloadedNumberUtil.FloatOrByte(-1f, (byte) -1));
+ cipEqu(1.000009f, new OverloadedNumberUtil.FloatOrByte(1.000009f, (byte) 1));
+ cipEqu(-1.000009f, new OverloadedNumberUtil.FloatOrByte(-1.000009f, (byte) -1));
+ cipEqu(1.999991f, new OverloadedNumberUtil.FloatOrByte(1.999991f, (byte) 2));
+ cipEqu(-1.999991f, new OverloadedNumberUtil.FloatOrByte(-1.999991f, (byte) -2));
+
+ cipEqu(1000f, new OverloadedNumberUtil.FloatOrShort(1000f, (short) 1000));
+ cipEqu(-1000f, new OverloadedNumberUtil.FloatOrShort(-1000f, (short) -1000));
+ cipEqu(1000.00006f);
+
+ cipEqu(60000f, new OverloadedNumberUtil.FloatOrInteger(60000f, 60000));
+ cipEqu(-60000f, new OverloadedNumberUtil.FloatOrInteger(-60000f, -60000));
+ cipEqu(60000.004f);
+
+ cipEqu(100f, new OverloadedNumberUtil.FloatOrByte(100f, (byte) 100), OverloadedNumberUtil.MASK_KNOWN_INTEGERS);
+ cipEqu(1000f, new OverloadedNumberUtil.FloatOrShort(1000f, (short) 1000), OverloadedNumberUtil.MASK_KNOWN_INTEGERS);
+ cipEqu(60000f, new OverloadedNumberUtil.FloatOrInteger(60000f, 60000), OverloadedNumberUtil.MASK_KNOWN_INTEGERS);
+ cipEqu(60000f, new OverloadedNumberUtil.FloatOrInteger(60000f, 60000), OverloadedNumberUtil.FLAG_LONG);
+ cipEqu((float) Integer.MAX_VALUE, (float) Integer.MAX_VALUE, OverloadedNumberUtil.FLAG_LONG);
+ cipEqu((float) -Integer.MAX_VALUE, (float) -Integer.MAX_VALUE);
+
+ cipEqu(0.5f, 0.5f, OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(0.5f, 0.5f, OverloadedNumberUtil.FLAG_DOUBLE);
+ }
+
+ public void testBigIntegerCoercion() {
+ BigInteger bi;
+
+ cipEqu(BigInteger.ZERO, new OverloadedNumberUtil.BigIntegerOrByte(BigInteger.ZERO));
+ bi = new BigInteger(String.valueOf(Byte.MAX_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrByte(bi));
+ bi = new BigInteger(String.valueOf(Byte.MIN_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrByte(bi));
+
+ bi = new BigInteger(String.valueOf(Byte.MAX_VALUE + 1));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+ bi = new BigInteger(String.valueOf(Byte.MIN_VALUE - 1));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+ bi = new BigInteger(String.valueOf(Short.MAX_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+ bi = new BigInteger(String.valueOf(Short.MIN_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+
+ bi = new BigInteger(String.valueOf(Short.MAX_VALUE + 1));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+ bi = new BigInteger(String.valueOf(Short.MIN_VALUE - 1));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+ bi = new BigInteger(String.valueOf(Integer.MAX_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+ bi = new BigInteger(String.valueOf(Integer.MIN_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+
+ bi = new BigInteger(String.valueOf(Integer.MAX_VALUE + 1L));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+ bi = new BigInteger(String.valueOf(Integer.MIN_VALUE - 1L));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+ bi = new BigInteger(String.valueOf(Long.MAX_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+ bi = new BigInteger(String.valueOf(Long.MIN_VALUE));
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+
+ cipEqu(new BigInteger(String.valueOf(Long.MAX_VALUE)).add(BigInteger.ONE));
+ cipEqu(new BigInteger(String.valueOf(Long.MIN_VALUE)).subtract(BigInteger.ONE));
+
+ bi = new BigInteger("0");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi),
+ OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi),
+ OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi),
+ OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi),
+ OverloadedNumberUtil.FLAG_DOUBLE);
+
+ bi = new BigInteger("16777215");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ bi = new BigInteger("-16777215");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+
+ bi = new BigInteger("16777216");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ bi = new BigInteger("-16777216");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+
+ bi = new BigInteger("16777217");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+ bi = new BigInteger("-16777217");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+
+ bi = new BigInteger("9007199254740991");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+ bi = new BigInteger("-9007199254740991");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+
+ bi = new BigInteger("9007199254740992");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+ bi = new BigInteger("-9007199254740992");
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+
+ bi = new BigInteger("9007199254740993");
+ cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE);
+ bi = new BigInteger("-9007199254740993");
+ cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+ cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE);
+
+ bi = new BigInteger("9007199254740994");
+ cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ bi = new BigInteger("-9007199254740994");
+ cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+ }
+
+ private void cipEqu(Number actualAndExpected) {
+ cipEqu(actualAndExpected, actualAndExpected, -1);
+ }
+
+ private void cipEqu(Number actual, Number expected) {
+ cipEqu(actual, expected, -1);
+ }
+
+ private void cipEqu(Number actualAndExpected, int flags) {
+ cipEqu(actualAndExpected, actualAndExpected, flags);
+ }
+
+ @SuppressWarnings("boxing")
+ private void cipEqu(Number actual, Number expected, int flags) {
+ Number res = OverloadedNumberUtil.addFallbackType(actual, flags);
+ assertEquals(expected.getClass(), res.getClass());
+ assertEquals(expected, res);
+
+ // Some number types wrap the number with multiple types and equals() only compares one of them. So we try to
+ // catch any inconsistency:
+ assertEquals(expected.byteValue(), res.byteValue());
+ assertEquals(expected.shortValue(), res.shortValue());
+ assertEquals(expected.intValue(), res.intValue());
+ assertEquals(expected.longValue(), res.longValue());
+ assertEquals(expected.floatValue(), res.floatValue());
+ assertEquals(expected.doubleValue(), res.doubleValue());
+ }
+
+ public void testGetArgumentConversionPrice() {
+ // Unknown number types:
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ RationalNumber.class, Integer.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, RationalNumber.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ RationalNumber.class, Float.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ Float.class, RationalNumber.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ RationalNumber.class, RationalNumber.class));
+
+ // Fully check some rows (not all of them; the code is generated anyways):
+
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, Byte.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, Short.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, Integer.class));
+ assertEquals(10004, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, Long.class));
+ assertEquals(10005, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, BigInteger.class));
+ assertEquals(30006, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, Float.class));
+ assertEquals(20007, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, Double.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, BigDecimal.class));
+
+ assertEquals(45001, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, Byte.class));
+ assertEquals(44002, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, Short.class));
+ assertEquals(41003, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, Integer.class));
+ assertEquals(41004, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, Long.class));
+ assertEquals(40005, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, BigInteger.class));
+ assertEquals(33006, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, Float.class));
+ assertEquals(32007, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, Double.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, BigDecimal.class));
+
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, Byte.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, Short.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, Integer.class));
+ assertEquals(21004, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, Long.class));
+ assertEquals(21005, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, BigInteger.class));
+ assertEquals(40006, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, Float.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, Double.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, BigDecimal.class));
+
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, Byte.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, Short.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, Integer.class));
+ assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, Long.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, BigInteger.class));
+ assertEquals(40006, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, Float.class));
+ assertEquals(20007, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, Double.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, BigDecimal.class));
+
+ // Check if all fromC is present:
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Byte.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Short.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Integer.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Long.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigInteger.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Float.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ Double.class, BigDecimal.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ BigDecimal.class, BigDecimal.class));
+ assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.IntegerBigDecimal.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrFloat.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.FloatOrByte.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrShort.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.FloatOrByte.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.FloatOrShort.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.FloatOrInteger.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrByte.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrIntegerOrFloat.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrInteger.class, BigDecimal.class));
+ assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.DoubleOrLong.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrByte.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrShort.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrInteger.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrLong.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrFloat.class, BigDecimal.class));
+ assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+ OverloadedNumberUtil.BigIntegerOrDouble.class, BigDecimal.class));
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java b/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
new file mode 100644
index 0000000..6908554
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
@@ -0,0 +1,408 @@
+package freemarker.ext.beans;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+import freemarker.template.utility.NumberUtil;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class ParameterListPreferabilityTest extends TestCase {
+
+ public ParameterListPreferabilityTest(String name) {
+ super(name);
+ }
+
+ public void testNumberical() {
+ // Note: the signature lists consists of the same elements, only their order changes depending on the type
+ // of the argument value.
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { byte.class },
+ new Class[] { Byte.class },
+ new Class[] { short.class },
+ new Class[] { Short.class },
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { long.class },
+ new Class[] { Long.class },
+ new Class[] { BigInteger.class },
+ new Class[] { float.class },
+ new Class[] { Float.class },
+ new Class[] { double.class },
+ new Class[] { Double.class },
+ new Class[] { BigDecimal.class },
+ new Class[] { Number.class },
+ new Class[] { Serializable.class },
+ new Class[] { Object.class }
+ },
+ new Object[] { (byte) 1 });
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { BigDecimal.class },
+ new Class[] { BigInteger.class },
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { long.class },
+ new Class[] { Long.class },
+ new Class[] { double.class },
+ new Class[] { Double.class },
+ new Class[] { float.class },
+ new Class[] { Float.class },
+ new Class[] { short.class },
+ new Class[] { Short.class },
+ new Class[] { byte.class },
+ new Class[] { Byte.class },
+ new Class[] { Number.class },
+ new Class[] { Serializable.class },
+ new Class[] { Object.class },
+ },
+ new Object[] { new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal("1")) });
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { BigDecimal.class },
+ new Class[] { double.class },
+ new Class[] { Double.class },
+ new Class[] { float.class },
+ new Class[] { Float.class },
+ new Class[] { BigInteger.class },
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { long.class },
+ new Class[] { Long.class },
+ new Class[] { short.class },
+ new Class[] { Short.class },
+ new Class[] { byte.class },
+ new Class[] { Byte.class },
+ new Class[] { Number.class },
+ new Class[] { Serializable.class },
+ new Class[] { Object.class },
+ },
+ new Object[] { new BigDecimal("1") /* possibly non-integer */ });
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { float.class },
+ new Class[] { Float.class },
+ new Class[] { double.class },
+ new Class[] { Double.class },
+ new Class[] { BigDecimal.class },
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { long.class },
+ new Class[] { Long.class },
+ new Class[] { short.class },
+ new Class[] { Short.class },
+ new Class[] { byte.class },
+ new Class[] { Byte.class },
+ new Class[] { BigInteger.class },
+ new Class[] { Number.class },
+ new Class[] { Serializable.class },
+ new Class[] { Object.class },
+ },
+ new Object[] { new OverloadedNumberUtil.FloatOrByte(1f, (byte) 1) });
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { long.class },
+ new Class[] { Long.class },
+ new Class[] { BigInteger.class },
+ new Class[] { double.class },
+ new Class[] { Double.class },
+ new Class[] { BigDecimal.class },
+ new Class[] { short.class },
+ new Class[] { Short.class },
+ new Class[] { float.class },
+ new Class[] { Float.class },
+
+ // Two incompatibles removed, would be removed in reality:
+ new Class[] { byte.class },
+ new Class[] { Byte.class },
+
+ new Class[] { Number.class },
+ new Class[] { Serializable.class },
+ new Class[] { Object.class }
+ },
+ new Object[] { new OverloadedNumberUtil.IntegerOrShort(1, (short) 1) });
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { long.class },
+ new Class[] { Long.class },
+ new Class[] { BigInteger.class },
+ new Class[] { BigDecimal.class },
+ new Class[] { double.class },
+ new Class[] { Double.class },
+ new Class[] { float.class },
+ new Class[] { Float.class },
+ // skip byte and short as the would be equal with int (all invalid target types)
+ new Class[] { int.class }, // In reality, this and Integer are removed as not-applicable overloads
+ new Class[] { Integer.class },
+ new Class[] { Number.class },
+ new Class[] { Serializable.class },
+ new Class[] { Object.class }
+ },
+ new Object[] { 1L });
+
+ // Undecidable comparisons:
+
+ testAllCmpPermutationsEqu(
+ new Class[][] {
+ new Class[] { Byte.class },
+ new Class[] { Short.class },
+ new Class[] { Integer.class },
+ new Class[] { Long.class },
+ new Class[] { BigInteger.class },
+ new Class[] { Float.class },
+ },
+ new Object[] { 1.0 });
+
+ testAllCmpPermutationsEqu(
+ new Class[][] {
+ new Class[] { byte.class },
+ new Class[] { short.class },
+ new Class[] { int.class },
+ new Class[] { long.class },
+ new Class[] { float.class },
+ },
+ new Object[] { 1.0 });
+ }
+
+ public void testPrimitiveIsMoreSpecific() {
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { boolean.class },
+ new Class[] { Boolean.class }
+ },
+ new Object[] { true });
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { char.class },
+ new Class[] { Character.class }
+ },
+ new Object[] { 'x' });
+ }
+
+ public void testClassHierarchy() {
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { LinkedHashMap.class },
+ new Class[] { HashMap.class },
+ new Class[] { Map.class },
+ new Class[] { Object.class }
+ },
+ new Object[] { new LinkedHashMap() });
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { LinkedHashMap.class },
+ new Class[] { Cloneable.class },
+ new Class[] { Object.class }
+ },
+ new Object[] { new LinkedHashMap() });
+ }
+
+ public void testNumericalWithNonNumerical() {
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { Comparable.class },
+ new Class[] { Object.class },
+ },
+ new Object[] { 1 });
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { int.class },
+ new Class[] { Integer.class },
+ new Class[] { char.class },
+ new Class[] { Character.class },
+ },
+ new Object[] { 1 });
+ }
+
+ public void testUnrelated() {
+ testAllCmpPermutationsEqu(
+ new Class[][] {
+ new Class[] { Serializable.class },
+ new Class[] { CharSequence.class },
+ new Class[] { Comparable.class }
+ },
+ new Object[] { "s" });
+
+ testAllCmpPermutationsEqu(
+ new Class[][] {
+ new Class[] { HashMap.class },
+ new Class[] { TreeMap.class }
+ },
+ new Object[] { null });
+ }
+
+ public void testMultiParameter() {
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { String.class, String.class, String.class },
+
+ new Class[] { String.class, String.class, Object.class },
+ new Class[] { String.class, Object.class, String.class },
+ new Class[] { Object.class, String.class, String.class },
+
+ new Class[] { String.class, Object.class, Object.class },
+ new Class[] { Object.class, String.class, Object.class },
+ new Class[] { Object.class, Object.class, String.class },
+
+ new Class[] { Object.class, Object.class, Object.class },
+ },
+ new Object[] { "a", "b", "c" });
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { String.class, String.class },
+ new Class[] { String.class, Object.class },
+ new Class[] { CharSequence.class, CharSequence.class },
+ new Class[] { CharSequence.class, Object.class },
+ new Class[] { Object.class, String.class },
+ new Class[] { Object.class, CharSequence.class },
+ new Class[] { Object.class, Object.class },
+ },
+ new Object[] { "a", "b" });
+
+ /** Subclassing is more important than primitive-VS-boxed: */
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { boolean.class, boolean.class, boolean.class, String.class },
+ new Class[] { Boolean.class, boolean.class, boolean.class, String.class },
+ new Class[] { boolean.class, Boolean.class, Boolean.class, String.class },
+ new Class[] { Boolean.class, boolean.class, Boolean.class, String.class },
+ new Class[] { Boolean.class, Boolean.class, boolean.class, String.class },
+ new Class[] { Boolean.class, Boolean.class, Boolean.class, String.class },
+ new Class[] { Boolean.class, Boolean.class, Boolean.class, CharSequence.class },
+ new Class[] { boolean.class, boolean.class, boolean.class, Object.class },
+ new Class[] { Boolean.class, boolean.class, boolean.class, Object.class },
+ new Class[] { boolean.class, Boolean.class, Boolean.class, Object.class },
+ new Class[] { Boolean.class, boolean.class, Boolean.class, Object.class },
+ new Class[] { Boolean.class, Boolean.class, boolean.class, Object.class },
+ new Class[] { Boolean.class, Boolean.class, Boolean.class, Object.class },
+ },
+ new Object[] { true, false, true, "a" });
+
+ /** Subclassing is more important than primitive-VS-boxed: */
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { int.class, int.class, int.class, String.class },
+ new Class[] { Integer.class, int.class, int.class, String.class },
+ new Class[] { int.class, Integer.class, Integer.class, String.class },
+ new Class[] { Integer.class, int.class, Integer.class, String.class },
+ new Class[] { Integer.class, Integer.class, int.class, String.class },
+ new Class[] { Integer.class, Integer.class, Integer.class, String.class },
+ new Class[] { Integer.class, Integer.class, Integer.class, CharSequence.class },
+ new Class[] { int.class, int.class, int.class, Object.class },
+ new Class[] { Integer.class, int.class, int.class, Object.class },
+ new Class[] { int.class, Integer.class, Integer.class, Object.class },
+ new Class[] { Integer.class, int.class, Integer.class, Object.class },
+ new Class[] { Integer.class, Integer.class, int.class, Object.class },
+ new Class[] { Integer.class, Integer.class, Integer.class, Object.class },
+ },
+ new Object[] { 1, 2, 3, "a" });
+ }
+
+ public void testVarargs() {
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { String.class, String[].class },
+ new Class[] { String.class, CharSequence[].class },
+ new Class[] { String.class, Object[].class },
+ },
+ new Object[] { "a", "b", "c" },
+ true);
+
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { String.class, int[].class },
+ new Class[] { String.class, Integer[].class },
+ new Class[] { String.class, long[].class },
+ new Class[] { String.class, Long[].class },
+ new Class[] { String.class, double[].class },
+ new Class[] { String.class, Double[].class },
+ new Class[] { String.class, Serializable[].class },
+ new Class[] { String.class, Object[].class },
+ },
+ new Object[] { "a", 1, 2, 3 },
+ true);
+
+ // 0-long varargs list; in case of ambiguity, the varargs component type decides:
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { String.class, Object[].class },
+ new Class[] { CharSequence.class, int[].class },
+ new Class[] { CharSequence.class, Integer[].class },
+ new Class[] { CharSequence.class, long[].class },
+ new Class[] { CharSequence.class, Long[].class },
+ new Class[] { CharSequence.class, double[].class },
+ new Class[] { CharSequence.class, Double[].class },
+ new Class[] { CharSequence.class, Serializable[].class },
+ new Class[] { CharSequence.class, Object[].class },
+ new Class[] { Object.class, int[].class },
+ },
+ new Object[] { "a" },
+ true);
+
+
+ // Different fixed prefix length; in the case of ambiguity, the one with higher fixed param count wins.
+ testAllCmpPermutationsInc(
+ new Class[][] {
+ new Class[] { String.class, int.class, int.class, int[].class },
+ new Class[] { String.class, int.class, int[].class },
+ new Class[] { String.class, int[].class },
+ },
+ new Object[] { "a", 1, 2, 3 },
+ true);
+ }
+
+ private void testAllCmpPermutationsInc(Class[][] sortedSignatures, Object[] args) {
+ testAllCmpPermutationsInc(sortedSignatures, args, false);
+ }
+
+ /**
+ * Compares all items with all other items in the provided descending sorted array of signatures, checking that
+ * for all valid indexes i and j, where j > i, it stands that sortedSignatures[i] > sortedSignatures[j].
+ * The comparisons are done with both operand orders, also each items is compared to itself too.
+ *
+ * @param sortedSignatures method signatures sorted by decreasing specificity
+ */
+ private void testAllCmpPermutationsInc(Class[][] sortedSignatures, Object[] args, boolean varargs) {
+ final ArgumentTypes argTs = new ArgumentTypes(args, true);
+ for (int i = 0; i < sortedSignatures.length; i++) {
+ for (int j = 0; j < sortedSignatures.length; j++) {
+ assertEquals("sortedSignatures[" + i + "] <==> sortedSignatures [" + j + "]",
+ NumberUtil.getSignum(
+ Integer.valueOf(j).compareTo(i)),
+ NumberUtil.getSignum(
+ argTs.compareParameterListPreferability(
+ sortedSignatures[i], sortedSignatures[j], varargs)));
+ }
+ }
+ }
+
+ private void testAllCmpPermutationsEqu(Class[][] signatures, Object[] args) {
+ final ArgumentTypes argTs = new ArgumentTypes(args, true);
+ for (int i = 0; i < signatures.length; i++) {
+ for (int j = 0; j < signatures.length; j++) {
+ assertEquals("sortedSignatures[" + i + "] <==> sortedSignatures [" + j + "]",
+ 0,
+ argTs.compareParameterListPreferability(signatures[i], signatures[j], false));
+ }
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/RationalNumber.java b/src/test/java/freemarker/ext/beans/RationalNumber.java
new file mode 100644
index 0000000..b4aa63d
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/RationalNumber.java
@@ -0,0 +1,71 @@
+package freemarker.ext.beans;
+
+public final class RationalNumber extends Number {
+
+ final int divident;
+ final int divisor;
+
+ public RationalNumber(int divident, int divisor) {
+ this.divident = divident;
+ this.divisor = divisor;
+ }
+
+ @Override
+ public int intValue() {
+ return divident / divisor;
+ }
+
+ @Override
+ public long longValue() {
+ return divident / (long) divisor;
+ }
+
+ @Override
+ public float floatValue() {
+ return (float) (divident / (double) divisor);
+ }
+
+ @Override
+ public double doubleValue() {
+ return divident / (double) divisor;
+ }
+
+ public int getDivident() {
+ return divident;
+ }
+
+ public int getDivisor() {
+ return divisor;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + divident;
+ result = prime * result + divisor;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ RationalNumber other = (RationalNumber) obj;
+ if (divident != other.divident)
+ return false;
+ if (divisor != other.divisor)
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return divident + "/" + divisor;
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/template/utility/NumberUtilTest.java b/src/test/java/freemarker/template/utility/NumberUtilTest.java
index 3759cd6..2bfb3ea 100644
--- a/src/test/java/freemarker/template/utility/NumberUtilTest.java
+++ b/src/test/java/freemarker/template/utility/NumberUtilTest.java
@@ -60,5 +60,37 @@
assertEquals(0, NumberUtil.getSignum(BigInteger.valueOf(0)));
assertEquals(-1, NumberUtil.getSignum(BigInteger.valueOf(-3)));
}
+
+ public void testIsBigDecimalInteger() {
+ BigDecimal n1 = new BigDecimal("1.125");
+ if (n1.precision() != 4 || n1.scale() != 3) {
+ throw new RuntimeException("Wrong: " + n1);
+ }
+ BigDecimal n2 = new BigDecimal("1.125").subtract(new BigDecimal("0.005"));
+ if (n2.precision() != 4 || n2.scale() != 3) {
+ throw new RuntimeException("Wrong: " + n2);
+ }
+ BigDecimal n3 = new BigDecimal("123");
+ BigDecimal n4 = new BigDecimal("6000");
+ BigDecimal n5 = new BigDecimal("1.12345").subtract(new BigDecimal("0.12345"));
+ if (n5.precision() != 6 || n5.scale() != 5) {
+ throw new RuntimeException("Wrong: " + n5);
+ }
+ BigDecimal n6 = new BigDecimal("0");
+ BigDecimal n7 = new BigDecimal("0.001").subtract(new BigDecimal("0.001"));
+ BigDecimal n8 = new BigDecimal("60000.5").subtract(new BigDecimal("0.5"));
+ BigDecimal n9 = new BigDecimal("6").movePointRight(3).setScale(-3);
+
+ BigDecimal[] ns = new BigDecimal[] {
+ n1, n2, n3, n4, n5, n6, n7, n8, n9,
+ n1.negate(), n2.negate(), n3.negate(), n4.negate(), n5.negate(), n6.negate(), n7.negate(), n8.negate(),
+ n9.negate(),
+ };
+
+ for (BigDecimal n : ns) {
+ assertEquals(n.doubleValue() == n.longValue(), NumberUtil.isBigDecimalInteger(n));
+ }
+
+ }
}
diff --git a/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java b/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
index d563d8d..8b1f31d 100644
--- a/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
+++ b/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
@@ -91,6 +91,7 @@
import freemarker.template.TemplateNodeModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.NullWriter;
import freemarker.template.utility.StringUtil;
import freemarker.test.templatesuite.models.BooleanAndStringTemplateModel;
@@ -119,11 +120,17 @@
private final boolean noOutput;
private Configuration conf = new Configuration();
+
+ public TemplateTestCase(String name) {
+ this(name, null, null, false);
+ }
public TemplateTestCase(String name, String templateName, String expectedFileName, boolean noOutput) {
super(name);
- this.templateName = templateName;
- this.expectedFileName = expectedFileName;
+
+ NullArgumentException.check("name", name);
+ this.templateName = templateName != null ? templateName : name + ".ftl";
+ this.expectedFileName = expectedFileName != null ? expectedFileName : name + ".txt";
this.noOutput = noOutput;
}
diff --git a/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java b/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java
index 2d4d30e..849562d 100644
--- a/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java
+++ b/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java
@@ -136,7 +136,7 @@
}
}
if (n.getNodeName().equals("testcase")) {
- TestCase tc = createTestCaseFromNode((Element) n, filter);
+ TemplateTestCase tc = createTestCaseFromNode((Element) n, filter);
if (tc != null) addTest(tc);
}
}
@@ -164,44 +164,32 @@
* it must extend {@link TestCase} and have a constructor with the same parameters as of
* {@link TemplateTestCase#TemplateTestCase(String, String, String, boolean)}.
*/
- private TestCase createTestCaseFromNode(Element e, Pattern filter) throws Exception {
+ private TemplateTestCase createTestCaseFromNode(Element e, Pattern filter) throws Exception {
String name = StringUtil.emptyToNull(e.getAttribute("name"));
if (name == null) throw new Exception("Invalid XML: the \"name\" attribute is mandatory.");
if (filter != null && !filter.matcher(name).matches()) return null;
String templateName = StringUtil.emptyToNull(e.getAttribute("template"));
- if (templateName == null) templateName = name + ".ftl";
String expectedFileName = StringUtil.emptyToNull(e.getAttribute("expected"));
- if (expectedFileName == null) expectedFileName = name + ".txt";
String noOutputStr = StringUtil.emptyToNull(e.getAttribute("nooutput"));
boolean noOutput = noOutputStr == null ? false : StringUtil.getYesNo(noOutputStr);
- String classname = StringUtil.emptyToNull(e.getAttribute("class"));
-
- if (classname != null) {
- Class cl = Class.forName(classname);
- Constructor cons = cl.getConstructor(new Class[] {
- String.class, String.class, String.class, boolean.class});
- return (TestCase) cons.newInstance(new Object [] {
- name, templateName, expectedFileName, Boolean.valueOf(noOutput)});
- } else {
- TemplateTestCase result = new TemplateTestCase(name, templateName, expectedFileName, noOutput);
- for (Iterator it=configParams.entrySet().iterator(); it.hasNext();) {
- Map.Entry entry = (Map.Entry) it.next();
- result.setConfigParam(entry.getKey().toString(), entry.getValue().toString());
- }
- NodeList configs = e.getElementsByTagName("config");
- for (int i=0; i<configs.getLength(); i++) {
- NamedNodeMap atts = configs.item(i).getAttributes();
- for (int j=0; j<atts.getLength(); j++) {
- Attr att = (Attr) atts.item(j);
- result.setConfigParam(att.getName(), att.getValue());
- }
- }
- return result;
+ TemplateTestCase result = new TemplateTestCase(name, templateName, expectedFileName, noOutput);
+ for (Iterator it=configParams.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ result.setConfigParam(entry.getKey().toString(), entry.getValue().toString());
}
+ NodeList configs = e.getElementsByTagName("config");
+ for (int i=0; i<configs.getLength(); i++) {
+ NamedNodeMap atts = configs.item(i).getAttributes();
+ for (int j=0; j<atts.getLength(); j++) {
+ Attr att = (Attr) atts.item(j);
+ result.setConfigParam(att.getName(), att.getValue());
+ }
+ }
+ return result;
}
public static void main (String[] args) throws Exception {
diff --git a/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java b/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java
index d7633b2..995828e 100644
--- a/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java
+++ b/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java
@@ -1,9 +1,18 @@
package freemarker.test.templatesuite.models;
import java.io.File;
+import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
+import freemarker.ext.beans.RationalNumber;
+import freemarker.ext.util.WrapperTemplateModel;
+import freemarker.template.AdapterTemplateModel;
+import freemarker.template.TemplateBooleanModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.StringUtil;
+
public class OverloadedMethods2 {
public String mVarargs(String... a1) {
@@ -17,6 +26,10 @@
public BigInteger bigInteger(BigDecimal n) {
return n.toBigInteger();
}
+
+ public RationalNumber rational(int a, int b) {
+ return new RationalNumber(a, b);
+ }
public String mVarargs(File a1, String... a2) {
return "mVarargs(File a1, String... a2)";
@@ -223,4 +236,345 @@
return "mDecimalLoss(double a1 = " + a1 + ")";
}
+ public String mNumConversionLoses1(byte i, Object o1, Object o2) {
+ return "byte " + i;
+ }
+
+ public String mNumConversionLoses1(double i, Object o1, Object o2) {
+ return "double " + i;
+ }
+
+ public String mNumConversionLoses1(Number i, String o1, String o2) {
+ return "Number " + i + " " + i.getClass().getName();
+ }
+
+ public String mNumConversionLoses2(int i, Object o1, Object o2) {
+ return "int " + i;
+ }
+
+ public String mNumConversionLoses2(long i, Object o1, Object o2) {
+ return "long " + i;
+ }
+
+ public String mNumConversionLoses2(Number i, String o1, String o2) {
+ return "Number " + i + " " + i.getClass().getName();
+ }
+
+ public String mNumConversionLoses3(int i, Object o1, Object o2) {
+ return "int " + i;
+ }
+
+ public String mNumConversionLoses3(Serializable i, String o1, String o2) {
+ return "Serializable " + i + " " + i.getClass().getName();
+ }
+
+ public String nIntAndLong(int i) {
+ return "nIntAndLong(int " + i + ")";
+ }
+
+ public String nIntAndLong(long i) {
+ return "nIntAndLong(long " + i + ")";
+ }
+
+ public String nIntAndShort(int i) {
+ return "nIntAndShort(int " + i + ")";
+ }
+
+ public String nIntAndShort(short i) {
+ return "nIntAndShort(short " + i + ")";
+ }
+
+ public String nLongAndShort(long i) {
+ return "nLongAndShort(long " + i + ")";
+ }
+
+ public String nLongAndShort(short i) {
+ return "nLongAndShort(short " + i + ")";
+ }
+
+ public String varargs1(String s, int... xs) {
+ return "varargs1(String s = " + StringUtil.jQuote(s) + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs1(String s, double... xs) {
+ return "varargs1(String s = " + StringUtil.jQuote(s) + ", double... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs1(String s, Object... xs) {
+ return "varargs1(String s = " + StringUtil.jQuote(s) + ", Object... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs1(Object s, Object... xs) {
+ return "varargs1(Object s = " + s + ", Object... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs2(int... xs) {
+ return "varargs2(int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs2(double... xs) {
+ return "varargs2(double... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs3(String... xs) {
+ return "varargs3(String... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs3(Comparable... xs) {
+ return "varargs3(Comparable... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs3(Object... xs) {
+ return "varargs3(Object... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs4(Integer... xs) {
+ return "varargs4(Integer... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs4(int... xs) {
+ return "varargs4(int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs5(int... xs) {
+ return "varargs5(int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs5(int a1, int... xs) {
+ return "varargs5(int a1 = " + a1 + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs5(int a1, int a2, int... xs) {
+ return "varargs5(int a1 = " + a1 + ", int a2 = " + a2 + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs5(int a1, int a2, int a3, int... xs) {
+ return "varargs5(int a1 = " + a1 + ", int a2 = " + a2 + ", int a3 = " + a3
+ + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs6(String a1, int... xs) {
+ return "varargs6(String a1 = " + a1 + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs6(Object a1, int a2, int... xs) {
+ return "varargs6(Object a1 = " + a1 + ", int a2 = " + a2 + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs7(int... xs) {
+ return "varargs7(int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ public String varargs7(short a1, int... xs) {
+ return "varargs7(short a1 = " + a1 + ", int... xs = [" + arrayToString(xs) + "])";
+ }
+
+ private String arrayToString(int[] xs) {
+ StringBuilder sb = new StringBuilder();
+ for (int x : xs) {
+ if (sb.length() != 0) sb.append(", ");
+ sb.append(x);
+ }
+ return sb.toString();
+ }
+
+ private String arrayToString(double[] xs) {
+ StringBuilder sb = new StringBuilder();
+ for (double x : xs) {
+ if (sb.length() != 0) sb.append(", ");
+ sb.append(x);
+ }
+ return sb.toString();
+ }
+
+ private String arrayToString(Object[] xs) {
+ StringBuilder sb = new StringBuilder();
+ for (Object x : xs) {
+ if (sb.length() != 0) sb.append(", ");
+ sb.append(x);
+ }
+ return sb.toString();
+ }
+
+ public String mNullAmbiguous(String s) {
+ return "mNullAmbiguous(String s = " + s + ")";
+ }
+
+ public String mNullAmbiguous(int i) {
+ return "mNullAmbiguous(int i = " + i + ")";
+ }
+
+ public String mNullAmbiguous(File f) {
+ return "mNullAmbiguous(File f = " + f + ")";
+ }
+
+ public String mNullAmbiguous2(String s) {
+ return "mNullNonAmbiguous(String s = " + s + ")";
+ }
+
+ public String mNullAmbiguous2(File f) {
+ return "mNullAmbiguous(File f = " + f + ")";
+ }
+
+ public String mNullAmbiguous2(Object o) {
+ return "mNullAmbiguous(Object o = " + o + ")";
+ }
+
+ public String mNullNonAmbiguous(String s) {
+ return "mNullNonAmbiguous(String s = " + s + ")";
+ }
+
+ public String mNullNonAmbiguous(int i) {
+ return "mNullNonAmbiguous(int i = " + i + ")";
+ }
+
+ public String mVarargsIgnoredTail(int i, double... ds) {
+ return "mVarargsIgnoredTail(int i = " + i + ", double... ds = [" + arrayToString(ds) + "])";
+ }
+
+ public String mVarargsIgnoredTail(int... is) {
+ return "mVarargsIgnoredTail(int... is = [" + arrayToString(is) + "])";
+ }
+
+ public String mLowRankWins(int x, int y, Object o) {
+ return "mLowRankWins(int x = " + x + ", int y = " + y + ", Object o = " + o + ")";
+ }
+
+ public String mLowRankWins(Integer x, Integer y, String s) {
+ return "mLowRankWins(Integer x = " + x + ", Integer y = " + y + ", String s = " + s + ")";
+ }
+
+ public String mRareWrappings(File f, double d1, Double d2, double d3, boolean b) {
+ return "mRareWrappings(File f = " + f + ", double d1 = " + d1 + ", Double d2 = " + d2
+ + ", double d3 = " + d3 + ", b = " + b + ")";
+ }
+
+ public String mRareWrappings(Object o, double d1, Double d2, Double d3, boolean b) {
+ return "mRareWrappings(Object o = " + o + ", double d1 = " + d1 + ", Double d2 = " + d2
+ + ", double d3 = " + d3 + ", b = " + b + ")";
+ }
+
+ public String mRareWrappings(String s, double d1, Double d2, Double d3, boolean b) {
+ return "mRareWrappings(String s = " + s + ", double d1 = " + d1 + ", Double d2 = " + d2
+ + ", double d3 = " + d3 + ", b = " + b + ")";
+ }
+
+ public String mRareWrappings2(String s) {
+ return "mRareWrappings2(String s = " + s + ")";
+ }
+
+ public String mRareWrappings2(byte b) {
+ return "mRareWrappings2(byte b = " + b + ")";
+ }
+
+ public File getFile() {
+ return new File("file");
+ }
+
+ public TemplateNumberModel getAdaptedNumber() {
+ return new MyAdapterNumberModel();
+ }
+
+ public TemplateNumberModel getWrapperNumber() {
+ return new MyWrapperNumberModel();
+ }
+
+ public TemplateBooleanModel getStringAdaptedToBoolean() {
+ return new MyStringAdaptedToBooleanModel();
+ }
+
+ public TemplateBooleanModel getStringAdaptedToBoolean2() {
+ return new MyStringAdaptedToBooleanModel2();
+ }
+
+ public TemplateBooleanModel getStringWrappedAsBoolean() {
+ return new MyStringWrapperAsBooleanModel();
+ }
+
+ public TemplateBooleanModel getBooleanWrappedAsAnotherBoolean() {
+ return new MyBooleanWrapperAsAnotherBooleanModel();
+ }
+
+ private static class MyAdapterNumberModel implements TemplateNumberModel, AdapterTemplateModel {
+
+ public Object getAdaptedObject(Class hint) {
+ if (hint == double.class) {
+ return Double.valueOf(123.0001);
+ } else if (hint == Double.class) {
+ return Double.valueOf(123.0002);
+ } else {
+ return Long.valueOf(124L);
+ }
+ }
+
+ public Number getAsNumber() throws TemplateModelException {
+ return Integer.valueOf(122);
+ }
+
+ }
+
+ private static class MyWrapperNumberModel implements TemplateNumberModel, WrapperTemplateModel {
+
+ public Number getAsNumber() throws TemplateModelException {
+ return Integer.valueOf(122);
+ }
+
+ public Object getWrappedObject() {
+ return Double.valueOf(123.0001);
+ }
+
+ }
+
+ private static class MyStringWrapperAsBooleanModel implements TemplateBooleanModel, WrapperTemplateModel {
+
+ public Object getWrappedObject() {
+ return "yes";
+ }
+
+ public boolean getAsBoolean() throws TemplateModelException {
+ return true;
+ }
+
+ }
+
+ private static class MyBooleanWrapperAsAnotherBooleanModel implements TemplateBooleanModel, WrapperTemplateModel {
+
+ public Object getWrappedObject() {
+ return Boolean.TRUE;
+ }
+
+ public boolean getAsBoolean() throws TemplateModelException {
+ return false;
+ }
+
+ }
+
+ private static class MyStringAdaptedToBooleanModel implements TemplateBooleanModel, AdapterTemplateModel {
+
+ public Object getAdaptedObject(Class hint) {
+ if (hint != Boolean.class && hint != boolean.class) {
+ return "yes";
+ } else {
+ return Boolean.TRUE;
+ }
+ }
+
+ public boolean getAsBoolean() throws TemplateModelException {
+ return false;
+ }
+
+ }
+
+ private static class MyStringAdaptedToBooleanModel2 implements TemplateBooleanModel, AdapterTemplateModel {
+
+ public Object getAdaptedObject(Class hint) {
+ return "yes";
+ }
+
+ public boolean getAsBoolean() throws TemplateModelException {
+ return true;
+ }
+
+ }
+
}
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl
index 35b6f1e..3200723 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl
@@ -3,5 +3,18 @@
<@assertFails message="no compatible overloaded">${obj.mIntPrimVSBoxed(123?long)}</@>
<@assertEquals actual=obj.mIntPrimVSBoxed(123?short) expected="mIntPrimVSBoxed(int a1 = 123)" />
<@assertEquals actual=obj.mIntPrimVSBoxed(123) expected="mIntPrimVSBoxed(int a1 = 123)" />
+<@assertEquals actual=obj.varargs4(1, 2, 3) expected='varargs4(int... xs = [1, 2, 3])' />
+
+<@assertFails message="multiple compatible overloaded">${obj.mVarargsIgnoredTail(1, 2, 3)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.mVarargsIgnoredTail(1, 2, 3.5)}</@>
+
+<@assertEquals actual=obj.mLowRankWins(1, 2, 'a') expected='mLowRankWins(int x = 1, int y = 2, Object o = a)' />
+
+<@assertEquals actual=obj.mRareWrappings(obj.file, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringWrappedAsBoolean)
+ expected='mRareWrappings(File f = file, double d1 = 123.0001, Double d2 = 123.0002, double d3 = 124.0, b = true)' />
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.stringWrappedAsBoolean, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringAdaptedToBoolean)}</@>
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, obj.booleanWrappedAsAnotherBoolean)}</@>
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.adaptedNumber, 0, 0, 0, !obj.booleanWrappedAsAnotherBoolean)}</@>
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, !obj.stringAdaptedToBoolean)}</@>
<#include 'overloaded-methods-2-ici-2.3.20.ftl'>
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl
index 3d3c941..2a1c34e 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl
@@ -74,4 +74,80 @@
<@assertEquals actual=obj.mNumBoxedFallbackToNumber('x') expected="mNumBoxedFallbackToNumber(Object a1 = x)" />
<@assertEquals actual=obj.mDecimalLoss(1.5) expected="mDecimalLoss(int a1 = 1)" /><#-- Yes, buggy... -->
-<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
\ No newline at end of file
+<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
+
+<#-- BigDecimal conversions chose the smallest target type before IcI 2.3.31, increasing the risk of overflows: -->
+<@assertEquals actual=obj.nIntAndLong(1) expected="nIntAndLong(int 1)" />
+<@assertEquals actual=obj.nIntAndLong(1?long) expected="nIntAndLong(long 1)" />
+<@assertEquals actual=obj.nIntAndShort(1) expected="nIntAndShort(short 1)" />
+<@assertEquals actual=obj.nIntAndShort(1?short) expected="nIntAndShort(short 1)" />
+<@assertEquals actual=obj.nIntAndShort(1?int) expected="nIntAndShort(int 1)" />
+<@assertEquals actual=obj.nLongAndShort(1) expected="nLongAndShort(short 1)" />
+<@assertEquals actual=obj.nLongAndShort(1?short) expected="nLongAndShort(short 1)" />
+<@assertEquals actual=obj.nLongAndShort(1?long) expected="nLongAndShort(long 1)" />
+
+<#-- Usual wrong choice on null: -->
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3.5) expected='varargs1(Object s = null, Object... xs = [1, 2, 3.5])' />
+
+<#-- Some bugs that cause loosing of decimals will occur here... -->
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1, 2, 3.5)}</@>
+<@assertEquals actual=obj.varargs1('s', 1, 2, 'c') expected='varargs1(String s = "s", Object... xs = [1, 2, c])' />
+<@assertEquals actual=obj.varargs1('s', 1, 'b', 3) expected='varargs1(String s = "s", Object... xs = [1, b, 3])' />
+<@assertEquals actual=obj.varargs1('s', 'a', 2, 3) expected='varargs1(String s = "s", Object... xs = [a, 2, 3])' />
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1, 2, 3)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1.1, 2.1, 3.1)}</@>
+<@assertEquals actual=obj.varargs1('s', 'a', 'b', 'c') expected='varargs1(String s = "s", Object... xs = [a, b, c])' />
+<@assertFails message="multiple compatible overloaded"><@assertEquals actual=obj.varargs1('s', 1?double, 2?byte, 3?byte) expected='varargs1(String s = "s", int... xs = [1, 2, 3])' /></@>
+<@assertEquals actual=obj.varargs1(0, 1, 2, 3) expected='varargs1(Object s = 0, Object... xs = [1, 2, 3])' />
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1?double, 2?double, 3?double)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s')}</@>
+
+<@assertEquals actual=obj.varargs2(1, 2.5, 3) expected='varargs2(int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs2(1, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?int, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2?double, 3) expected='varargs2(double... xs = [1.0, 2.0, 3.0])' />
+
+<@assertEquals actual=obj.varargs3(1, 2, 3) expected='varargs3(Comparable... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 'c') expected='varargs3(String... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs3(1, 'b', 'c') expected='varargs3(Comparable... xs = [1, b, c])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 3) expected='varargs3(Comparable... xs = [a, b, 3])' />
+<@assertEquals actual=obj.varargs3('a', [], 3) expected='varargs3(Object... xs = [a, [], 3])' />
+<@assertEquals actual=obj.varargs3(null, 'b', null) expected='varargs3(Object... xs = [null, b, null])' />
+<@assertEquals actual=obj.varargs3(null, 2, null) expected='varargs3(Object... xs = [null, 2, null])' />
+<@assertEquals actual=obj.varargs3(null, [], null) expected='varargs3(Object... xs = [null, [], null])' />
+<@assertEquals actual=obj.varargs3(null, null, null) expected='varargs3(Object... xs = [null, null, null])' />
+<@assertEquals actual=obj.varargs3() expected='varargs3(String... xs = [])' />
+
+<@assertFails message="no compatible overloaded">${obj.varargs4(null, null, null)}</@>
+
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2, 3, 4, 5)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2, 3, 4)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2, 3)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1)}</@>
+<@assertEquals actual=obj.varargs5() expected='varargs5(int... xs = [])' />
+
+<@assertEquals actual=obj.varargs6('s', 2) expected='varargs6(String a1 = s, int... xs = [2])' />
+<@assertEquals actual=obj.varargs6('s') expected='varargs6(String a1 = s, int... xs = [])' />
+<@assertEquals actual=obj.varargs6(1, 2) expected='varargs6(Object a1 = 1, int a2 = 2, int... xs = [])' />
+<@assertFails message="no compatible overloaded">${obj.varargs6(1)}</@>
+
+<@assertEquals actual=obj.varargs7(1?int, 2?int) expected='varargs7(int... xs = [1, 2])' />
+<@assertEquals actual=obj.varargs7(1?short, 2?int) expected='varargs7(short a1 = 1, int... xs = [2])' />
+
+<@assertEquals actual=obj.mNullAmbiguous('a') expected='mNullAmbiguous(String s = a)' />
+<@assertEquals actual=obj.mNullAmbiguous(123) expected='mNullAmbiguous(int i = 123)' />
+<@assertEquals actual=obj.mNullAmbiguous(1.9) expected='mNullAmbiguous(int i = 1)' />
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(1?double)}</@>
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(1.9?double)}</@>
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(null)}</@>
+
+<@assertEquals actual=obj.mNullAmbiguous2(null) expected='mNullAmbiguous(Object o = null)' />
+
+<@assertFails message="no compatible overloaded">${obj.mNullNonAmbiguous(null)}</@>
+
+<@assertEquals actual=obj.mRareWrappings(obj.stringAdaptedToBoolean2, obj.wrapperNumber, obj.wrapperNumber, obj.wrapperNumber, obj.stringAdaptedToBoolean2)
+ expected='mRareWrappings(String s = yes, double d1 = 123.0001, Double d2 = 123.0001, double d3 = 123.0001, b = true)' />
+
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings2(obj.adaptedNumber)}</@>
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl
index 6e18758..d31fa67 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl
@@ -6,8 +6,8 @@
<@assertEquals actual=obj.mNull1(123) expected="mNull1(int a1 = 123)" />
<@assertEquals actual=obj.mNull2(null) expected="mNull2(String a1 = null)" />
<@assertEquals actual=obj.mVarargs('a', null) expected="mVarargs(String... a1 = anull)" />
-<@assertFails message="multiple compatible overloaded">${obj.mVarargs(null, 'a')}</@>
-<@assertFails message="multiple compatible overloaded">${obj.mSpecificity('a', 'b')}</@>
+<@assertEquals actual=obj.mVarargs(null, 'a') expected="mVarargs(File a1, String... a2)" />
+<@assertEquals actual=obj.mSpecificity('a', 'b') expected="mSpecificity(String a1, Object a2)" />
<@assertEquals actual=obj.mChar('a') expected='mChar(char a1 = a)' />
<@assertEquals actual=obj.mBoolean(true) expected="mBoolean(boolean a1 = true)" />
@@ -16,25 +16,25 @@
<@assertEquals actual=obj.mIntNonOverloaded(123?long) expected=123 />
<@assertEquals actual=obj.mIntNonOverloaded(123) expected=123 />
<@assertEquals actual=obj.mIntNonOverloaded(123.5) expected=123 />
-<@assertEquals actual=obj.mIntNonOverloaded(2147483648) expected=-2147483648 /> <#-- FIXME overflow -->
-<@assertFails message="no compatible overloaded"><@assertEquals actual=obj.mNumBoxedVSBoxed(123.5) expected=123 /></@><#-- FIXME -->
-<@assertFails message="no compatible overloaded"><@assertEquals actual=obj.mNumBoxedVSBoxed(123?int) expected=123 /></@><#-- FIXME -->
+<@assertEquals actual=obj.mIntNonOverloaded(2147483648) expected=-2147483648 /> <#-- overflow -->
+<@assertEquals actual=obj.mNumBoxedVSBoxed(123.5) expected='mNumBoxedVSBoxed(Long a1 = 123)' />
+<@assertEquals actual=obj.mNumBoxedVSBoxed(123?int) expected='mNumBoxedVSBoxed(Long a1 = 123)' />
<@assertEquals actual=obj.mNumBoxedVSBoxed(123?long) expected="mNumBoxedVSBoxed(Long a1 = 123)" />
<@assertEquals actual=obj.mNumBoxedVSBoxed(123?short) expected="mNumBoxedVSBoxed(Short a1 = 123)" />
<@assertEquals
- actual=obj.mNumUnambigous(2147483648) expected="mNumUnambigous(Integer a1 = -2147483648)" /> <#-- FIXME overflow -->
+ actual=obj.mNumUnambigous(2147483648) expected="mNumUnambigous(Integer a1 = -2147483648)" /> <#-- overflow -->
<@assertEquals actual=obj.mIntPrimVSBoxed(123?int) expected="mIntPrimVSBoxed(int a1 = 123)" />
<@assertEquals actual=obj.mIntPrimVSBoxed(123?short) expected="mIntPrimVSBoxed(int a1 = 123)" />
<@assertEquals actual=obj.mIntPrimVSBoxed(123) expected="mIntPrimVSBoxed(int a1 = 123)" />
-<#-- This doesn't fail as the hint will be Integer, and thus unwrapping will truncate the Long to that: -->
+<#-- This doesn't fail as 123L can be converted to int without loss: -->
<@assertEquals actual=obj.mIntPrimVSBoxed(123?long) expected="mIntPrimVSBoxed(int a1 = 123)" />
<@assertEquals actual=obj.mNumPrimVSPrim(123?short) expected="mNumPrimVSPrim(short a1 = 123)" />
<@assertEquals actual=obj.mNumPrimVSPrim(123?int) expected="mNumPrimVSPrim(long a1 = 123)" />
<@assertEquals actual=obj.mNumPrimVSPrim(123?long) expected="mNumPrimVSPrim(long a1 = 123)" />
-<@assertFails message="no compatible overloaded">${obj.mNumPrimVSPrim(123?double)}</@><#-- FIXME? -->
-<@assertEquals actual=obj.mNumPrimVSPrim(123456) expected="mNumPrimVSPrim(short a1 = -7616)" /><#-- FIXME overflow, had to choose long -->
+<@assertEquals actual=obj.mNumPrimVSPrim(123?double) expected="mNumPrimVSPrim(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimVSPrim(123456) expected="mNumPrimVSPrim(long a1 = 123456)" />
<@assertEquals actual=obj.mNumPrimAll(123?byte) expected="mNumPrimAll(byte a1 = 123)" />
<@assertEquals actual=obj.mNumPrimAll(123?short) expected="mNumPrimAll(short a1 = 123)" />
@@ -42,7 +42,7 @@
<@assertEquals actual=obj.mNumPrimAll(123?long) expected="mNumPrimAll(long a1 = 123)" />
<@assertEquals actual=obj.mNumPrimAll(123?float) expected="mNumPrimAll(float a1 = 123.0)" />
<@assertEquals actual=obj.mNumPrimAll(123?double) expected="mNumPrimAll(double a1 = 123.0)" />
-<@assertFails message="multiple compatible overloaded">${obj.mNumPrimAll(123)}</@><#-- FIXME -->
+<@assertEquals actual=obj.mNumPrimAll(123) expected="mNumPrimAll(BigDecimal a1 = 123)" />
<@assertEquals actual=obj.mNumPrimAll(obj.bigInteger(123)) expected="mNumPrimAll(BigInteger a1 = 123)" />
<@assertEquals actual=obj.mNumBoxedAll(123?byte) expected="mNumBoxedAll(Byte a1 = 123)" />
@@ -61,36 +61,144 @@
<@assertEquals actual=obj.mNumPrimAll2nd(123?float) expected="mNumPrimAll2nd(double a1 = 123.0)" />
<@assertEquals actual=obj.mNumPrimAll2nd(123?double) expected="mNumPrimAll2nd(double a1 = 123.0)" />
-<#-- FIXME
<@assertEquals actual=obj.mNumBoxedAll2nd(123?byte) expected="mNumBoxedAll2nd(Short a1 = 123)" />
<@assertEquals actual=obj.mNumBoxedAll2nd(123?short) expected="mNumBoxedAll2nd(Short a1 = 123)" />
<@assertEquals actual=obj.mNumBoxedAll2nd(123?int) expected="mNumBoxedAll2nd(Long a1 = 123)" />
<@assertEquals actual=obj.mNumBoxedAll2nd(123?long) expected="mNumBoxedAll2nd(Long a1 = 123)" />
<@assertEquals actual=obj.mNumBoxedAll2nd(123?float) expected="mNumBoxedAll2nd(Double a1 = 123.0)" />
<@assertEquals actual=obj.mNumBoxedAll2nd(123?double) expected="mNumBoxedAll2nd(Double a1 = 123.0)" />
- -->
<@assertEquals actual=obj.mNumPrimFallbackToNumber(123?int) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
<@assertEquals actual=obj.mNumPrimFallbackToNumber(123?long) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
-<@assertEquals actual=obj.mNumPrimFallbackToNumber(123?double) expected="mNumPrimFallbackToNumber(Number a1 = 123.0)" />
-<#-- FIXME
-<@assertEquals actual=obj.mNumPrimFallbackToNumber(123) expected="mNumPrimFallbackToNumber(Number a1 = 123)" />
--->
-<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.bigInteger(123)) expected="mNumPrimFallbackToNumber(Number a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(123?double) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(123.5?double) expected="mNumPrimFallbackToNumber(Number a1 = 123.5)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(123) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.bigInteger(123)) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.bigInteger(9223372036854775808))
+ expected="mNumPrimFallbackToNumber(Number a1 = 9223372036854775808)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.rational(246, 2)) expected="mNumPrimFallbackToNumber(Number a1 = 246/2)" />
<@assertEquals actual=obj.mNumPrimFallbackToNumber('x') expected="mNumPrimFallbackToNumber(Object a1 = x)" />
-<#-- FIXME
<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?int) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
--->
<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?long) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
-<#-- FIXME
-<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?double) expected="mNumBoxedFallbackToNumber(Number a1 = 123.0)" />
--->
-<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123) expected="mNumBoxedFallbackToNumber(Number a1 = 123)" />
-<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.bigInteger(123)) expected="mNumBoxedFallbackToNumber(Number a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?double) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123.5?double) expected="mNumBoxedFallbackToNumber(Number a1 = 123.5)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.bigInteger(123)) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.bigInteger(9223372036854775808))
+ expected="mNumBoxedFallbackToNumber(Number a1 = 9223372036854775808)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.rational(246, 2)) expected="mNumBoxedFallbackToNumber(Number a1 = 246/2)" />
<@assertEquals actual=obj.mNumBoxedFallbackToNumber('x') expected="mNumBoxedFallbackToNumber(Object a1 = x)" />
-<#-- FIXME
<@assertEquals actual=obj.mDecimalLoss(1.5) expected="mDecimalLoss(double a1 = 1.5)" />
--->
-<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
\ No newline at end of file
+<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
+
+<@assertEquals actual=obj.mNumConversionLoses1(1?double, '', '') expected="Number 1.0 java.lang.Double" />
+<@assertEquals actual=obj.mNumConversionLoses1(1?short, '', '') expected="Number 1 java.lang.Short" />
+<@assertEquals actual=obj.mNumConversionLoses1(1?long, '', '') expected="Number 1 java.lang.Long" />
+<@assertEquals actual=obj.mNumConversionLoses2(1?double, '', '') expected="Number 1.0 java.lang.Double" />
+<@assertEquals actual=obj.mNumConversionLoses2(1?short, '', '') expected="Number 1 java.lang.Short" />
+<@assertEquals actual=obj.mNumConversionLoses2(1?long, '', '') expected="Number 1 java.lang.Long" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?double, '', '') expected="Serializable 1.0 java.lang.Double" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?int, '', '') expected="Serializable 1 java.lang.Integer" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?short, '', '') expected="Serializable 1 java.lang.Short" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?long, '', '') expected="Serializable 1 java.lang.Long" />
+
+<#-- BigDecimal-to-int is preferred over to-long for BC and user expectations: -->
+<@assertEquals actual=obj.nIntAndLong(1) expected="nIntAndLong(int 1)" />
+<@assertEquals actual=obj.nIntAndLong(1?long) expected="nIntAndLong(long 1)" />
+<#-- BigDecimal-to-short is, however unfavored due to the higher chance of overflow: -->
+<@assertEquals actual=obj.nIntAndShort(1) expected="nIntAndShort(int 1)" />
+<@assertEquals actual=obj.nIntAndShort(1?short) expected="nIntAndShort(short 1)" />
+<@assertEquals actual=obj.nLongAndShort(1) expected="nLongAndShort(long 1)" />
+<@assertEquals actual=obj.nLongAndShort(1?short) expected="nLongAndShort(short 1)" />
+
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3.5) expected='varargs1(String s = null, double... xs = [1.0, 2.0, 3.5])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2.5, 3) expected='varargs1(String s = null, double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1.5, 2, 3) expected='varargs1(String s = null, double... xs = [1.5, 2.0, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2, 'c') expected='varargs1(String s = null, Object... xs = [1, 2, c])' />
+<@assertEquals actual=obj.varargs1(null, 1, 'b', 3) expected='varargs1(String s = null, Object... xs = [1, b, 3])' />
+<@assertEquals actual=obj.varargs1(null, 'a', 2, 3) expected='varargs1(String s = null, Object... xs = [a, 2, 3])' />
+<@assertEquals actual=obj.varargs1('s', 1, 2, 3) expected='varargs1(String s = "s", int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1('s', 1.1, 2.1, 3.1) expected='varargs1(String s = "s", double... xs = [1.1, 2.1, 3.1])' />
+<@assertEquals actual=obj.varargs1('s', 'a', 'b', 'c') expected='varargs1(String s = "s", Object... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3) expected='varargs1(String s = null, int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1(null, 1.1, 2.1, 3.1) expected='varargs1(String s = null, double... xs = [1.1, 2.1, 3.1])' />
+<@assertEquals actual=obj.varargs1(null, 'a', 'b', 'c') expected='varargs1(String s = null, Object... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3?double) expected='varargs1(String s = null, int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2?double, 3?double) expected='varargs1(String s = null, double... xs = [1.0, 2.0, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2?float, 3?float) expected='varargs1(String s = null, double... xs = [1.0, 2.0, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1?double, 2?byte, 3?byte) expected='varargs1(String s = null, int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1(0, 1, 2, 3) expected='varargs1(Object s = 0, Object... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1('s') expected='varargs1(String s = "s", int... xs = [])' />
+
+<@assertEquals actual=obj.varargs2(1, 2.5, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?int, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2?double, 3) expected='varargs2(int... xs = [1, 2, 3])' />
+
+<@assertEquals actual=obj.varargs3(1, 2, 3) expected='varargs3(Comparable... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 'c') expected='varargs3(String... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs3(1, 'b', 'c') expected='varargs3(Comparable... xs = [1, b, c])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 3) expected='varargs3(Comparable... xs = [a, b, 3])' />
+<@assertEquals actual=obj.varargs3('a', [], 3) expected='varargs3(Object... xs = [a, [], 3])' />
+<@assertEquals actual=obj.varargs3(null, 'b', null) expected='varargs3(String... xs = [null, b, null])' />
+<@assertEquals actual=obj.varargs3(null, 2, null) expected='varargs3(Comparable... xs = [null, 2, null])' />
+<@assertEquals actual=obj.varargs3(null, [], null) expected='varargs3(Object... xs = [null, [], null])' />
+<@assertEquals actual=obj.varargs3(null, null, null) expected='varargs3(String... xs = [null, null, null])' />
+<@assertEquals actual=obj.varargs3() expected='varargs3(String... xs = [])' />
+
+<@assertEquals actual=obj.varargs4(null) expected='varargs4(Integer... xs = [null])' />
+<@assertEquals actual=obj.varargs4(null, null, null) expected='varargs4(Integer... xs = [null, null, null])' />
+<@assertEquals actual=obj.varargs4(1, null, 2) expected='varargs4(Integer... xs = [1, null, 2])' />
+<@assertEquals actual=obj.varargs4(1) expected='varargs4(int... xs = [1])' />
+<@assertEquals actual=obj.varargs4(1, 2, 3) expected='varargs4(int... xs = [1, 2, 3])' />
+
+<@assertEquals actual=obj.varargs5(1, 2, 3, 4, 5) expected='varargs5(int a1 = 1, int a2 = 2, int a3 = 3, int... xs = [4, 5])' />
+<@assertEquals actual=obj.varargs5(1, 2, 3, 4) expected='varargs5(int a1 = 1, int a2 = 2, int a3 = 3, int... xs = [4])' />
+<@assertEquals actual=obj.varargs5(1, 2, 3) expected='varargs5(int a1 = 1, int a2 = 2, int a3 = 3, int... xs = [])' />
+<@assertEquals actual=obj.varargs5(1, 2) expected='varargs5(int a1 = 1, int a2 = 2, int... xs = [])' />
+<@assertEquals actual=obj.varargs5(1) expected='varargs5(int a1 = 1, int... xs = [])' />
+<@assertEquals actual=obj.varargs5() expected='varargs5(int... xs = [])' />
+
+<@assertEquals actual=obj.varargs6('s', 2) expected='varargs6(String a1 = s, int... xs = [2])' />
+<@assertEquals actual=obj.varargs6('s') expected='varargs6(String a1 = s, int... xs = [])' />
+<@assertEquals actual=obj.varargs6(1, 2) expected='varargs6(Object a1 = 1, int a2 = 2, int... xs = [])' />
+<@assertFails message="no compatible overloaded">${obj.varargs6(1)}</@>
+
+<@assertEquals actual=obj.varargs7(1?int, 2?int) expected='varargs7(int... xs = [1, 2])' />
+<@assertEquals actual=obj.varargs7(1?short, 2?int) expected='varargs7(short a1 = 1, int... xs = [2])' />
+
+<#-- Tests that a pre-2.3.21 bug is fixed now: -->
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3) expected='mVarargsIgnoredTail(int... is = [1, 2, 3])' />
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3.5) expected='mVarargsIgnoredTail(int i = 1, double... ds = [2.0, 3.5])' />
+
+<@assertEquals actual=obj.mNullAmbiguous('a') expected='mNullAmbiguous(String s = a)' />
+<@assertEquals actual=obj.mNullAmbiguous(123) expected='mNullAmbiguous(int i = 123)' />
+<@assertEquals actual=obj.mNullAmbiguous(1.9) expected='mNullAmbiguous(int i = 1)' />
+<@assertEquals actual=obj.mNullAmbiguous(1?double) expected='mNullAmbiguous(int i = 1)' />
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(1.9?double)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.mNullAmbiguous(null)}</@>
+
+<@assertFails message="multiple compatible overloaded">${obj.mNullAmbiguous2(null)}</@>
+
+<@assertEquals actual=obj.mNullNonAmbiguous(null) expected='mNullNonAmbiguous(String s = null)' />
+
+<#-- The primitive int-s will win twice, but then String wins over Object, which is stronger: -->
+<@assertEquals actual=obj.mLowRankWins(1, 2, 'a') expected='mLowRankWins(Integer x = 1, Integer y = 2, String s = a)' />
+
+<@assertEquals actual=obj.mRareWrappings(obj.file, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringWrappedAsBoolean)
+ expected='mRareWrappings(File f = file, double d1 = 123.0002, Double d2 = 123.0002, double d3 = 123.0002, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.stringWrappedAsBoolean, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringAdaptedToBoolean)
+ expected='mRareWrappings(String s = yes, double d1 = 123.0002, Double d2 = 123.0002, double d3 = 123.0002, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.stringAdaptedToBoolean2, obj.wrapperNumber, obj.wrapperNumber, obj.wrapperNumber, obj.stringAdaptedToBoolean2)
+ expected='mRareWrappings(String s = yes, double d1 = 123.0001, Double d2 = 123.0001, double d3 = 123.0001, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, obj.booleanWrappedAsAnotherBoolean)
+ expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.adaptedNumber, 0, 0, 0, !obj.booleanWrappedAsAnotherBoolean)
+ expected='mRareWrappings(Object o = 124, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, !obj.stringAdaptedToBoolean)
+ expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+
+<@assertEquals actual=obj.mRareWrappings2(obj.adaptedNumber) expected='mRareWrappings2(byte b = 124)' />
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl
index 5644eb1..f91d7d8 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl
@@ -3,5 +3,22 @@
<@assertFails message="multiple compatible overload">${obj.mIntPrimVSBoxed(123?long)}</@>
<@assertFails message="multiple compatible overload">${obj.mIntPrimVSBoxed(123?short)}</@>
<@assertFails message="multiple compatible overload">${obj.mIntPrimVSBoxed(123)}</@>
+<@assertFails message="multiple compatible overload">${obj.varargs4(1, 2, 3)}</@>
+
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3) expected='mVarargsIgnoredTail(int... is = [1, 2, 3])' />
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3.5) expected='mVarargsIgnoredTail(int... is = [1, 2, 3])' />
+
+<@assertEquals actual=obj.mLowRankWins(1, 2, 'a') expected='mLowRankWins(Integer x = 1, Integer y = 2, String s = a)' />
+
+<@assertEquals actual=obj.mRareWrappings(obj.file, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringWrappedAsBoolean)
+ expected='mRareWrappings(File f = file, double d1 = 123.0001, Double d2 = 123.0002, double d3 = 123.0002, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.stringWrappedAsBoolean, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringAdaptedToBoolean)
+ expected='mRareWrappings(String s = yes, double d1 = 123.0001, Double d2 = 123.0002, double d3 = 123.0002, b = false)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, obj.booleanWrappedAsAnotherBoolean)
+ expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = false)' />
+<@assertEquals actual=obj.mRareWrappings(obj.adaptedNumber, 0, 0, 0, !obj.booleanWrappedAsAnotherBoolean)
+ expected='mRareWrappings(Object o = 124, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, !obj.stringAdaptedToBoolean)
+ expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
<#include 'overloaded-methods-2-ici-2.3.20.ftl'>