This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 5fbd2eba0a support better minidev and google json parsing and 
serialization
5fbd2eba0a is described below

commit 5fbd2eba0aa0b4c88b6b0fb1f83b42ba252ed0f0
Author: Alex Heneveld <[email protected]>
AuthorDate: Tue Sep 26 10:46:59 2023 +0100

    support better minidev and google json parsing and serialization
    
    minidev JSONObject persisted correctly if used;
    JsonFunctions cast using google JsonParser, and can cast to Object or Map 
deeply to remove JsonElements there;
    added utils NumberMath allowing detection of number types and math on them
---
 .../apache/brooklyn/feed/http/JsonFunctions.java   | 106 +++++++++---
 .../core/xstream/MinidevJsonObjectConverter.java   |  60 +++++++
 .../brooklyn/util/core/xstream/XmlSerializer.java  |   1 +
 .../brooklyn/feed/http/JsonFunctionsTest.java      |  15 ++
 .../util/core/xstream/XmlSerializerTest.java       |  34 +++-
 .../rest/resources/SensorResourceTest.java         |  37 +++++
 .../org/apache/brooklyn/util/math/NumberMath.java  | 178 +++++++++++++++++++++
 .../apache/brooklyn/util/math/NumberMathTest.java  |  47 ++++++
 8 files changed, 455 insertions(+), 23 deletions(-)

diff --git 
a/core/src/main/java/org/apache/brooklyn/feed/http/JsonFunctions.java 
b/core/src/main/java/org/apache/brooklyn/feed/http/JsonFunctions.java
index 90900fa1d0..69916a7c5e 100644
--- a/core/src/main/java/org/apache/brooklyn/feed/http/JsonFunctions.java
+++ b/core/src/main/java/org/apache/brooklyn/feed/http/JsonFunctions.java
@@ -25,10 +25,19 @@ import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
 
 import javax.annotation.Nullable;
 
