From 9a7f9ab0ac619acca970774d385a86a0646e62ce Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Mon, 23 May 2022 17:04:51 +0200 Subject: [PATCH] Ensure double values stay double even if they fit in an integer (#289) --- .../clients/json/jackson/JsonValueParser.java | 98 ++++++++++++++++++- .../json/jackson/JsonValueParserTest.java | 78 +++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 java-client/src/test/java/co/elastic/clients/elasticsearch/json/jackson/JsonValueParserTest.java diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java index 204c05d20..51cbd2099 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.JsonToken; import jakarta.json.JsonArray; import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonNumber; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonValue; @@ -31,6 +32,8 @@ import jakarta.json.stream.JsonParsingException; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; /** * Reads a Jsonp value/object/array from a Jackson parser. The parser's current token should be the start of the @@ -65,6 +68,7 @@ public JsonArray parseArray(JsonParser parser) throws IOException { } public JsonValue parseValue(JsonParser parser) throws IOException { + JsonToken jsonToken = parser.currentToken(); switch (parser.currentToken()) { case START_OBJECT: return parseObject(parser); @@ -93,7 +97,8 @@ public JsonValue parseValue(JsonParser parser) throws IOException { return provider.createValue(parser.getLongValue()); case FLOAT: case DOUBLE: - return provider.createValue(parser.getDoubleValue()); + // Use double also for floats, as JSON-P has no support for float + return new DoubleNumber(parser.getDoubleValue()); case BIG_DECIMAL: return provider.createValue(parser.getDecimalValue()); case BIG_INTEGER: @@ -105,4 +110,95 @@ public JsonValue parseValue(JsonParser parser) throws IOException { } } + + private static class DoubleNumber implements JsonNumber { + + private final double value; + + DoubleNumber(double value) { + this.value = value; + } + + @Override + public boolean isIntegral() { + return false; + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public int intValueExact() { + int result = (int) value; + + if ((double)result == value) { + return result; + } else { + throw new ArithmeticException(); + } + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public long longValueExact() { + long result = (long) value; + + if ((double)result == value) { + return result; + } else { + throw new ArithmeticException(); + } + } + + @Override + public BigInteger bigIntegerValue() { + return bigDecimalValue().toBigInteger(); + } + + @Override + public BigInteger bigIntegerValueExact() { + return bigDecimalValue().toBigIntegerExact(); + } + + @Override + public double doubleValue() { + return value; + } + + @Override + public BigDecimal bigDecimalValue() { + return new BigDecimal(value); + } + + @Override + public ValueType getValueType() { + return ValueType.NUMBER; + } + + @Override + public Number numberValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public int hashCode() { + return Double.hashCode(value); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof DoubleNumber && ((DoubleNumber)obj).value == value; + } + } } diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/json/jackson/JsonValueParserTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/json/jackson/JsonValueParserTest.java new file mode 100644 index 000000000..bed4b2c1b --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/json/jackson/JsonValueParserTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package co.elastic.clients.elasticsearch.json.jackson; + +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpUtils; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParser; +import org.junit.Assert; +import org.junit.Test; + +import java.io.StringReader; +import java.util.Map; + +public class JsonValueParserTest extends Assert { + + public static class Data { + public Map data; + } + + @Test + public void testFloatsShouldDeserializeAsFloats() throws Exception { + // When using Jackson to target a map of objects, values with a decimal separator + // should deserialize as a double even if they fit in an int or long. + // See https://github.com/elastic/elasticsearch-java/issues/156 + + String json = "{\"data\": {\"value\": 1.4778125E7, \"value2\": 1.4778125E7 }}"; + JsonpMapper mapper = new JacksonJsonpMapper(); + + { + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + Data data = mapper.deserialize(parser, Data.class); + + Double d = (Double)data.data.get("value"); + assertEquals(1.4778125E7, d, 0.001); + } + + { + // Test with buffering used in union resolution + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + parser.next(); + JsonObject object = parser.getObject(); + + // Test equals/hashcode + JsonValue v = object.getJsonObject("data").get("value"); + JsonValue v2 = object.getJsonObject("data").get("value2"); + + assertEquals(v.hashCode(), v2.hashCode()); + assertEquals(v, v2); + + parser = JsonpUtils.objectParser(object, mapper); + Data data = mapper.deserialize(parser, Data.class); + + Double d = (Double)data.data.get("value"); + assertEquals(1.4778125E7, d, 0.001); + } + + } +}