diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 9a2560759..f18253d67 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,7 @@ + Add support for varargs in ConstructorUtils, MemberUtils, and MethodUtils New methods for lang3.Validate Fix for incorrect comment on StringUtils.containsIgnoreCase method Fix typo on appendIfMissing javadoc diff --git a/src/main/java/org/apache/commons/lang3/ArrayUtils.java b/src/main/java/org/apache/commons/lang3/ArrayUtils.java index e8a327eb2..efd9c4a44 100644 --- a/src/main/java/org/apache/commons/lang3/ArrayUtils.java +++ b/src/main/java/org/apache/commons/lang3/ArrayUtils.java @@ -4692,6 +4692,39 @@ public class ArrayUtils { return result; } + /** + *

Create an array of primitive type from an array of wrapper types. + * + *

This method returns {@code null} for a {@code null} input array. + * + * @param array an array of wrapper object + * @return an array of the corresponding primitive type, or the original array + * @since 3.5 + */ + public static Object toPrimitive(final Object array) { + if (array == null) { + return null; + } + Class ct = array.getClass().getComponentType(); + Class pt = ClassUtils.wrapperToPrimitive(ct); + if(Integer.TYPE.equals(pt)) { + return toPrimitive((Integer[]) array); + } + if(Long.TYPE.equals(pt)) { + return toPrimitive((Long[]) array); + } + if(Short.TYPE.equals(pt)) { + return toPrimitive((Short[]) array); + } + if(Double.TYPE.equals(pt)) { + return toPrimitive((Double[]) array); + } + if(Float.TYPE.equals(pt)) { + return toPrimitive((Float[]) array); + } + return array; + } + // Boolean array converters // ---------------------------------------------------------------------- /** diff --git a/src/main/java/org/apache/commons/lang3/reflect/ConstructorUtils.java b/src/main/java/org/apache/commons/lang3/reflect/ConstructorUtils.java index 7d4369458..aa6a6ba93 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/ConstructorUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/ConstructorUtils.java @@ -114,6 +114,10 @@ public class ConstructorUtils { throw new NoSuchMethodException( "No such accessible constructor on object: " + cls.getName()); } + if (ctor.isVarArgs()) { + Class[] methodParameterTypes = ctor.getParameterTypes(); + args = MethodUtils.getVarArgs(args, methodParameterTypes); + } return ctor.newInstance(args); } @@ -258,14 +262,12 @@ public class ConstructorUtils { // return best match: for (Constructor ctor : ctors) { // compare parameters - if (ClassUtils.isAssignable(parameterTypes, ctor.getParameterTypes(), true)) { + if (MemberUtils.isMatchingConstructor(ctor, parameterTypes)) { // get accessible version of constructor ctor = getAccessibleConstructor(ctor); if (ctor != null) { MemberUtils.setAccessibleWorkaround(ctor); - if (result == null - || MemberUtils.compareParameterTypes(ctor.getParameterTypes(), result - .getParameterTypes(), parameterTypes) < 0) { + if (result == null || MemberUtils.compareConstructorFit(ctor, result, parameterTypes) < 0) { // temporary variable for annotation, see comment above (1) @SuppressWarnings("unchecked") final diff --git a/src/main/java/org/apache/commons/lang3/reflect/MemberUtils.java b/src/main/java/org/apache/commons/lang3/reflect/MemberUtils.java index 04c363731..3922bb414 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/MemberUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/MemberUtils.java @@ -17,7 +17,9 @@ package org.apache.commons.lang3.reflect; import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import org.apache.commons.lang3.ClassUtils; @@ -85,18 +87,52 @@ abstract class MemberUtils { } /** - * Compares the relative fitness of two sets of parameter types in terms of - * matching a third set of runtime parameter types, such that a list ordered + * Compares the relative fitness of two Constructors in terms of how well they + * match a set of runtime parameter types, such that a list ordered * by the results of the comparison would return the best match first * (least). * - * @param left the "left" parameter set - * @param right the "right" parameter set + * @param left the "left" Constructor + * @param right the "right" Constructor + * @param actual the runtime parameter types to match against + * {@code left}/{@code right} + * @return int consistent with {@code compare} semantics + * @since 3.5 + */ + static int compareConstructorFit(final Constructor left, final Constructor right, final Class[] actual) { + return compareParameterTypes(Executable.of(left), Executable.of(right), actual); + } + + /** + * Compares the relative fitness of two Methods in terms of how well they + * match a set of runtime parameter types, such that a list ordered + * by the results of the comparison would return the best match first + * (least). + * + * @param left the "left" Method + * @param right the "right" Method + * @param actual the runtime parameter types to match against + * {@code left}/{@code right} + * @return int consistent with {@code compare} semantics + * @since 3.5 + */ + static int compareMethodFit(final Method left, final Method right, final Class[] actual) { + return compareParameterTypes(Executable.of(left), Executable.of(right), actual); + } + + /** + * Compares the relative fitness of two Executables in terms of how well they + * match a set of runtime parameter types, such that a list ordered + * by the results of the comparison would return the best match first + * (least). + * + * @param left the "left" Executable + * @param right the "right" Executable * @param actual the runtime parameter types to match against * {@code left}/{@code right} * @return int consistent with {@code compare} semantics */ - static int compareParameterTypes(final Class[] left, final Class[] right, final Class[] actual) { + private static int compareParameterTypes(final Executable left, final Executable right, final Class[] actual) { final float leftCost = getTotalTransformationCost(actual, left); final float rightCost = getTotalTransformationCost(actual, right); return leftCost < rightCost ? -1 : rightCost < leftCost ? 1 : 0; @@ -107,15 +143,44 @@ abstract class MemberUtils { * source argument list. * @param srcArgs The source arguments * @param destArgs The destination arguments + * @param isVarArgs True if the destination arguments are for a varags methods * @return The total transformation cost */ - private static float getTotalTransformationCost(final Class[] srcArgs, final Class[] destArgs) { + private static float getTotalTransformationCost(final Class[] srcArgs, final Executable executable) { + final Class[] destArgs = executable.getParameterTypes(); + final boolean isVarArgs = executable.isVarArgs(); + + // "source" and "destination" are the actual and declared args respectively. float totalCost = 0.0f; - for (int i = 0; i < srcArgs.length; i++) { - Class srcClass, destClass; - srcClass = srcArgs[i]; - destClass = destArgs[i]; - totalCost += getObjectTransformationCost(srcClass, destClass); + final long normalArgsLen = isVarArgs ? destArgs.length-1 : destArgs.length; + if (srcArgs.length < normalArgsLen) + return Float.MAX_VALUE; + for (int i = 0; i < normalArgsLen; i++) { + totalCost += getObjectTransformationCost(srcArgs[i], destArgs[i]); + } + if (isVarArgs) { + // When isVarArgs is true, srcArgs and dstArgs may differ in length. + // There are two special cases to consider: + final boolean noVarArgsPassed = srcArgs.length < destArgs.length; + final boolean explicitArrayForVarags = (srcArgs.length == destArgs.length) && srcArgs[srcArgs.length-1].isArray(); + + final float varArgsCost = 0.001f; + Class destClass = destArgs[destArgs.length-1].getComponentType(); + if (noVarArgsPassed) { + // When no varargs passed, the best match is the most generic matching type, not the most specific. + totalCost += getObjectTransformationCost(destClass, Object.class) + varArgsCost; + } + else if (explicitArrayForVarags) { + Class sourceClass = srcArgs[srcArgs.length-1].getComponentType(); + totalCost += getObjectTransformationCost(sourceClass, destClass) + varArgsCost; + } + else { + // This is typical varargs case. + for (int i = destArgs.length-1; i < srcArgs.length; i++) { + Class srcClass = srcArgs[i]; + totalCost += getObjectTransformationCost(srcClass, destClass) + varArgsCost; + } + } } return totalCost; } @@ -147,7 +212,7 @@ abstract class MemberUtils { srcClass = srcClass.getSuperclass(); } /* - * If the destination class is null, we've travelled all the way up to + * If the destination class is null, we've traveled all the way up to * an Object match. We'll penalize this by adding 1.5 to the cost. */ if (srcClass == null) { @@ -182,4 +247,58 @@ abstract class MemberUtils { return cost; } + static boolean isMatchingMethod(Method method, Class[] parameterTypes) { + return MemberUtils.isMatchingExecutable(Executable.of(method), parameterTypes); + } + + static boolean isMatchingConstructor(Constructor method, Class[] parameterTypes) { + return MemberUtils.isMatchingExecutable(Executable.of(method), parameterTypes); + } + + private static boolean isMatchingExecutable(Executable method, Class[] parameterTypes) { + final Class[] methodParameterTypes = method.getParameterTypes(); + if (method.isVarArgs()) { + int i; + for (i = 0; i < methodParameterTypes.length - 1 && i < parameterTypes.length; i++) { + if (!ClassUtils.isAssignable(parameterTypes[i], methodParameterTypes[i], true)) { + return false; + } + } + Class varArgParameterType = methodParameterTypes[methodParameterTypes.length - 1].getComponentType(); + for (; i < parameterTypes.length; i++) { + if (!ClassUtils.isAssignable(parameterTypes[i], varArgParameterType, true)) { + return false; + } + } + return true; + } + return ClassUtils.isAssignable(parameterTypes, methodParameterTypes, true); + } + + /** + *

