Skip to content

Commit 6ac0dea

Browse files
committed
[LANG-1597][LANG-1782] Fix getMatchingAccessibleMethod when no varargs are supplied and when the supplied varargs don't match the exact class
The current implementation discarded methods which could be potential matches because the matching was way to strict. This change loosens the check to allow any assignable values. The change also checks all variable arguments instead of just the last argument. To facilitate various number types and conversion NumberUtils.convertIfNotNarrowing was also introduced. This allows integers to be passed to methods which take Long... varargs
1 parent 50696dd commit 6ac0dea

File tree

4 files changed

+211
-14
lines changed

4 files changed

+211
-14
lines changed

src/main/java/org/apache/commons/lang3/math/NumberUtils.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,130 @@ public static int compare(final short x, final short y) {
151151
return x < y ? -1 : 1;
152152
}
153153

154+
/**
155+
* Converts a given {@link Number} to the specified target number type, if and
156+
* only if the conversion is not a narrowing conversion. Narrowing conversions
157+
* (e.g., from
158+
* {@link Long} to {@link Integer}) will result in an
159+
* {@link IllegalArgumentException}.
160+
* <p>
161+
* Widening conversions are allowed in accordance with the following numeric
162+
* rank:
163+
*
164+
* <pre>
165+
* Byte &lt; Short &lt; Integer &lt; Long &lt; Float &lt; Double
166+
* </pre>
167+
*
168+
* For example:
169+
* <ul>
170+
* <li>{@code Integer -> Long} is allowed</li>
171+
* <li>{@code Long -> Double} is allowed</li>
172+
* <li>{@code Long -> Integer} is not allowed (narrowing)</li>
173+
* </ul>
174+
*
175+
* @param number the source {@link Number} to convert; must not be
176+
* {@code null}
177+
* @param targetType the target number type; must not be {@code null} and must
178+
* be one of:
179+
* {@link Byte}, {@link Short}, {@link Integer}, {@link Long},
180+
* {@link Float}, or {@link Double}
181+
* @param <T> the target number type parameter
182+
* @return the converted value as an instance of {@code targetType}
183+
* @throws IllegalArgumentException if:
184+
* <ul>
185+
* <li>{@code number} is {@code null}</li>
186+
* <li>{@code targetType} is {@code null}</li>
187+
* <li>{@code targetType} is not supported</li>
188+
* <li>the conversion would narrow the
189+
* value</li>
190+
* </ul>
191+
*/
192+
public static <T extends Number> T convertIfNotNarrowing(Number number, Class<T> targetType) {
193+
if (number == null || targetType == null) {
194+
throw new IllegalArgumentException("Number and targetType must not be null.");
195+
}
196+
197+
final Class<? extends Number> sourceType = number.getClass();
198+
199+
// Check the widening rank
200+
final int sourceRank = getRank(sourceType);
201+
final int targetRank = getRank(targetType);
202+
203+
if (sourceRank < 0 || targetRank < 0) {
204+
throw new IllegalArgumentException("Unsupported number type: " + sourceType + " or " + targetType);
205+
}
206+
207+
if (targetRank < sourceRank) {
208+
throw new IllegalArgumentException(
209+
"Narrowing conversion from " + sourceType.getSimpleName() + " to " + targetType.getSimpleName()
210+
+ " is not allowed.");
211+
}
212+
213+
// Perform conversion
214+
if (targetType == Byte.class) {
215+
return targetType.cast(number.byteValue());
216+
}
217+
if (targetType == Short.class) {
218+
return targetType.cast(number.shortValue());
219+
}
220+
if (targetType == Integer.class) {
221+
return targetType.cast(number.intValue());
222+
}
223+
if (targetType == Long.class) {
224+
return targetType.cast(number.longValue());
225+
}
226+
if (targetType == Float.class) {
227+
return targetType.cast(number.floatValue());
228+
}
229+
if (targetType == Double.class) {
230+
return targetType.cast(number.doubleValue());
231+
}
232+
233+
throw new IllegalArgumentException("Unsupported target number type: " + targetType);
234+
}
235+
236+
/**
237+
* Returns a numeric rank for the specified number type to determine
238+
* its position in the widening conversion hierarchy.
239+
* <p>
240+
* The ranks are defined as:
241+
* <pre>
242+
* Byte = 1
243+
* Short = 2
244+
* Integer = 3
245+
* Long = 4
246+
* Float = 5
247+
* Double = 6
248+
* </pre>
249+
* These ranks are used to determine whether a conversion is widening
250+
* (allowed) or narrowing (disallowed) such as in the
251+
* {@link #convertIfNotNarrowing(Number, Class)} method.
252+
*
253+
* @param type the number type class; must not be {@code null}
254+
* @return the numeric rank of the type, or {@code -1} if the type is unsupported
255+
*/
256+
private static <T extends Number> int getRank(Class<T> type) {
257+
if (type == Byte.class) {
258+
return 1;
259+
}
260+
if (type == Short.class) {
261+
return 2;
262+
}
263+
if (type == Integer.class) {
264+
return 3;
265+
}
266+
if (type == Long.class) {
267+
return 4;
268+
}
269+
if (type == Float.class) {
270+
return 5;
271+
}
272+
if (type == Double.class) {
273+
return 6;
274+
}
275+
return -1;
276+
}
277+
154278
/**
155279
* Creates a {@link BigDecimal} from a {@link String}.
156280
*

src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.commons.lang3.ClassUtils;
4141
import org.apache.commons.lang3.ClassUtils.Interfaces;
4242
import org.apache.commons.lang3.Validate;
43+
import org.apache.commons.lang3.math.NumberUtils;
4344

4445
/**
4546
* Utility reflection methods focused on {@link Method}s, originally from Commons BeanUtils.
@@ -334,16 +335,13 @@ public static Method getMatchingAccessibleMethod(final Class<?> cls, final Strin
334335
MemberUtils.setAccessibleWorkaround(bestMatch);
335336
}
336337
if (bestMatch != null && bestMatch.isVarArgs() && bestMatch.getParameterTypes().length > 0 && parameterTypes.length > 0) {
337-
final Class<?>[] methodParameterTypes = bestMatch.getParameterTypes();
338-
final Class<?> methodParameterComponentType = methodParameterTypes[methodParameterTypes.length - 1].getComponentType();
339-
final String methodParameterComponentTypeName = ClassUtils.primitiveToWrapper(methodParameterComponentType).getName();
340-
final Class<?> lastParameterType = parameterTypes[parameterTypes.length - 1];
341-
final String parameterTypeName = lastParameterType == null ? null : lastParameterType.getName();
342-
final String parameterTypeSuperClassName = lastParameterType == null ? null
343-
: lastParameterType.getSuperclass() != null ? lastParameterType.getSuperclass().getName() : null;
344-
if (parameterTypeName != null && parameterTypeSuperClassName != null && !methodParameterComponentTypeName.equals(parameterTypeName)
345-
&& !methodParameterComponentTypeName.equals(parameterTypeSuperClassName)) {
346-
return null;
338+
final Class<?>[] bestMatchParameterTypes = bestMatch.getParameterTypes();
339+
final Class<?> varArgType = bestMatchParameterTypes[bestMatchParameterTypes.length - 1].getComponentType();
340+
for (int paramIdx = bestMatchParameterTypes.length - 1; paramIdx < parameterTypes.length; paramIdx++) {
341+
final Class<?> parameterType = parameterTypes[paramIdx];
342+
if (!ClassUtils.isAssignable(parameterType, varArgType, true)) {
343+
return null;
344+
}
347345
}
348346
}
349347
return bestMatch;
@@ -566,10 +564,14 @@ static Object[] getVarArgs(final Object[] args, final Class<?>[] methodParameter
566564
final Object[] newArgs = ArrayUtils.arraycopy(args, 0, 0, mptLength - 1, () -> new Object[mptLength]);
567565
// Construct a new array for the variadic parameters
568566
final Class<?> varArgComponentType = methodParameterTypes[mptLength - 1].getComponentType();
567+
final Class<?> varArgComponentWrappedType = ClassUtils.primitiveToWrapper(varArgComponentType);
569568
final int varArgLength = args.length - mptLength + 1;
570-
// Copy the variadic arguments into the varargs array.
571-
Object varArgsArray = ArrayUtils.arraycopy(args, mptLength - 1, 0, varArgLength,
572-
s -> Array.newInstance(ClassUtils.primitiveToWrapper(varArgComponentType), varArgLength));
569+
// Copy the variadic arguments into the varargs array, converting types if needed.
570+
Object varArgsArray = Array.newInstance(varArgComponentWrappedType, varArgLength);
571+
for (int i = 0; i < varArgLength; i++) {
572+
final Object arg = args[mptLength - 1 + i];
573+
Array.set(varArgsArray, i, convertVarArg(arg, varArgComponentWrappedType));
574+
}
573575
if (varArgComponentType.isPrimitive()) {
574576
// unbox from wrapper type to primitive type
575577
varArgsArray = ArrayUtils.toPrimitive(varArgsArray);
@@ -580,6 +582,15 @@ static Object[] getVarArgs(final Object[] args, final Class<?>[] methodParameter
580582
return newArgs;
581583
}
582584

585+
@SuppressWarnings("unchecked")
586+
private static Object convertVarArg(final Object arg, final Class<?> varArgComponentWrappedType) {
587+
if (arg instanceof Number && Number.class.isAssignableFrom(varArgComponentWrappedType) && varArgComponentWrappedType != Number.class) {
588+
return NumberUtils.convertIfNotNarrowing((Number) arg, (Class<Number>) varArgComponentWrappedType);
589+
} else {
590+
return varArgComponentWrappedType.cast(arg);
591+
}
592+
}
593+
583594
/**
584595
* Invokes a method whose parameter types match exactly the object type.
585596
*

src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,4 +1897,34 @@ void testToShortStringI() {
18971897
assertEquals(5, NumberUtils.toShort("", (short) 5));
18981898
assertEquals(5, NumberUtils.toShort(null, (short) 5));
18991899
}
1900+
1901+
/**
1902+
* Test for {@link NumberUtils#convertIfNotNarrowing}.
1903+
*/
1904+
@Test
1905+
void testConvertIfNotNarrowing() {
1906+
assertEquals(42L, NumberUtils.convertIfNotNarrowing((Integer) 42, Long.class));
1907+
assertEquals(42.0, NumberUtils.convertIfNotNarrowing((Long) 42L, Double.class));
1908+
assertEquals(100, NumberUtils.convertIfNotNarrowing((Integer) 100, Integer.class));
1909+
1910+
final IllegalArgumentException exNarrowing = assertThrows(IllegalArgumentException.class,
1911+
() -> NumberUtils.convertIfNotNarrowing((Long) 100L, Integer.class));
1912+
assertTrue(exNarrowing.getMessage().contains("Narrowing conversion"));
1913+
1914+
final IllegalArgumentException exNumberNull = assertThrows(IllegalArgumentException.class,
1915+
() -> NumberUtils.convertIfNotNarrowing(null, Integer.class));
1916+
assertTrue(exNumberNull.getMessage().contains("must not be null"));
1917+
1918+
final IllegalArgumentException exTargetTypeNull = assertThrows(IllegalArgumentException.class,
1919+
() -> NumberUtils.convertIfNotNarrowing(42, null));
1920+
assertTrue(exTargetTypeNull.getMessage().contains("must not be null"));
1921+
1922+
final IllegalArgumentException exUnsupportedFromBigInteger = assertThrows(IllegalArgumentException.class,
1923+
() -> NumberUtils.convertIfNotNarrowing(new java.math.BigInteger("42"), Double.class));
1924+
assertTrue(exUnsupportedFromBigInteger.getMessage().contains("Unsupported number type"));
1925+
1926+
final IllegalArgumentException exUnsupportedToBigInteger = assertThrows(IllegalArgumentException.class,
1927+
() -> NumberUtils.convertIfNotNarrowing(42, java.math.BigInteger.class));
1928+
assertTrue(exUnsupportedToBigInteger.getMessage().contains("Unsupported number type"));
1929+
}
19001930
}

