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);
+ }
+
+}