+import com.google.common.reflect.TypeToken;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.internal.LazilyParsedNumber;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.guava.Functionals;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.guava.MaybeFunctions;
@@ -41,6 +50,8 @@ import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 import com.jayway.jsonpath.JsonPath;
+import org.apache.brooklyn.util.guava.TypeTokens;
+import org.apache.brooklyn.util.math.NumberMath;
 
 public class JsonFunctions {
 
@@ -340,11 +351,20 @@ public class JsonFunctions {
 
     @SuppressWarnings("unchecked")
     protected static <T> T doCast(JsonElement input, Class<T> expected) {
+        return doCast(input, TypeToken.of(expected));
+    }
+    protected static <T> T doCast(JsonElement input, TypeToken<T> 
expectedType) {
         if (input == null) {
             return null;
         } else if (input.isJsonNull()) {
             return null;
-        } else if (expected == boolean.class || expected == Boolean.class) {
+        }
+        Class<? super T> expected = expectedType.getRawType();
+        Function<Function<JsonPrimitive,Boolean>, Boolean> handlePrimitive = 
fn -> {
+            if (Object.class.equals(expected) && input.isJsonPrimitive()) 
return fn.apply((JsonPrimitive) input);
+            return false;
+        };
+        if (expected == boolean.class || expected == Boolean.class || 
handlePrimitive.apply(JsonPrimitive::isBoolean)) {
             return (T) (Boolean) input.getAsBoolean();
         } else if (expected == char.class || expected == Character.class) {
             return (T) (Character) input.getAsCharacter();
@@ -364,30 +384,76 @@ public class JsonFunctions {
             return (T) input.getAsBigDecimal();
         } else if (expected == BigInteger.class) {
             return (T) input.getAsBigInteger();
-        } else if (Number.class.isAssignableFrom(expected)) {
-            // TODO Will result in a class-cast if it's an unexpected sub-type 
of Number not handled above
-            return (T) input.getAsNumber();
-        } else if (expected == String.class) {
+        } else if (Number.class.isAssignableFrom(expected) || 
handlePrimitive.apply(JsonPrimitive::isNumber)) {
+            // May result in a class-cast if it's an unexpected sub-type of 
Number not handled above
+            // Also ends up as LazilyParsedNumber which we probably don't want
+            Number result = input.getAsNumber();
+            Number r2 = new NumberMath(result, 
Number.class).asTypeForced(Number.class);
+            if (r2==null) r2 = result;
+            return (T) r2;
+        } else if (expected == String.class || 
handlePrimitive.apply(JsonPrimitive::isString)) {
             return (T) input.getAsString();
-        } else if (expected.isArray()) {
+        }
+
+        // now complex types
+        if (JsonElement.class.isAssignableFrom(expected)) {
+            return (T) input;
+        }
+
+        if (Iterable.class.isAssignableFrom(expected) || expected.isArray()) {
             JsonArray array = input.getAsJsonArray();
-            Class<?> componentType = expected.getComponentType();
-            if (JsonElement.class.isAssignableFrom(componentType)) {
-                JsonElement[] result = new JsonElement[array.size()];
-                for (int i = 0; i < array.size(); i++) {
-                    result[i] = array.get(i);
-                }
-                return (T) result;
+            MutableList ml = MutableList.of();
+            TypeToken<?> componentType;
+            if (expectedType.getComponentType()!=null) componentType = 
expectedType.getComponentType();
+            else {
+                TypeToken<?>[] params = 
TypeTokens.getGenericParameterTypeTokens(expectedType);
+                componentType = params != null && params.length == 1 ? 
params[0] : TypeToken.of(Object.class);
+            }
+
+            if 
(JsonElement.class.isAssignableFrom(componentType.getRawType())) 
ml.addAll(array);
+            else array.forEach(a -> ml.add(doCast(a, componentType)));
+
+            if (expected.isAssignableFrom(MutableList.class)) {
+                return (T) ml;
+            }
+            if (expected.isAssignableFrom(MutableSet.class)) {
+                return (T) MutableSet.copyOf(ml);
+            }
+            if (expected.isArray()) {
+                return (T) ml.toArray((Object[]) 
Array.newInstance(componentType.getRawType(), 0));
+            }
+        }
+
+        if (Map.class.isAssignableFrom(expected)) {
+            JsonObject jo = input.getAsJsonObject();
+
+            TypeToken<?>[] params = 
TypeTokens.getGenericParameterTypeTokens(expectedType);
+            TypeToken<?> value;
+            if (params!=null && params.length==1) {
+                // probably shouldn't happen? but if we supported other maps 
it might
+                value = params[0];
+            } else if (params!=null && params.length==2) {
+                value = params[1];
+                TypeToken<?> key = params[0];
+                if (!TypeTokens.isAssignableFromRaw(key, String.class)) throw 
new IllegalArgumentException("Keys of type "+key+" not supported when 
deserializing JSON");
             } else {
-                Object[] result = (Object[]) Array.newInstance(componentType, 
array.size());
-                for (int i = 0; i < array.size(); i++) {
-                    result[i] = cast(componentType).apply(array.get(i));
-                }
-                return (T) result;
+                value = TypeToken.of(Object.class);
+            }
+
+            Map mm = MutableMap.of();
+            jo.entrySet().forEach(jos -> mm.put(jos.getKey(), 
doCast(jos.getValue(), value)));
+            if (expected.isAssignableFrom(MutableMap.class)) {
+                return (T) mm;
             }
-        } else {
-            throw new IllegalArgumentException("Cannot cast json element to 
type "+expected);
         }
+
+        if (Object.class.equals(expected)) {
+            // primitives should have beenhandled above
+            if (input.isJsonObject()) return (T) doCast(input, Map.class);
+            if (input.isJsonArray()) return (T) doCast(input, List.class);
+        }
+
+        throw new IllegalArgumentException("Cannot cast json element to type 
"+expected);
     }
     
     public static <T> Function<Maybe<JsonElement>, T> castM(final Class<T> 
expected) {
diff --git 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/MinidevJsonObjectConverter.java
 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/MinidevJsonObjectConverter.java
new file mode 100644
index 0000000000..7afdb293fc
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/MinidevJsonObjectConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.brooklyn.util.core.xstream;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+import net.minidev.json.JSONObject;
+
+// JSONObject is
+public class MinidevJsonObjectConverter extends MapConverter {
+
+    public MinidevJsonObjectConverter(Mapper mapper) {
+        super(mapper);
+    }
+
+    @Override
+    public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
+        return JSONObject.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public void marshal(Object source, HierarchicalStreamWriter writer, 
MarshallingContext context) {
+        // marshall as a normal map
+        super.marshal(source, writer, context);
+    }
+
+    // return as a JSONObject; this class invoked when XML attributes specify 
that is the type
+    @Override
+    public JSONObject unmarshal(HierarchicalStreamReader reader, 
UnmarshallingContext context) {
+        // unmarshall as a normal map
+        Map result = (Map) super.unmarshal(reader, context);
+        // but return as a JSONObject, just in case that is required
+        return new JSONObject(result);
+    }
+}
diff --git 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
index d7b19cb27f..b643391285 100644
--- 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
+++ 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
@@ -159,6 +159,7 @@ public class XmlSerializer<T> {
         xstream.registerConverter(new 
ImmutableListConverter(xstream.getMapper()));
         xstream.registerConverter(new 
ImmutableSetConverter(xstream.getMapper()));
         xstream.registerConverter(new 
ImmutableMapConverter(xstream.getMapper()));
+        xstream.registerConverter(new 
MinidevJsonObjectConverter(xstream.getMapper()));
 
         xstream.registerConverter(new 
HashMultimapConverter(xstream.getMapper()));
 
diff --git 
a/core/src/test/java/org/apache/brooklyn/feed/http/JsonFunctionsTest.java 
b/core/src/test/java/org/apache/brooklyn/feed/http/JsonFunctionsTest.java
index 638e10dd44..8e7b3f2cc9 100644
--- a/core/src/test/java/org/apache/brooklyn/feed/http/JsonFunctionsTest.java
+++ b/core/src/test/java/org/apache/brooklyn/feed/http/JsonFunctionsTest.java
@@ -18,10 +18,16 @@
  */
 package org.apache.brooklyn.feed.http;
 
+import java.lang.reflect.Array;
+import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 
+import com.google.common.reflect.TypeToken;
+import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.Jsonya;
 import org.apache.brooklyn.util.collections.Jsonya.Navigator;
+import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.guava.Functionals;
 import org.apache.brooklyn.util.guava.Maybe;
@@ -102,6 +108,15 @@ public class JsonFunctionsTest {
         Maybe<JsonElement> m = JsonFunctions.walkM("europe", "france").apply( 
Maybe.of( europeMap()) );
         JsonFunctions.castM(String.class).apply(m);
     }
+
+    @Test
+    public void testCastCollectionsAndMaps() {
+        
Asserts.assertEquals(JsonFunctions.doCast(JsonParser.parseString("[1,2,3]"), 
(new Integer[]{}).getClass()), new Integer[] { 1, 2, 3 });
+        
Asserts.assertEquals(JsonFunctions.doCast(JsonParser.parseString("[1,2,3]"), 
new TypeToken<List<Integer>>() {}), MutableList.of(1, 2, 3));
+        
Asserts.assertEquals(JsonFunctions.doCast(JsonParser.parseString("[1,2,3]"), 
JsonElement.class), JsonParser.parseString("[1,2,3]"));
+        Asserts.assertEquals(JsonFunctions.doCast(JsonParser.parseString("{ 
\"a\": 1 }"), new TypeToken<Map<String,Integer>>() {}), MutableMap.of("a", 1));
+        Asserts.assertEquals(JsonFunctions.doCast(JsonParser.parseString("{ 
\"a\": 1 }"), Object.class), MutableMap.of("a", 1));
+    }
     
     @Test
     public void testWalkN() {
diff --git 
a/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
 
b/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
index b3c38d24b8..5710b32f7e 100644
--- 
a/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
+++ 
b/core/src/test/java/org/apache/brooklyn/util/core/xstream/XmlSerializerTest.java
@@ -28,13 +28,14 @@ import java.util.Arrays;
 import java.util.Map;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
+
+import net.minidev.json.JSONObject;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import 
org.apache.brooklyn.util.core.xstream.LambdaPreventionMapper.LambdaPersistenceMode;
-import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import static org.testng.Assert.assertEquals;
@@ -217,11 +218,12 @@ public class XmlSerializerTest {
     }
 
 
-    protected void assertSerializeAndDeserialize(Object val) throws Exception {
+    protected Object assertSerializeAndDeserialize(Object val) throws 
Exception {
         String xml = serializer.toString(val);
         Object result = serializer.fromString(xml);
         LOG.debug("val="+val+"'; xml="+xml+"; result="+result);
         assertEquals(result, val);
+        return result;
     }
 
     public static class StringHolder {
@@ -270,7 +272,7 @@ public class XmlSerializerTest {
     public static class IntHolder {
         public int val;
         
-        IntHolder(int val) {
+        public IntHolder(int val) {
             this.val = val;
         }
         @Override
@@ -329,4 +331,30 @@ public class XmlSerializerTest {
             return (messageType == o.messageType) && important == o.important 
&& content.equals(o.content);
         }
     }
+
+
+    static class MinidevJsonObjectHolder {
+        JSONObject jo;
+
+        @Override
+        public boolean equals(Object o2) {
+            return (o2 instanceof MinidevJsonObjectHolder) && 
java.util.Objects.equals( ((MinidevJsonObjectHolder)o2).jo, jo );
+        }
+    }
+
+    @Test
+    public void testMinidevJsonObject() throws Exception {
+        JSONObject x = new JSONObject(MutableMap.of("cc", 3));
+        x = new JSONObject(MutableMap.of("a", 1, "b", 2, "c", x));
+        Map x0 = (Map) assertSerializeAndDeserialize(x);
+        Asserts.assertTrue(x0 instanceof JSONObject);
+        Asserts.assertTrue(x0.get("c") instanceof JSONObject);
+
+        // holder test doesn't really do anything here as above preserves 
type, type info stored in attribute;
+        // but kept for good measure
+        MinidevJsonObjectHolder y = new MinidevJsonObjectHolder();
+        y.jo = x;
+        MinidevJsonObjectHolder y0 = (MinidevJsonObjectHolder) 
assertSerializeAndDeserialize(y);
+        Asserts.assertTrue(y0.jo instanceof JSONObject);
+    }
 }
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
index d5f3e19112..23dbdcb79b 100644
--- 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
@@ -28,6 +28,7 @@ import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import net.minidev.json.JSONObject;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
@@ -444,4 +445,40 @@ public class SensorResourceTest extends 
BrooklynRestResourceTest {
                 .get();
         
Asserts.assertStringDoesNotContain(""+response.readEntity(Object.class), 
""+SECRET_VALUE);
     }
+
+    @Test
+    public void testSetFromMapPreservesMaps() throws Exception {
+        final AttributeSensor<Object> OBJ_SENSOR = 
Sensors.newSensor(Object.class, "obj_sensor");
+        final Runnable REMOVE = () -> entity.sensors().remove(OBJ_SENSOR);
+        try {
+
+            Response response = client().path(SENSORS_ENDPOINT)
+                    .type(MediaType.APPLICATION_JSON_TYPE)
+                    .post(MutableMap.of(OBJ_SENSOR.getName(), 
MutableMap.of("a", 1, "b", MutableMap.of("bb", "2"))));
+            assertEquals(response.getStatus(), 
Response.Status.NO_CONTENT.getStatusCode());
+
+            final Runnable CHECK = () -> {
+                // we don't want minidev JSON objects ending up in sensors; 
convert to normal maps
+                Map osm = (Map) entity.getAttribute(OBJ_SENSOR);
+                assertEquals(osm.get("a"), 1);
+                Asserts.assertFalse(osm instanceof JSONObject);
+
+                Map osmb = (Map) osm.get("b");
+                assertEquals(osmb.get("bb"), "2");
+                Asserts.assertFalse(osmb instanceof JSONObject);
+            };
+            CHECK.run();
+
+            REMOVE.run();
+
+            // and should be similar when posting to the endpont
+            response = client().path(SENSORS_ENDPOINT+"/"+OBJ_SENSOR.getName())
+                    .type(MediaType.APPLICATION_JSON_TYPE)
+                    .post(MutableMap.of("a", 1, "b", MutableMap.of("bb", 
"2")));
+            assertEquals(response.getStatus(), 
Response.Status.NO_CONTENT.getStatusCode());
+            CHECK.run();
+
+        } finally { REMOVE.run(); }
+    }
+
 }
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/math/NumberMath.java 
b/utils/common/src/main/java/org/apache/brooklyn/util/math/NumberMath.java
new file mode 100644
index 0000000000..023506fc66
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/math/NumberMath.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.brooklyn.util.math;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+
+public class NumberMath<T extends Number> {
+
+    static final BigDecimal DEFAULT_TOLERANCE = new BigDecimal(0.00000001);
+
+    final T number;
+    final Class<T> desiredType;
+    final Function<Number, T> handlerForUncastableType;
+    final BigDecimal tolerance;
+
+    public NumberMath(T number) {
+        this(number, (Class<T>)number.getClass(), null);
+    }
+    /** callers can pass `Number` as second arg if they will accept any type; 
otherwise the input type is expected as the default */
+    public NumberMath(T number, Class<T> desiredType) {
+        this(number, desiredType, null);
+    }
+    public NumberMath(T number, Class<T> desiredType, Function<Number,T> 
handlerForUncastableType) {
+        this(number, desiredType, handlerForUncastableType, null);
+    }
+    public NumberMath(T number, Class<T> desiredType, Function<Number,T> 
handlerForUncastableType, BigDecimal tolerance) {
+        this.number = number;
+        this.desiredType = desiredType;
+        this.handlerForUncastableType = handlerForUncastableType==null ? (x -> 
{ throw new IllegalArgumentException("Cannot cast "+x+" to 
"+number.getClass()); }) : handlerForUncastableType;
+        this.tolerance = tolerance==null ? DEFAULT_TOLERANCE : null;
+    }
+
+    public BigDecimal asBigDecimal() { return asBigDecimal(number); }
+    public Optional<BigInteger> asBigIntegerWithinTolerance() { return 
asBigIntegerWithinTolerance(number); }
+
+    public <T extends Number> T asTypeForced(Class<T> desiredType) {
+        return asTypeFirstMatching(number, desiredType, y -> 
withinTolerance(number, y));
+    }
+    public <T extends Number> Optional<T> asTypeWithinTolerance(Class<T> 
desiredType, Number tolerance) {
+        return Optional.ofNullable(asTypeFirstMatching(number, desiredType, y 
-> withinTolerance(number, y, tolerance)));
+    }
+
+    public static boolean isPrimitiveWholeNumberType(Number number) {
+        return number instanceof Long || number instanceof Integer || number 
instanceof Short || number instanceof Byte;
+    }
+
+    public static boolean isPrimitiveFloatingPointType(Number number) {
+        return number instanceof Double || number instanceof Float;
+    }
+
+    public static boolean isPrimitiveNumberType(Number number) {
+        return isPrimitiveFloatingPointType(number) || 
isPrimitiveWholeNumberType(number);
+    }
+
+    public static BigDecimal asBigDecimal(Number number) {
+        if (number instanceof BigDecimal) return (BigDecimal) number;
+        if (isPrimitiveFloatingPointType(number)) return new 
BigDecimal(number.doubleValue());
+        if (isPrimitiveWholeNumberType(number)) return new 
BigDecimal(number.longValue());
+        if (number instanceof BigInteger) return new BigDecimal((BigInteger) 
number);
+        return new BigDecimal(""+number);
+    }
+
+    public Optional<BigInteger> asBigIntegerWithinTolerance(Number number) {
+        BigInteger candidate = asBigIntegerForced(number);
+        if (withinTolerance(number, candidate)) return Optional.of(candidate);
+        return Optional.empty();
+    }
+
+    public static BigInteger asBigIntegerForced(Number number) {
+        if (number instanceof BigInteger) return (BigInteger) number;
+        if (isPrimitiveWholeNumberType(number)) return 
BigInteger.valueOf(number.longValue());
+        return asBigDecimal(number).toBigInteger();
+    }
+
+    public static <T extends Number> T asTypeForced(Number x, Class<T> 
desiredType) {
+        return asTypeFirstMatching(x, desiredType, y -> withinTolerance(x, y, 
DEFAULT_TOLERANCE));
+    }
+    public static <T extends Number> Optional<T> asTypeWithinTolerance(Number 
x, Class<T> desiredType, Number tolerance) {
+        return Optional.ofNullable(asTypeFirstMatching(x, desiredType, y -> 
withinTolerance(x, y, tolerance)));
+    }
+    protected static <T extends Number> T asTypeFirstMatching(Number x, 
Class<T> desiredType, Predicate<T> check) {
+        if (desiredType.isAssignableFrom(Integer.class)) { T candidate = (T) 
(Object) x.intValue(); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(Long.class)) { T candidate = (T) 
(Object) x.longValue(); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(Double.class)) { T candidate = (T) 
(Object) x.doubleValue(); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(Float.class)) { T candidate = (T) 
(Object) x.floatValue(); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(Short.class)) { T candidate = (T) 
(Object) x.shortValue(); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(Byte.class)) { T candidate = (T) 
(Object) x.byteValue(); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(BigInteger.class)) { T candidate = 
(T) asBigIntegerForced(x); if (check!=null && check.test(candidate)) return 
candidate; }
+        if (desiredType.isAssignableFrom(BigDecimal.class)) { T candidate = 
(T) asBigDecimal(x); if (check!=null && check.test(candidate)) return 
candidate; }
+        return null;
+    }
+
+    public boolean withinTolerance(Number a, Number b) {
+        return withinTolerance(a, b, tolerance);
+    }
+    public static boolean withinTolerance(Number a, Number b, Number 
tolerance) {
+        return 
asBigDecimal(a).subtract(asBigDecimal(b)).abs().compareTo(asBigDecimal(tolerance))
 <= 0;
+    }
+
+    // from https://www.w3schools.com/java/java_type_casting.asp
+//    In Java, there are two types of casting:
+//
+//    Widening Casting (automatically) - converting a smaller type to a larger 
type size
+//    byte -> short -> char -> int -> long -> float -> double
+//
+//    Narrowing Casting (manually) - converting a larger type to a smaller 
size type
+//    double -> float -> long -> int -> char -> short -> byte
+
+    protected T attemptCast(Number candidate) {
+        Optional<T> result = asTypeWithinTolerance(candidate, desiredType, 
tolerance);
+        if (result.isPresent()) return result.get();
+        return handlerForUncastableType.apply(candidate);
+    }
+
+    protected T attemptUnary(Function<Long,Long> intFn, 
Function<Double,Double> doubleFn, Function<BigInteger,BigInteger> bigIntegerFn, 
Function<BigDecimal,BigDecimal> bigDecimalFn) {
+        if (isPrimitiveWholeNumberType(number)) return 
attemptCast(intFn.apply(number.longValue()));
+        if (isPrimitiveNumberType(number)) return 
attemptCast(doubleFn.apply(number.doubleValue()));
+        if (number instanceof BigInteger) return 
attemptCast(bigIntegerFn.apply((BigInteger)number));
+        if (number instanceof BigDecimal) return 
attemptCast(bigDecimalFn.apply((BigDecimal)number));
+        return attemptCast(bigDecimalFn.apply(asBigDecimal()));
+    }
+
+    protected T attemptBinary(T rhs, @Nullable BiFunction<Long,Long,Long> 
intFn, BiFunction<Double,Double,Double> doubleFn, @Nullable 
BiFunction<BigInteger,BigInteger,BigInteger> bigIntegerFn, 
BiFunction<BigDecimal,BigDecimal,BigDecimal> bigDecimalFn) {
+        if (isPrimitiveWholeNumberType(number) && 
isPrimitiveWholeNumberType(rhs) && intFn!=null) return 
attemptCast(intFn.apply(number.longValue(), rhs.longValue()));
+        if (isPrimitiveNumberType(number) && isPrimitiveNumberType(rhs)) 
return attemptCast(doubleFn.apply(number.doubleValue(), rhs.doubleValue()));
+        if (number instanceof BigInteger && bigIntegerFn!=null) {
+            BigInteger rhsI = asBigIntegerWithinTolerance(rhs).orElse(null);
+            if (rhsI!=null) return attemptCast(bigIntegerFn.apply((BigInteger) 
number, rhsI));
+        }
+        if (number instanceof BigDecimal) {
+            return attemptCast(bigDecimalFn.apply((BigDecimal) number, 
asBigDecimal(rhs)));
+        }
+        return attemptCast(bigDecimalFn.apply(asBigDecimal(), 
asBigDecimal(rhs)));
+    }
+    protected T attemptBinaryWithDecimalPrecision(T rhs, 
BiFunction<Double,Double,Double> doubleFn, 
BiFunction<BigDecimal,BigDecimal,BigDecimal> bigDecimalFn) {
+        return attemptBinary(rhs, null, doubleFn, null, bigDecimalFn);
+    }
+
+    public static <T extends Number> T pairwise(BiFunction<NumberMath<T>,T,T> 
fn, T ...rhsAll) {
+        T result = rhsAll[0];
+        for (T rhs: rhsAll) result = fn.apply(new NumberMath<T>(result),rhs);
+        return result;
+    }
+
+    public T abs() { return attemptUnary(x -> x<0 ? -x : x, x -> x<0 ? -x : x, 
BigInteger::abs, BigDecimal::abs); }
+    public T negate() { return attemptUnary(x -> -x, x -> -x, 
BigInteger::negate, BigDecimal::negate); }
+
+    public T add(T rhs) { return attemptBinary(rhs, (x,y) -> x+y, (x,y) -> 
x+y, BigInteger::add, BigDecimal::add); }
+    public T subtract(T rhs) { return attemptBinary(rhs, (x,y) -> x-y, (x,y) 
-> x-y, BigInteger::subtract, BigDecimal::subtract); }
+    public T multiply(T rhs) { return attemptBinary(rhs, (x,y) -> x*y, (x,y) 
-> x*y, BigInteger::multiply, BigDecimal::multiply); }
+    public T divide(T rhs) { return attemptBinaryWithDecimalPrecision(rhs, 
(x,y) -> x/y, BigDecimal::divide); }
+
+    public T max(T rhs) { return attemptBinary(rhs, (x,y) -> x>y ? x : y, 
(x,y) -> x>y ? x : y, BigInteger::max, BigDecimal::max); }
+    public T min(T rhs) { return attemptBinary(rhs, (x,y) -> x<y ? x : y, 
(x,y) -> x<y ? x : y, BigInteger::min, BigDecimal::min); }
+
+}
diff --git 
a/utils/common/src/test/java/org/apache/brooklyn/util/math/NumberMathTest.java 
b/utils/common/src/test/java/org/apache/brooklyn/util/math/NumberMathTest.java
new file mode 100644
index 0000000000..8bd5f72e04
--- /dev/null
+++ 
b/utils/common/src/test/java/org/apache/brooklyn/util/math/NumberMathTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.brooklyn.util.math;
+
+import org.apache.brooklyn.test.Asserts;
+import org.testng.annotations.Test;
+
+public class NumberMathTest {
+
+    @Test
+    public void testVarious() {
+        Asserts.assertEquals((int) new NumberMath<>(1).add(2), 3);
+        Asserts.assertEquals((Object) new NumberMath<>(1).add(2), 3);
+
+        Asserts.assertEquals(new NumberMath<>((Number)1).add(2.0d), 3);
+        Asserts.assertFailsWith(() -> new NumberMath<>((Number)1).add(2.1d), e 
-> Asserts.expectedFailureContains(e, "Cannot cast 3.1 to class 
java.lang.Integer"));
+        Asserts.assertFailsWith(() -> new NumberMath<>((Number)1).add(2.1d), e 
-> Asserts.expectedFailureContains(e, "Cannot cast 3.1 to class 
java.lang.Integer"));
+        Asserts.assertEquals(new NumberMath<>(1, Number.class).add(2.0d), 3);
+        Asserts.assertEquals(new NumberMath<>(1.1).add(2.0d), 3.1d);
+        Asserts.assertEquals(new NumberMath<>((Number)1.1).add(2.1d), 3.2d);
+        Asserts.assertThat(new NumberMath<Number>((byte)(-10)).add(4), x -> { 
Asserts.assertInstanceOf(x, Byte.class); Asserts.assertEquals(x, (byte) -6); 
return true; });
+
+        // division must be exact
+//        Asserts.assertEquals((Object) new NumberMath<>(3).divide(2), 1);
+        Asserts.assertFailsWith(() -> new NumberMath<>(3).divide(2), e -> 
Asserts.expectedFailureContains(e, "Cannot cast 1.5 to class 
java.lang.Integer"));
+
+        // if division might be double then cast to Number
+        Asserts.assertEquals(new NumberMath(3, Number.class).divide(2), 1.5d);
+    }
+
+}

Reply via email to