src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ protected abstract static class AbstractGetMatchingMethod2 implements InterfaceG
7272
@Override
7373
public void testMethod6() { }
7474
}
75+
76+
interface VarArgInterface {
77+
}
78+
79+
public static class VarArgInterfaceImpl1 implements VarArgInterface {
80+
}
81+
82+
public static class VarArgInterfaceImpl2 implements VarArgInterface {
83+
}
84+
7585
interface ChildInterface {
7686
}
7787

@@ -289,6 +299,14 @@ public static String varOverload(final String... args) {
289299
return "String...";
290300
}
291301

302+
public static String varargsWithOtherArg(final int firstArg, final String... args) {
303+
return "int, String...";
304+
}
305+
306+
public static String varargsInterface(final VarArgInterface... args) {
307+
return "VarArgInterface...";
308+
}
309+
292310
public static ImmutablePair<String, Object[]> varOverloadEchoStatic(final Number... args) {
293311
return new ImmutablePair<>("Number...", args);
294312
}
@@ -994,6 +1012,16 @@ void testInvokeJavaVarargsOverloadingResolution() throws Exception {
9941012
(Object[]) ArrayUtils.EMPTY_CLASS_ARRAY));
9951013
}
9961014

1015+
@Test
1016+
void testInvokeJavaVarargsResolution() throws Exception {
1017+
assertEquals("int, String...", MethodUtils.invokeStaticMethod(TestBean.class, "varargsWithOtherArg", 1));
1018+
assertEquals("int, String...", MethodUtils.invokeStaticMethod(TestBean.class, "varargsWithOtherArg", 1, "s"));
1019+
assertEquals("int, String...", MethodUtils.invokeStaticMethod(TestBean.class, "varargsWithOtherArg", 1, "s1", "s2"));
1020+
assertThrows(NoSuchMethodException.class, () -> MethodUtils.invokeStaticMethod(TestBean.class, "varargsWithOtherArg", 1, "s1", 5));
1021+
assertEquals("VarArgInterface...", MethodUtils.invokeStaticMethod(TestBean.class, "varargsInterface",
1022+
new VarArgInterfaceImpl1(), new VarArgInterfaceImpl2()));
1023+
}
1024+
9971025
@Test
9981026
void testInvokeMethod() throws Exception {
9991027
assertEquals("foo()", MethodUtils.invokeMethod(testBean, "foo", (Object[]) ArrayUtils.EMPTY_CLASS_ARRAY));
@@ -1011,7 +1039,7 @@ void testInvokeMethod() throws Exception {
10111039
assertEquals("foo(String...)", MethodUtils.invokeMethod(testBean, "foo", "a", "b", "c"));
10121040
assertEquals("foo(int, String...)", MethodUtils.invokeMethod(testBean, "foo", 5, "a", "b", "c"));
10131041
assertEquals("foo(long...)", MethodUtils.invokeMethod(testBean, "foo", 1L, 2L));
1014-
assertThrows(NoSuchMethodException.class, () -> MethodUtils.invokeMethod(testBean, "foo", 1, 2));
1042+
assertEquals("foo(long...)", MethodUtils.invokeMethod(testBean, "foo", 1, 2));
10151043
TestBean.verify(new ImmutablePair<>("String...", new String[] { "x", "y" }), MethodUtils.invokeMethod(testBean, "varOverloadEcho", "x", "y"));
10161044
TestBean.verify(new ImmutablePair<>("Number...", new Number[] { 17, 23, 42 }), MethodUtils.invokeMethod(testBean, "varOverloadEcho", 17, 23, 42));
10171045
TestBean.verify(new ImmutablePair<>("String...", new String[] { "x", "y" }), MethodUtils.invokeMethod(testBean, "varOverloadEcho", "x", "y"));
@@ -1129,5 +1157,9 @@ public void verifyJavaVarargsOverloadingResolution() {
11291157
assertEquals("Number...", TestBean.varOverload((short) 1, (byte) 1));
11301158
assertEquals("Object...", TestBean.varOverload(1, 'c'));
11311159
assertEquals("Object...", TestBean.varOverload('c', "s"));
1160+
assertEquals("VarArgInterface...", TestBean.varargsInterface(
1161+
new VarArgInterfaceImpl1(),
1162+
new VarArgInterfaceImpl2()
1163+
));
11321164
}
11331165
}

0 commit comments

Comments
 (0)