diff --git a/core/src/main/java/io/grpc/internal/JsonUtil.java b/core/src/main/java/io/grpc/internal/JsonUtil.java index 6c9274702b6..f633c0910cc 100644 --- a/core/src/main/java/io/grpc/internal/JsonUtil.java +++ b/core/src/main/java/io/grpc/internal/JsonUtil.java @@ -18,6 +18,8 @@ import static com.google.common.math.LongMath.checkedAdd; +import java.math.BigDecimal; +import java.math.BigInteger; import java.text.ParseException; import java.util.List; import java.util.Locale; @@ -106,8 +108,8 @@ public static Double getNumberAsDouble(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Double) { - return (Double) value; + if (value instanceof Number) { + return ((Number) value).doubleValue(); } if (value instanceof String) { try { @@ -132,8 +134,8 @@ public static Float getNumberAsFloat(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Float) { - return (Float) value; + if (value instanceof Number) { + return ((Number) value).floatValue(); } if (value instanceof String) { try { @@ -159,13 +161,29 @@ public static Integer getNumberAsInteger(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Double) { - Double d = (Double) value; - int i = d.intValue(); - if (i != d) { - throw new ClassCastException("Number expected to be integer: " + d); + if (value instanceof Number) { + Number n = (Number) value; + if (n instanceof Integer || n instanceof Short || n instanceof Byte) { + return n.intValue(); } - return i; + try { + if (n instanceof Long) { + return Math.toIntExact(n.longValue()); + } + if (n instanceof BigInteger) { + return ((BigInteger) n).intValueExact(); + } + if (n instanceof BigDecimal) { + return ((BigDecimal) n).intValueExact(); + } + } catch (ArithmeticException e) { + throw new ClassCastException("Number expected to be integer: " + n); + } + double d = n.doubleValue(); + if (Math.rint(d) == d && d >= Integer.MIN_VALUE && d <= Integer.MAX_VALUE) { + return n.intValue(); + } + throw new ClassCastException("Number expected to be integer: " + n); } if (value instanceof String) { try { @@ -190,13 +208,26 @@ public static Long getNumberAsLong(Map obj, String key) { return null; } Object value = obj.get(key); - if (value instanceof Double) { - Double d = (Double) value; - long l = d.longValue(); - if (l != d) { - throw new ClassCastException("Number expected to be long: " + d); + if (value instanceof Number) { + Number n = (Number) value; + if (n instanceof Long || n instanceof Integer || n instanceof Short || n instanceof Byte) { + return n.longValue(); + } + try { + if (n instanceof BigInteger) { + return ((BigInteger) n).longValueExact(); + } + if (n instanceof BigDecimal) { + return ((BigDecimal) n).longValueExact(); + } + } catch (ArithmeticException e) { + throw new ClassCastException("Number expected to be long: " + n); + } + double d = n.doubleValue(); + if (Math.rint(d) == d && d >= (double) Long.MIN_VALUE && d < Math.scalb(1.0, 63)) { + return n.longValue(); } - return l; + throw new ClassCastException("Number expected to be long: " + n); } if (value instanceof String) { try { diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 128c929ec0e..5224298984d 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -582,7 +582,7 @@ public ManagedChannelImplBuilder defaultServiceConfig(@Nullable Map s parsedMap.put(key, checkListEntryTypes((List) value)); } else if (value instanceof String) { parsedMap.put(key, value); - } else if (value instanceof Double) { + } else if (value instanceof Number) { parsedMap.put(key, value); } else if (value instanceof Boolean) { parsedMap.put(key, value); @@ -606,7 +606,7 @@ private static List checkListEntryTypes(List list) { parsedList.add(checkListEntryTypes((List) value)); } else if (value instanceof String) { parsedList.add(value); - } else if (value instanceof Double) { + } else if (value instanceof Number) { parsedList.add(value); } else if (value instanceof Boolean) { parsedList.add(value); diff --git a/core/src/test/java/io/grpc/internal/JsonUtilTest.java b/core/src/test/java/io/grpc/internal/JsonUtilTest.java index 058171814ea..c4172beab26 100644 --- a/core/src/test/java/io/grpc/internal/JsonUtilTest.java +++ b/core/src/test/java/io/grpc/internal/JsonUtilTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -160,6 +162,74 @@ public void getNumber() { } } + @Test + public void getNumber_precision() { + Map map = new HashMap<>(); + long largeLong = (1L << 60) + 1; + map.put("large_long", (double) largeLong); // This is 2^60 + map.put("large_long_actual", largeLong); + map.put("big_int", new BigInteger(String.valueOf(largeLong))); + map.put("big_decimal", new BigDecimal(String.valueOf(largeLong))); + map.put( + "big_int_too_large", + new BigInteger(String.valueOf(largeLong)).multiply(BigInteger.valueOf(100))); + map.put("double_fractional", 1.5D); + map.put("double_nan", Double.NaN); + map.put("double_inf", Double.POSITIVE_INFINITY); + + // Large long represented as double is 2^60, which is a valid long + assertThat(JsonUtil.getNumberAsLong(map, "large_long")).isEqualTo(1L << 60); + + // Large long actual should pass + assertThat(JsonUtil.getNumberAsLong(map, "large_long_actual")).isEqualTo(largeLong); + + // BigInteger and BigDecimal should pass + assertThat(JsonUtil.getNumberAsLong(map, "big_int")).isEqualTo(largeLong); + assertThat(JsonUtil.getNumberAsLong(map, "big_decimal")).isEqualTo(largeLong); + + // Too large BigInteger should fail + try { + JsonUtil.getNumberAsLong(map, "big_int_too_large"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + + // Integer specific tests + map.put("int_actual", 123); + map.put("long_as_int", 123L); + map.put("long_too_large_for_int", (long) Integer.MAX_VALUE + 1); + + assertThat(JsonUtil.getNumberAsInteger(map, "int_actual")).isEqualTo(123); + assertThat(JsonUtil.getNumberAsInteger(map, "long_as_int")).isEqualTo(123); + try { + JsonUtil.getNumberAsInteger(map, "long_too_large_for_int"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be integer:"); + } + + // Fractional and special doubles + try { + JsonUtil.getNumberAsLong(map, "double_fractional"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + try { + JsonUtil.getNumberAsLong(map, "double_nan"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + try { + JsonUtil.getNumberAsLong(map, "double_inf"); + fail("Should have failed"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat().startsWith("Number expected to be long:"); + } + } + @Test public void getObject_mapExplicitNullValue() { Map mapWithNullValue = Collections.singletonMap("key", null); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index b0939239477..a34b4debb19 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -737,7 +737,39 @@ public void defaultServiceConfig_intValue() { Map config = new HashMap<>(); config.put("key", 3); - assertThrows(IllegalArgumentException.class, () -> builder.defaultServiceConfig(config)); + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void defaultServiceConfig_longValue() { + Map config = new HashMap<>(); + config.put("key", 3L); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void defaultServiceConfig_list_intValue() { + Map config = new HashMap<>(); + config.put("key", Collections.singletonList(3)); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void defaultServiceConfig_list_longValue() { + Map config = new HashMap<>(); + config.put("key", Collections.singletonList(3L)); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); } @Test