Merge pull request #1412 from jcabrerizo/security-provider-unauthorized-helper
Allow to forward some to the headers when a SecurityProviderDeniedAut…
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/store/WorkflowStateActiveInMemory.java b/core/src/main/java/org/apache/brooklyn/core/workflow/store/WorkflowStateActiveInMemory.java
index af6ef21..762b26e 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/store/WorkflowStateActiveInMemory.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/store/WorkflowStateActiveInMemory.java
@@ -18,6 +18,9 @@
*/
package org.apache.brooklyn.core.workflow.store;
+import java.util.Map;
+import java.util.Set;
+
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.config.ConfigKey;
@@ -25,11 +28,10 @@
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Map;
-
public class WorkflowStateActiveInMemory {
private static final Logger log = LoggerFactory.getLogger(WorkflowStateActiveInMemory.class);
@@ -64,14 +66,20 @@
public void expireAbsentEntities() {
lastInMemClear = System.currentTimeMillis();
- MutableMap.copyOf(data).forEach( (entityId, mapByWorkflowId) -> {
- if (mgmt.getEntityManager().getEntity(entityId)==null) data.remove(entityId);
+ Set<String> copy;
+ synchronized (data) {
+ copy = MutableSet.copyOf(data.keySet());
+ }
+ copy.forEach(entityId -> {
+ if (mgmt.getEntityManager().getEntity(entityId) == null) {
+ data.remove(entityId);
+ }
});
}
public void checkpoint(WorkflowExecutionContext context) {
// keep active workflows in memory, even if disabled
- Map<String, WorkflowExecutionContext> entityActiveWorkflows = data.get(context.getEntity().getId());
+ Map<String, WorkflowExecutionContext> entityActiveWorkflows = getSynchronizedForWorkflowId(context.getEntity().getId());
if (context.getStatus().expirable) {
if (entityActiveWorkflows!=null) entityActiveWorkflows.remove(context.getWorkflowId());
} else {
@@ -94,19 +102,25 @@
}
public Map<String,WorkflowExecutionContext> getWorkflows(Entity entity) {
- return MutableMap.copyOf(data.get(entity.getId()));
+ return getSynchronizedForWorkflowId(entity.getId());
}
boolean deleteWorkflow(WorkflowExecutionContext context) {
- Map<String, WorkflowExecutionContext> entityActiveWorkflows = data.get(context.getEntity().getId());
+ Map<String, WorkflowExecutionContext> entityActiveWorkflows = getSynchronizedForWorkflowId(context.getEntity().getId());
if (entityActiveWorkflows!=null) {
return entityActiveWorkflows.remove(context.getWorkflowId()) != null;
}
return false;
}
+ private Map<String, WorkflowExecutionContext> getSynchronizedForWorkflowId(String entityId) {
+ synchronized (data) {
+ return data.get(entityId);
+ }
+ }
+
public WorkflowExecutionContext getFromTag(BrooklynTaskTags.WorkflowTaskTag tag) {
- Map<String, WorkflowExecutionContext> activeForEntity = data.get(tag.getEntityId());
+ Map<String, WorkflowExecutionContext> activeForEntity = getSynchronizedForWorkflowId(tag.getEntityId());
if (activeForEntity!=null) {
return activeForEntity.get(tag.getWorkflowId());
}
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 90900fa..69916a7 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.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.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 @@
@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 @@
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()) {
- 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;
- } 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;
- }
- } else {
- throw new IllegalArgumentException("Cannot cast json element to type "+expected);
}
+
+ // now complex types
+ if (JsonElement.class.isAssignableFrom(expected)) {
+ return (T) input;
+ }
+
+ if (Iterable.class.isAssignableFrom(expected) || expected.isArray()) {
+ JsonArray array = input.getAsJsonArray();
+ 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 {
+ 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;
+ }
+ }
+
+ 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 0000000..7afdb29
--- /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 d7b19cb..b643391 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 @@
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/core/mgmt/internal/EntityExecutionManagerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/internal/EntityExecutionManagerTest.java
index 1073578..cb6cbd8 100644
--- a/core/src/test/java/org/apache/brooklyn/core/mgmt/internal/EntityExecutionManagerTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/internal/EntityExecutionManagerTest.java
@@ -353,6 +353,7 @@
return e;
}
+ // non-det failure, see below
public void testUnmanagedEntityCanBeGcedEvenIfPreviouslyTagged() throws Exception {
TestEntity e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
String eId = e.getId();
@@ -376,6 +377,7 @@
}
if ((tag instanceof WrappedEntity) && ((WrappedEntity) tag).unwrap().getId().equals(eId)
&& ((WrappedItem<?>) tag).getWrappingType().equals(BrooklynTaskTags.CONTEXT_ENTITY)) {
+ // non-deterministic failure observed here; only once so far, so might simply be GC being ignored
fail("tags contains unmanaged entity (wrapped) " + tag + "; tasks: " + app.getManagementContext().getExecutionManager().getTasksWithTag(tag));
}
}
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 638e10d..8e7b3f2 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 @@
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 b3c38d2..5710b32 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.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 @@
}
- 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 static class IntHolder {
public int val;
- IntHolder(int val) {
+ public IntHolder(int val) {
this.val = val;
}
@Override
@@ -329,4 +331,30 @@
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 d5f3e19..23dbdcb 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.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 @@
.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 0000000..023506f
--- /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 0000000..8bd5f72
--- /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);
+ }
+
+}