A class providing a subset of the API of java.lang.reflect.Executable in Java 1.8, + * providing a common representation for function signatures for Constructors and Methods.

+ */ + private static final class Executable { + private final Class[] parameterTypes; + private final boolean isVarArgs; + + private static Executable of(Method method) { return new Executable(method); } + private static Executable of(Constructor constructor) { return new Executable(constructor); } + + private Executable(Method method) { + parameterTypes = method.getParameterTypes(); + isVarArgs = method.isVarArgs(); + } + + private Executable(Constructor constructor) { + parameterTypes = constructor.getParameterTypes(); + isVarArgs = constructor.isVarArgs(); + } + + public Class[] getParameterTypes() { return parameterTypes; } + + public boolean isVarArgs() { return isVarArgs; } + } + } diff --git a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java index 180c35cf5..c938cb627 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java @@ -17,13 +17,14 @@ package org.apache.commons.lang3.reflect; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -155,6 +156,7 @@ public class MethodUtils { + methodName + "() on object: " + object.getClass().getName()); } + args = toVarArgs(method, args); return method.invoke(object, args); } @@ -341,9 +343,61 @@ public class MethodUtils { throw new NoSuchMethodException("No such accessible method: " + methodName + "() on class: " + cls.getName()); } + args = toVarArgs(method, args); return method.invoke(null, args); } + private static Object[] toVarArgs(Method method, Object[] args) { + if (method.isVarArgs()) { + Class[] methodParameterTypes = method.getParameterTypes(); + args = getVarArgs(args, methodParameterTypes); + } + return args; + } + + /** + *

Given an arguments array passed to a varargs method, return an array of arguments in the canonical form, + * i.e. an array with the declared number of parameters, and whose last parameter is an array of the varargs type. + *

+ * + * @param args the array of arguments passed to the varags method + * @param methodParameterTypes the declared array of method parameter types + * @return an array of the variadic arguments passed to the method + * @since 3.5 + */ + static Object[] getVarArgs(Object[] args, Class[] methodParameterTypes) { + if (args.length == methodParameterTypes.length + && args[args.length - 1].getClass().equals(methodParameterTypes[methodParameterTypes.length - 1])) { + // The args array is already in the canonical form for the method. + return args; + } + + // Construct a new array matching the method's declared parameter types. + Object[] newArgs = new Object[methodParameterTypes.length]; + + // Copy the normal (non-varargs) parameters + System.arraycopy(args, 0, newArgs, 0, methodParameterTypes.length - 1); + + // Construct a new array for the variadic parameters + Class varArgComponentType = methodParameterTypes[methodParameterTypes.length - 1].getComponentType(); + int varArgLength = args.length - methodParameterTypes.length + 1; + + Object varArgsArray = Array.newInstance(ClassUtils.primitiveToWrapper(varArgComponentType), varArgLength); + // Copy the variadic arguments into the varargs array. + System.arraycopy(args, methodParameterTypes.length - 1, varArgsArray, 0, varArgLength); + + if(varArgComponentType.isPrimitive()) { + // unbox from wrapper type to primitive type + varArgsArray = ArrayUtils.toPrimitive(varArgsArray); + } + + // Store the varargs array in the last position of the array to return + newArgs[methodParameterTypes.length - 1] = varArgsArray; + + // Return the canonical varargs array. + return newArgs; + } + /** *

Invokes a {@code static} method whose parameter types match exactly the object * types.

@@ -533,15 +587,16 @@ public class MethodUtils { final Method[] methods = cls.getMethods(); for (final Method method : methods) { // compare name and parameters - if (method.getName().equals(methodName) && ClassUtils.isAssignable(parameterTypes, method.getParameterTypes(), true)) { + if (method.getName().equals(methodName) && + MemberUtils.isMatchingMethod(method, parameterTypes)) { // get accessible version of method final Method accessibleMethod = getAccessibleMethod(method); - if (accessibleMethod != null && (bestMatch == null || MemberUtils.compareParameterTypes( - accessibleMethod.getParameterTypes(), - bestMatch.getParameterTypes(), + if (accessibleMethod != null && (bestMatch == null || MemberUtils.compareMethodFit( + accessibleMethod, + bestMatch, parameterTypes) < 0)) { - bestMatch = accessibleMethod; - } + bestMatch = accessibleMethod; + } } } if (bestMatch != null) { diff --git a/src/test/java/org/apache/commons/lang3/ArrayUtilsTest.java b/src/test/java/org/apache/commons/lang3/ArrayUtilsTest.java index 588068e75..a50ed1ab2 100644 --- a/src/test/java/org/apache/commons/lang3/ArrayUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/ArrayUtilsTest.java @@ -16,7 +16,15 @@ */ package org.apache.commons.lang3; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; @@ -25,6 +33,7 @@ import java.util.Comparator; import java.util.Date; import java.util.Map; +import org.junit.Assert; import org.junit.Test; /** @@ -4406,4 +4415,14 @@ public class ArrayUtilsTest { assertFalse(ArrayUtils.isSorted(array)); } + @Test + public void testCreatePrimitiveArray() { + Assert.assertNull(ArrayUtils.toPrimitive((Object[])null)); + Assert.assertArrayEquals(new int[]{}, (int[]) ArrayUtils.toPrimitive(new Integer[]{})); + Assert.assertArrayEquals(new short[]{2}, (short[]) ArrayUtils.toPrimitive(new Short[]{2})); + Assert.assertArrayEquals(new long[]{2, 3}, (long[]) ArrayUtils.toPrimitive(new Long[]{2L, 3L})); + Assert.assertArrayEquals(new float[]{3.14f}, (float[]) ArrayUtils.toPrimitive(new Float[]{3.14f}), 0.1f); + Assert.assertArrayEquals(new double[]{2.718}, (double[]) ArrayUtils.toPrimitive(new Double[]{2.718}), 0.1); + } + } diff --git a/src/test/java/org/apache/commons/lang3/reflect/ConstructorUtilsTest.java b/src/test/java/org/apache/commons/lang3/reflect/ConstructorUtilsTest.java index c40f66fc3..31d6d640b 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/ConstructorUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/ConstructorUtilsTest.java @@ -16,10 +16,11 @@ */ package org.apache.commons.lang3.reflect; -import org.junit.Test; -import org.junit.Before; - -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.lang.reflect.Constructor; import java.util.Arrays; @@ -29,6 +30,9 @@ import java.util.Map; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.mutable.MutableObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; /** * Unit tests ConstructorUtils @@ -36,35 +40,65 @@ import org.apache.commons.lang3.mutable.MutableObject; public class ConstructorUtilsTest { public static class TestBean { private final String toString; + final String[] varArgs; public TestBean() { toString = "()"; + varArgs = null; } public TestBean(final int i) { toString = "(int)"; + varArgs = null; } public TestBean(final Integer i) { toString = "(Integer)"; + varArgs = null; } public TestBean(final double d) { toString = "(double)"; + varArgs = null; } public TestBean(final String s) { toString = "(String)"; + varArgs = null; } public TestBean(final Object o) { toString = "(Object)"; + varArgs = null; + } + + public TestBean(final String... s) { + toString = "(String...)"; + varArgs = s; + } + + public TestBean(final Integer i, String... s) { + toString = "(Integer, String...)"; + varArgs = s; + } + + public TestBean(final Integer first, int... args) { + toString = "(Integer, String...)"; + varArgs = new String[args.length]; + for(int i = 0; i< args.length; ++i) { + varArgs[i] = Integer.toString(args[i]); + } } @Override public String toString() { return toString; } + + void verify(final String str, final String[] args) { + assertEquals(str, toString); + assertEquals(args, varArgs); + } } private static class PrivateClass { @@ -117,6 +151,12 @@ public class ConstructorUtilsTest { TestBean.class, NumberUtils.LONG_ONE).toString()); assertEquals("(double)", ConstructorUtils.invokeConstructor( TestBean.class, NumberUtils.DOUBLE_ONE).toString()); + ConstructorUtils.invokeConstructor(TestBean.class, NumberUtils.INTEGER_ONE) + .verify("(Integer)", null); + ConstructorUtils.invokeConstructor(TestBean.class, "a", "b") + .verify("(String...)", new String[]{"a", "b"}); + ConstructorUtils.invokeConstructor(TestBean.class, NumberUtils.INTEGER_ONE, "a", "b") + .verify("(Integer, String...)", new String[]{"a", "b"}); } @Test @@ -242,4 +282,12 @@ public class ConstructorUtilsTest { return result; } + @Test + public void testVarArgsUnboxing() throws Exception { + TestBean testBean = ConstructorUtils.invokeConstructor( + TestBean.class, Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)); + + Assert.assertArrayEquals(new String[]{"2", "3"}, testBean.varArgs); + } + } diff --git a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java index c30088c4e..b91dae199 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java @@ -38,14 +38,16 @@ import java.util.List; import java.util.Map; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ClassUtils.Interfaces; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; -import org.apache.commons.lang3.ClassUtils.Interfaces; import org.apache.commons.lang3.reflect.testbed.Annotated; import org.apache.commons.lang3.reflect.testbed.GenericConsumer; import org.apache.commons.lang3.reflect.testbed.GenericParent; import org.apache.commons.lang3.reflect.testbed.StringParameterizedChild; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -53,15 +55,15 @@ import org.junit.Test; * Unit tests MethodUtils */ public class MethodUtilsTest { - + private static interface PrivateInterface {} - + static class TestBeanWithInterfaces implements PrivateInterface { public String foo() { return "foo()"; } } - + public static class TestBean { public static String bar() { @@ -87,7 +89,15 @@ public class MethodUtilsTest { public static String bar(final Object o) { return "bar(Object)"; } - + + public static String bar(final String... s) { + return "bar(String...)"; + } + + public static String bar(final Integer i, final String... s) { + return "bar(int, String...)"; + } + public static void oneParameterStatic(final String s) { // empty } @@ -120,10 +130,78 @@ public class MethodUtilsTest { public String foo(final Object o) { return "foo(Object)"; } - + + public String foo(final String... s) { + return "foo(String...)"; + } + + public String foo(final Integer i, final String... s) { + return "foo(int, String...)"; + } + public void oneParameter(final String s) { // empty } + + public String foo(final Object... s) { + return "foo(Object...)"; + } + + public int[] unboxing(int... values) { + return values; + } + + // This method is overloaded for the wrapper class for every primitive type, plus the common supertypes + // Number and Object. This is an acid test since it easily leads to ambiguous methods. + public static String varOverload(Byte... args) { return "Byte..."; } + public static String varOverload(Character... args) { return "Character..."; } + public static String varOverload(Short... args) { return "Short..."; } + public static String varOverload(Boolean... args) { return "Boolean..."; } + public static String varOverload(Float... args) { return "Float..."; } + public static String varOverload(Double... args) { return "Double..."; } + public static String varOverload(Integer... args) { return "Integer..."; } + public static String varOverload(Long... args) { return "Long..."; } + public static String varOverload(Number... args) { return "Number..."; } + public static String varOverload(Object... args) { return "Object..."; } + public static String varOverload(String... args) { return "String..."; } + + // This method is overloaded for the wrapper class for every numeric primitive type, plus the common + // supertype Number + public static String numOverload(Byte... args) { return "Byte..."; } + public static String numOverload(Short... args) { return "Short..."; } + public static String numOverload(Float... args) { return "Float..."; } + public static String numOverload(Double... args) { return "Double..."; } + public static String numOverload(Integer... args) { return "Integer..."; } + public static String numOverload(Long... args) { return "Long..."; } + public static String numOverload(Number... args) { return "Number..."; } + + // These varOverloadEcho and varOverloadEchoStatic methods are designed to verify that + // not only is the correct overloaded variant invoked, but that the varags arguments + // are also delivered correctly to the method. + public ImmutablePair varOverloadEcho(String... args) { + return new ImmutablePair("String...", args); + } + public ImmutablePair varOverloadEcho(Number... args) { + return new ImmutablePair("Number...", args); + } + + public static ImmutablePair varOverloadEchoStatic(String... args) { + return new ImmutablePair("String...", args); + } + public static ImmutablePair varOverloadEchoStatic(Number... args) { + return new ImmutablePair("Number...", args); + } + + static void verify(ImmutablePair a, ImmutablePair b) { + assertEquals(a.getLeft(), b.getLeft()); + assertArrayEquals(a.getRight(), b.getRight()); + } + + static void verify(ImmutablePair a, Object _b) { + final ImmutablePair b = (ImmutablePair) _b; + verify(a, b); + } + } private static class TestMutable implements Mutable { @@ -151,6 +229,81 @@ public class MethodUtilsTest { assertNotNull(MethodUtils.class.newInstance()); } + @Test + public void verifyJavaVarargsOverloadingResolution() throws Exception { + // This code is not a test of MethodUtils. + // Rather it makes explicit the behavior of the Java specification for + // various cases of overload resolution. + assertEquals("Byte...", TestBean.varOverload((byte) 1, (byte) 2)); + assertEquals("Short...", TestBean.varOverload((short) 1, (short) 2)); + assertEquals("Integer...", TestBean.varOverload(1, 2)); + assertEquals("Long...", TestBean.varOverload(1L, 2L)); + assertEquals("Float...", TestBean.varOverload(1f, 2f)); + assertEquals("Double...", TestBean.varOverload(1d, 2d)); + assertEquals("Character...", TestBean.varOverload('a', 'b')); + assertEquals("String...", TestBean.varOverload("a", "b")); + assertEquals("Boolean...", TestBean.varOverload(true, false)); + + assertEquals("Object...", TestBean.varOverload(1, "s")); + assertEquals("Object...", TestBean.varOverload(1, true)); + assertEquals("Object...", TestBean.varOverload(1.1, true)); + assertEquals("Object...", TestBean.varOverload('c', true)); + assertEquals("Number...", TestBean.varOverload(1, 1.1)); + assertEquals("Number...", TestBean.varOverload(1, 1L)); + assertEquals("Number...", TestBean.varOverload(1d, 1f)); + assertEquals("Number...", TestBean.varOverload((short) 1, (byte) 1)); + assertEquals("Object...", TestBean.varOverload(1, 'c')); + assertEquals("Object...", TestBean.varOverload('c', "s")); + } + + @Test + public void testInvokeJavaVarargsOverloadingResolution() throws Exception { + assertEquals("Byte...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", (byte) 1, (byte) 2)); + assertEquals("Short...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", (short) 1, (short) 2)); + assertEquals("Integer...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1, 2)); + assertEquals("Long...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1L, 2L)); + assertEquals("Float...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1f, 2f)); + assertEquals("Double...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1d, 2d)); + assertEquals("Character...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 'a', 'b')); + assertEquals("String...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", "a", "b")); + assertEquals("Boolean...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", true, false)); + + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1, "s")); + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1, true)); + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1.1, true)); + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 'c', true)); + assertEquals("Number...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1, 1.1)); + assertEquals("Number...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1, 1L)); + assertEquals("Number...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1d, 1f)); + assertEquals("Number...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", (short) 1, (byte) 1)); + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 1, 'c')); + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, + "varOverload", 'c', "s")); + + assertEquals("Object...", MethodUtils.invokeStaticMethod(TestBean.class, "varOverload", + (Object[]) ArrayUtils.EMPTY_CLASS_ARRAY)); + assertEquals("Number...", MethodUtils.invokeStaticMethod(TestBean.class, "numOverload", + (Object[]) ArrayUtils.EMPTY_CLASS_ARRAY)); + } + @Test public void testInvokeMethod() throws Exception { assertEquals("foo()", MethodUtils.invokeMethod(testBean, "foo", @@ -158,7 +311,7 @@ public class MethodUtilsTest { assertEquals("foo()", MethodUtils.invokeMethod(testBean, "foo")); assertEquals("foo()", MethodUtils.invokeMethod(testBean, "foo", (Object[]) null)); - assertEquals("foo()", MethodUtils.invokeMethod(testBean, "foo", + assertEquals("foo()", MethodUtils.invokeMethod(testBean, "foo", (Object[]) null, (Class[]) null)); assertEquals("foo(String)", MethodUtils.invokeMethod(testBean, "foo", "")); @@ -174,6 +327,21 @@ public class MethodUtilsTest { NumberUtils.LONG_ONE)); assertEquals("foo(double)", MethodUtils.invokeMethod(testBean, "foo", NumberUtils.DOUBLE_ONE)); + assertEquals("foo(String...)", MethodUtils.invokeMethod(testBean, "foo", + "a", "b", "c")); + assertEquals("foo(String...)", MethodUtils.invokeMethod(testBean, "foo", + "a", "b", "c")); + assertEquals("foo(int, String...)", MethodUtils.invokeMethod(testBean, "foo", + 5, "a", "b", "c")); + + TestBean.verify(new ImmutablePair("String...", new String[]{"x", "y"}), + MethodUtils.invokeMethod(testBean, "varOverloadEcho", "x", "y")); + TestBean.verify(new ImmutablePair("Number...", new Number[]{17, 23, 42}), + MethodUtils.invokeMethod(testBean, "varOverloadEcho", 17, 23, 42)); + TestBean.verify(new ImmutablePair("String...", new String[]{"x", "y"}), + MethodUtils.invokeMethod(testBean, "varOverloadEcho", new String[]{"x", "y"})); + TestBean.verify(new ImmutablePair("Number...", new Number[]{17, 23, 42}), + MethodUtils.invokeMethod(testBean, "varOverloadEcho", new Number[]{17, 23, 42})); } @Test @@ -183,7 +351,7 @@ public class MethodUtilsTest { assertEquals("foo()", MethodUtils.invokeExactMethod(testBean, "foo")); assertEquals("foo()", MethodUtils.invokeExactMethod(testBean, "foo", (Object[]) null)); - assertEquals("foo()", MethodUtils.invokeExactMethod(testBean, "foo", + assertEquals("foo()", MethodUtils.invokeExactMethod(testBean, "foo", (Object[]) null, (Class[]) null)); assertEquals("foo(String)", MethodUtils.invokeExactMethod(testBean, "foo", "")); @@ -236,7 +404,20 @@ public class MethodUtilsTest { TestBean.class, "bar", NumberUtils.LONG_ONE)); assertEquals("bar(double)", MethodUtils.invokeStaticMethod( TestBean.class, "bar", NumberUtils.DOUBLE_ONE)); - + assertEquals("bar(String...)", MethodUtils.invokeStaticMethod( + TestBean.class, "bar", "a", "b")); + assertEquals("bar(int, String...)", MethodUtils.invokeStaticMethod( + TestBean.class, "bar", NumberUtils.INTEGER_ONE, "a", "b")); + + TestBean.verify(new ImmutablePair("String...", new String[]{"x", "y"}), + MethodUtils.invokeStaticMethod(TestBean.class, "varOverloadEchoStatic", "x", "y")); + TestBean.verify(new ImmutablePair("Number...", new Number[]{17, 23, 42}), + MethodUtils.invokeStaticMethod(TestBean.class, "varOverloadEchoStatic", 17, 23, 42)); + TestBean.verify(new ImmutablePair("String...", new String[]{"x", "y"}), + MethodUtils.invokeStaticMethod(TestBean.class, "varOverloadEchoStatic", new String[]{"x", "y"})); + TestBean.verify(new ImmutablePair("Number...", new Number[]{17, 23, 42}), + MethodUtils.invokeStaticMethod(TestBean.class, "varOverloadEchoStatic", new Number[]{17, 23, 42})); + try { MethodUtils.invokeStaticMethod(TestBean.class, "does_not_exist"); fail("should throw NoSuchMethodException"); @@ -292,7 +473,7 @@ public class MethodUtilsTest { assertSame(Mutable.class, accessibleMethod.getDeclaringClass()); } } - + @Test public void testGetAccessibleMethodPrivateInterface() throws Exception { final Method expected = TestBeanWithInterfaces.class.getMethod("foo"); @@ -325,7 +506,7 @@ public class MethodUtilsTest { MutableObject.class, "getValue", ArrayUtils.EMPTY_CLASS_ARRAY) .getDeclaringClass()); } - + @Test public void testGetAccessibleMethodInaccessible() throws Exception { final Method expected = TestBean.class.getDeclaredMethod("privateStuff"); @@ -375,6 +556,10 @@ public class MethodUtilsTest { singletonArray(Double.TYPE), singletonArray(Double.TYPE)); expectMatchingAccessibleMethodParameterTypes(TestBean.class, "foo", singletonArray(Double.TYPE), singletonArray(Double.TYPE)); + expectMatchingAccessibleMethodParameterTypes(TestBean.class, "foo", + new Class[] {String.class, String.class}, new Class[] {String[].class}); + expectMatchingAccessibleMethodParameterTypes(TestBean.class, "foo", + new Class[] {Integer.TYPE, String.class, String.class}, new Class[] {Integer.class, String[].class}); expectMatchingAccessibleMethodParameterTypes(InheritanceBean.class, "testOne", singletonArray(ParentObject.class), singletonArray(ParentObject.class)); expectMatchingAccessibleMethodParameterTypes(InheritanceBean.class, "testOne", @@ -485,11 +670,13 @@ public class MethodUtilsTest { public void testGetMethodsListWithAnnotationIllegalArgumentException3() { MethodUtils.getMethodsListWithAnnotation(null, null); } - + private void expectMatchingAccessibleMethodParameterTypes(final Class cls, final String methodName, final Class[] requestTypes, final Class[] actualTypes) { final Method m = MethodUtils.getMatchingAccessibleMethod(cls, methodName, requestTypes); + assertNotNull("could not find any matches for " + methodName + + " (" + (requestTypes == null ? null : toString(requestTypes)) + ")", m); assertTrue(toString(m.getParameterTypes()) + " not equals " + toString(actualTypes), Arrays.equals(actualTypes, m .getParameterTypes())); @@ -516,8 +703,8 @@ public class MethodUtilsTest { public void testTwo(final GrandParentObject obj) {} public void testTwo(final ChildInterface obj) {} } - - interface ChildInterface {} + + interface ChildInterface {} public static class GrandParentObject {} public static class ParentObject extends GrandParentObject {} public static class ChildObject extends ParentObject implements ChildInterface {} @@ -533,4 +720,11 @@ public class MethodUtilsTest { this.parameterTypes = parameterTypes; } } + + @Test + public void testVarArgsUnboxing() throws Exception { + TestBean testBean = new TestBean(); + int[] actual = (int[])MethodUtils.invokeMethod(testBean, "unboxing", Integer.valueOf(1), Integer.valueOf(2)); + Assert.assertArrayEquals(new int[]{1, 2}, actual); + } }