diff --git a/src/integration/java/com/couchbase/client/java/KeyValueTest.java b/src/integration/java/com/couchbase/client/java/KeyValueTest.java index 494bd42a..de64a5fc 100644 --- a/src/integration/java/com/couchbase/client/java/KeyValueTest.java +++ b/src/integration/java/com/couchbase/client/java/KeyValueTest.java @@ -32,6 +32,8 @@ import org.junit.Test; import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -572,4 +574,42 @@ public void shouldStoreAndGetKeyWithUtf16Char() { assertEquals(inserted.content(), retrieved.content()); } + @Test + public void shouldStoreAndGetBigInteger() { + BigInteger bigint = new BigInteger("12345678901234567890432423432324"); + JsonObject original = JsonObject + .create() + .put("value", bigint); + + bucket().upsert(JsonDocument.create("bigIntegerDoc", original)); + + RawJsonDocument rawLoaded = bucket().get("bigIntegerDoc", RawJsonDocument.class); + assertEquals("{\"value\":12345678901234567890432423432324}", rawLoaded.content()); + + JsonDocument loaded = bucket().get("bigIntegerDoc"); + assertEquals(bigint, loaded.content().getBigInteger("value")); + } + + @Test + public void shouldStoreAndGetBigDecimal() { + BigDecimal bigdec = new BigDecimal("12345.678901234567890432423432324"); + JsonObject original = JsonObject + .create() + .put("value", bigdec); + + bucket().upsert(JsonDocument.create("bigDecimalDoc", original)); + + RawJsonDocument rawLoaded = bucket().get("bigDecimalDoc", RawJsonDocument.class); + assertEquals("{\"value\":12345.678901234567890432423432324}", rawLoaded.content()); + + // Precision loss through double storage, use com.couchbase.json.decimalForFloat for higher + // precision. + JsonDocument loaded = bucket().get("bigDecimalDoc"); + assertEquals( + new BigDecimal("12345.678901234567092615179717540740966796875"), + loaded.content().getBigDecimal("value") + ); + assertEquals(12345.678901234567, loaded.content().getDouble("value"), 0); + } + } diff --git a/src/main/java/com/couchbase/client/java/document/json/JsonArray.java b/src/main/java/com/couchbase/client/java/document/json/JsonArray.java index 947d56c8..c621ac76 100644 --- a/src/main/java/com/couchbase/client/java/document/json/JsonArray.java +++ b/src/main/java/com/couchbase/client/java/document/json/JsonArray.java @@ -19,6 +19,8 @@ import com.couchbase.client.java.transcoder.JacksonTransformers; import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -421,6 +423,17 @@ public JsonArray add(List value) { return add(JsonArray.from(value)); } + /** + * Append a {@link Number} element to the {@link JsonArray}. + * + * @param value the value to append. + * @return the {@link JsonArray}. + */ + public JsonArray add(Number value) { + content.add(value); + return this; + } + /** * Retrieves the value by the position in the {@link JsonArray} and casts it to {@link JsonArray}. * @@ -432,6 +445,45 @@ public JsonArray getArray(int index) { return (JsonArray) content.get(index); } + /** + * Retrieves the value by the position in the {@link JsonArray} and casts it to {@link BigInteger}. + * + * @param index the index of the value. + * @return the value at the given index. + * @throws IndexOutOfBoundsException if the index is negative or too large. + */ + public BigInteger getBigInteger(int index) { + return (BigInteger) content.get(index); + } + + /** + * Retrieves the value by the position in the {@link JsonArray} and casts it to {@link BigDecimal}. + * + * @param index the index of the value. + * @return the value at the given index. + * @throws IndexOutOfBoundsException if the index is negative or too large. + */ + public BigDecimal getBigDecimal(int index) { + Object found = content.get(index); + if (found == null) { + return null; + } else if (found instanceof Double) { + return new BigDecimal((Double) found); + } + return (BigDecimal) found; + } + + /** + * Retrieves the value by the position in the {@link JsonArray} and casts it to {@link Number}. + * + * @param index the index of the value. + * @return the value at the given index. + * @throws IndexOutOfBoundsException if the index is negative or too large. + */ + public Number getNumber(int index) { + return (Number) content.get(index); + } + /** * Copies the content of the {@link JsonArray} into a new {@link List} and returns it. * Note that if the array contains sub-{@link JsonObject} or {@link JsonArray}, they diff --git a/src/main/java/com/couchbase/client/java/document/json/JsonObject.java b/src/main/java/com/couchbase/client/java/document/json/JsonObject.java index 23123d17..4444a630 100644 --- a/src/main/java/com/couchbase/client/java/document/json/JsonObject.java +++ b/src/main/java/com/couchbase/client/java/document/json/JsonObject.java @@ -19,7 +19,10 @@ import com.couchbase.client.java.CouchbaseAsyncBucket; import com.couchbase.client.java.transcoder.JacksonTransformers; +import java.io.ObjectInput; import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -389,6 +392,18 @@ public JsonObject put(String name, JsonArray value) { return this; } + /** + * Stores a {@link Number} value identified by the field name. + * + * @param name the name of the JSON field. + * @param value the value of the JSON field. + * @return the {@link JsonObject}. + */ + public JsonObject put(String name, Number value) { + content.put(name, value); + return this; + } + /** * Stores a {@link JsonArray} value identified by the field name. * @@ -410,6 +425,42 @@ public JsonArray getArray(String name) { return (JsonArray) content.get(name); } + /** + * Retrieves the value from the field name and casts it to {@link BigInteger}. + * + * @param name the name of the field. + * @return the result or null if it does not exist. + */ + public BigInteger getBigInteger(String name) { + return (BigInteger) content.get(name); + } + + /** + * Retrieves the value from the field name and casts it to {@link BigDecimal}. + * + * @param name the name of the field. + * @return the result or null if it does not exist. + */ + public BigDecimal getBigDecimal(String name) { + Object found = content.get(name); + if (found == null) { + return null; + } else if (found instanceof Double) { + return new BigDecimal((Double) found); + } + return (BigDecimal) found; + } + + /** + * Retrieves the value from the field name and casts it to {@link Number}. + * + * @param name the name of the field. + * @return the result or null if it does not exist. + */ + public Number getNumber(String name) { + return (Number) content.get(name); + } + /** * Store a null value identified by the field's name. * diff --git a/src/main/java/com/couchbase/client/java/document/json/JsonValue.java b/src/main/java/com/couchbase/client/java/document/json/JsonValue.java index 69890270..bdc40b37 100644 --- a/src/main/java/com/couchbase/client/java/document/json/JsonValue.java +++ b/src/main/java/com/couchbase/client/java/document/json/JsonValue.java @@ -15,6 +15,9 @@ */ package com.couchbase.client.java.document.json; +import java.math.BigDecimal; +import java.math.BigInteger; + /** * Represents a JSON value (either a {@link JsonObject} or a {@link JsonArray}. * @@ -59,6 +62,8 @@ public static boolean checkType(Object item) { || item instanceof Long || item instanceof Double || item instanceof Boolean + || item instanceof BigInteger + || item instanceof BigDecimal || item instanceof JsonObject || item instanceof JsonArray; } diff --git a/src/main/java/com/couchbase/client/java/transcoder/JacksonTransformers.java b/src/main/java/com/couchbase/client/java/transcoder/JacksonTransformers.java index 1f31b5d2..7ee8a9ab 100644 --- a/src/main/java/com/couchbase/client/java/transcoder/JacksonTransformers.java +++ b/src/main/java/com/couchbase/client/java/transcoder/JacksonTransformers.java @@ -22,6 +22,7 @@ import com.couchbase.client.java.document.json.JsonObject; import java.io.IOException; +import java.math.BigDecimal; public class JacksonTransformers { @@ -56,6 +57,15 @@ public void serialize(JsonArray value, JsonGenerator jgen, } static abstract class AbstractJsonValueDeserializer extends JsonDeserializer { + + private final boolean decimalForFloat; + + public AbstractJsonValueDeserializer() { + decimalForFloat = Boolean.parseBoolean( + System.getProperty("com.couchbase.json.decimalForFloat", "false") + ); + } + protected JsonObject decodeObject(final JsonParser parser, final JsonObject target) throws IOException { JsonToken current = parser.nextToken(); String field = null; @@ -76,10 +86,12 @@ protected JsonObject decodeObject(final JsonParser parser, final JsonObject targ target.put(field, parser.getValueAsString()); break; case VALUE_NUMBER_INT: - target.put(field, parser.getNumberValue()); - break; case VALUE_NUMBER_FLOAT: - target.put(field, parser.getDoubleValue()); + Number numberValue = parser.getNumberValue(); + if (numberValue instanceof Double && decimalForFloat) { + numberValue = parser.getDecimalValue(); + } + target.put(field, numberValue); break; case VALUE_NULL: target.put(field, (JsonObject) null); @@ -111,10 +123,12 @@ protected JsonArray decodeArray(final JsonParser parser, final JsonArray target) target.add(parser.getValueAsString()); break; case VALUE_NUMBER_INT: - target.add(parser.getNumberValue()); - break; case VALUE_NUMBER_FLOAT: - target.add(parser.getDoubleValue()); + Number numberValue = parser.getNumberValue(); + if (numberValue instanceof Double && decimalForFloat) { + numberValue = parser.getDecimalValue(); + } + target.add(numberValue); break; case VALUE_NULL: target.add((JsonObject) null); diff --git a/src/test/java/com/couchbase/client/java/document/json/JsonArrayTest.java b/src/test/java/com/couchbase/client/java/document/json/JsonArrayTest.java index 6e1a01b5..0fff0056 100644 --- a/src/test/java/com/couchbase/client/java/document/json/JsonArrayTest.java +++ b/src/test/java/com/couchbase/client/java/document/json/JsonArrayTest.java @@ -18,6 +18,8 @@ import com.couchbase.client.java.SerializationHelper; import org.junit.Test; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -359,4 +361,38 @@ public void shouldSupportSerialization() throws Exception { JsonArray deserialized = SerializationHelper.deserializeFromBytes(serialized, JsonArray.class); assertEquals(original, deserialized); } + + @Test + public void shouldSupportBigInteger() throws Exception { + BigInteger bigint = new BigInteger("12345678901234567890"); + JsonArray original = JsonArray.from(bigint); + + + String encoded = original.toString(); + assertEquals("[12345678901234567890]", encoded); + + JsonArray decoded = JsonArray.fromJson(encoded); + assertEquals(bigint, decoded.getBigInteger(0)); + assertTrue(decoded.getNumber(0) instanceof BigInteger); + + } + + @Test + public void shouldSupportBigDecimalConverted() throws Exception { + BigDecimal bigdec = new BigDecimal("1234.5678901234567890432423432324"); + JsonArray original = JsonArray.from(bigdec); + + + String encoded = original.toString(); + assertEquals("[1234.5678901234567890432423432324]", encoded); + + JsonArray decoded = JsonArray.fromJson(encoded); + // This happens because of double rounding, set com.couchbase.json.decimalForFloat true for better accuracy + // but more overhead + assertEquals( + new BigDecimal("1234.5678901234568911604583263397216796875"), decoded.getBigDecimal(0) + ); + assertEquals(1234.567890123457, decoded.getDouble(0), 0); + assertTrue(decoded.getNumber(0) instanceof Double); + } } diff --git a/src/test/java/com/couchbase/client/java/document/json/JsonObjectTest.java b/src/test/java/com/couchbase/client/java/document/json/JsonObjectTest.java index e52e1795..191ac99f 100644 --- a/src/test/java/com/couchbase/client/java/document/json/JsonObjectTest.java +++ b/src/test/java/com/couchbase/client/java/document/json/JsonObjectTest.java @@ -18,6 +18,8 @@ import com.couchbase.client.java.SerializationHelper; import org.junit.Test; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -441,4 +443,42 @@ public void shouldSupportSerialization() throws Exception { assertEquals(original, deserialized); } + @Test + public void shouldSupportBigInteger() throws Exception { + BigInteger bigint = new BigInteger("12345678901234567890"); + JsonObject original = JsonObject + .create() + .put("value", bigint); + + + String encoded = original.toString(); + assertEquals("{\"value\":12345678901234567890}", encoded); + + JsonObject decoded = JsonObject.fromJson(encoded); + assertEquals(bigint, decoded.getBigInteger("value")); + assertTrue(decoded.getNumber("value") instanceof BigInteger); + } + + @Test + public void shouldSupportBigDecimalConverted() throws Exception { + BigDecimal bigdec = new BigDecimal("1234.5678901234567890432423432324"); + JsonObject original = JsonObject + .create() + .put("value", bigdec); + + + String encoded = original.toString(); + assertEquals("{\"value\":1234.5678901234567890432423432324}", encoded); + + JsonObject decoded = JsonObject.fromJson(encoded); + // This happens because of double rounding, set com.couchbase.json.decimalForFloat true for better accuracy + // but more overhead + assertEquals( + new BigDecimal("1234.5678901234568911604583263397216796875"), + decoded.getBigDecimal("value") + ); + assertEquals(1234.567890123457, decoded.getDouble("value"), 0); + assertTrue(decoded.getNumber("value") instanceof Double); + } + } \ No newline at end of file diff --git a/src/test/java/com/couchbase/client/java/transcoder/BigDecimalExactConversionTest.java b/src/test/java/com/couchbase/client/java/transcoder/BigDecimalExactConversionTest.java new file mode 100644 index 00000000..e28ebca9 --- /dev/null +++ b/src/test/java/com/couchbase/client/java/transcoder/BigDecimalExactConversionTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016 Couchbase, Inc. + * + * Licensed 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 com.couchbase.client.java.transcoder; + +import com.couchbase.client.deps.com.fasterxml.jackson.core.Version; +import com.couchbase.client.deps.com.fasterxml.jackson.databind.ObjectMapper; +import com.couchbase.client.deps.com.fasterxml.jackson.databind.module.SimpleModule; +import com.couchbase.client.java.document.json.JsonArray; +import com.couchbase.client.java.document.json.JsonObject; +import org.junit.Test; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Verifies the functionality of {@link java.math.BigDecimal} conversion with the system property + * set. + * + * @author Michael Nitschinger + * @since 2.4.0 + */ +public class BigDecimalExactConversionTest { + + public static final ObjectMapper MAPPER = new ObjectMapper(); + public static final SimpleModule JSON_VALUE_MODULE = new SimpleModule("JsonValueModule", + new Version(1, 0, 0, null, null, null)); + + static { + System.setProperty("com.couchbase.json.decimalForFloat", "true"); + + JSON_VALUE_MODULE.addSerializer(JsonObject.class, new JacksonTransformers.JsonObjectSerializer()); + JSON_VALUE_MODULE.addSerializer(JsonArray.class, new JacksonTransformers.JsonArraySerializer()); + JSON_VALUE_MODULE.addDeserializer(JsonObject.class, new JacksonTransformers.JsonObjectDeserializer()); + JSON_VALUE_MODULE.addDeserializer(JsonArray.class, new JacksonTransformers.JsonArrayDeserializer()); + MAPPER.registerModule(JSON_VALUE_MODULE); + } + + @Test + public void shouldSupportBigDecimalOnJsonObject() throws Exception { + BigDecimal bigdec = new BigDecimal("1234.5678901234567890432423432324"); + JsonObject original = JsonObject + .create() + .put("value", bigdec); + + String encoded = original.toString(); + assertEquals("{\"value\":1234.5678901234567890432423432324}", encoded); + + + JsonObject decoded = MAPPER.readValue(encoded, JsonObject.class); + assertEquals(bigdec, decoded.getBigDecimal("value")); + assertEquals(1234.567890123457, decoded.getDouble("value"), 0); + assertTrue(decoded.getNumber("value") instanceof BigDecimal); + } + + @Test + public void shouldSupportBigDecimalOnJsonArray() throws Exception { + BigDecimal bigdec = new BigDecimal("1234.5678901234567890432423432324"); + JsonArray original = JsonArray.from(bigdec); + + String encoded = original.toString(); + assertEquals("[1234.5678901234567890432423432324]", encoded); + + JsonArray decoded = MAPPER.readValue(encoded, JsonArray.class); + assertEquals(bigdec, decoded.getBigDecimal(0)); + assertEquals(1234.567890123457, decoded.getDouble(0), 0); + assertTrue(decoded.getNumber(0) instanceof BigDecimal); + } + +}