From 1f973df594cd359d1ad04c52c4f003d18191b339 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Fri, 24 Jun 2016 10:58:38 +0100 Subject: [PATCH 1/7] Maybe gives more control over the exceptions it throws You can do `orThrowingUnwrapped()` to suppress the add'l caller trace, and you can change more easily change the type of the exception it will throw. --- .../brooklyn/util/exceptions/Exceptions.java | 11 ++++ .../util/guava/AnyExceptionSupplier.java | 66 +++++++++++++++++++ .../guava/IllegalStateExceptionSupplier.java | 20 ++---- .../org/apache/brooklyn/util/guava/Maybe.java | 41 ++++++++++++ 4 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/Exceptions.java b/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/Exceptions.java index 13dcafb6a2..2997f4937b 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/Exceptions.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/exceptions/Exceptions.java @@ -39,6 +39,7 @@ import com.google.common.annotations.Beta; import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -367,6 +368,16 @@ else if (t2.getCause()!=null) return result; } + public static class CollapseTextSupplier implements Supplier { + final Throwable cause; + public CollapseTextSupplier(Throwable cause) { this.cause = cause; } + @Override + public String get() { + return Exceptions.collapseText(cause); + } + public Throwable getOriginal() { return cause; } + } + public static RuntimeException propagate(Collection exceptions) { throw propagate(create(exceptions)); } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java b/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java new file mode 100644 index 0000000000..7111c39fe9 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java @@ -0,0 +1,66 @@ +/* + * 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.guava; + +import org.apache.brooklyn.util.exceptions.Exceptions.CollapseTextSupplier; +import org.apache.brooklyn.util.javalang.Reflections; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; + +public class AnyExceptionSupplier implements Supplier { + + protected final Class type; + protected final Supplier message; + protected final Throwable cause; + + public AnyExceptionSupplier(Class type) { this(type, (Supplier)null, null); } + public AnyExceptionSupplier(Class type, String message) { this(type, message, null); } + public AnyExceptionSupplier(Class type, Throwable cause) { this(type, new CollapseTextSupplier(cause), cause); } + public AnyExceptionSupplier(Class type, String message, Throwable cause) { this(type, Suppliers.ofInstance(message), cause); } + public AnyExceptionSupplier(Class type, Supplier message, Throwable cause) { + this.type = type; + this.message = message; + this.cause = cause; + } + + @Override + public T get() { + String msg = message==null ? null : message.get(); + Maybe result = Maybe.absent(); + if (result.isAbsent() && msg==null && cause==null) result = Reflections.invokeConstructorWithArgs(type); + if (result.isAbsent() && cause==null) result = Reflections.invokeConstructorWithArgs(type, msg); + if (result.isAbsent() && msg==null) result = Reflections.invokeConstructorWithArgs(type, cause); + if (result.isAbsent()) result = Reflections.invokeConstructorWithArgs(type, msg, cause); + if (result.isAbsent()) { + throw new IllegalStateException("Cannot create desired "+type+" (missing constructor)", + new IllegalStateException(message==null ? null : message.get(), cause)); + } + return result.get(); + } + + public Throwable getCause() { + return cause; + } + + public Supplier getMessageSupplier() { + return message; + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/guava/IllegalStateExceptionSupplier.java b/utils/common/src/main/java/org/apache/brooklyn/util/guava/IllegalStateExceptionSupplier.java index a79a8c9b3f..9a36930530 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/guava/IllegalStateExceptionSupplier.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/guava/IllegalStateExceptionSupplier.java @@ -18,35 +18,23 @@ */ package org.apache.brooklyn.util.guava; -import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.exceptions.Exceptions.CollapseTextSupplier; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -public class IllegalStateExceptionSupplier implements Supplier { +public class IllegalStateExceptionSupplier extends AnyExceptionSupplier { public static final IllegalStateExceptionSupplier EMPTY_EXCEPTION = new IllegalStateExceptionSupplier(); - protected final Supplier message; - protected final Throwable cause; - public IllegalStateExceptionSupplier() { this((Supplier)null, null); } public IllegalStateExceptionSupplier(String message) { this(message, null); } public IllegalStateExceptionSupplier(Throwable cause) { this(new CollapseTextSupplier(cause), cause); } public IllegalStateExceptionSupplier(String message, Throwable cause) { this(Suppliers.ofInstance(message), cause); } - public IllegalStateExceptionSupplier(Supplier message, Throwable cause) { - this.message = message; - this.cause = cause; + public IllegalStateExceptionSupplier(Supplier message, Throwable cause) { + super(IllegalStateException.class, message, cause); } - private static class CollapseTextSupplier implements Supplier { - final Throwable cause; - CollapseTextSupplier(Throwable cause) { this.cause = cause; } - @Override - public String get() { - return Exceptions.collapseText(cause); - } - } @Override public RuntimeException get() { return new IllegalStateException(message==null ? null : message.get(), cause); diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java b/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java index 6b3df49d18..f64d71238b 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java @@ -202,6 +202,23 @@ public T orNull() { if (isPresent()) return get(); return null; } + + /** As {@link #get()} but if the Maybe wraps an exception + * (and where {@link #get()} throws a {@link RuntimeException} indicating the caller, + * caused by the exception in original processing) + * this throws the original unwrapped exception + * (masking the caller's location) + *

+ * As this masks the caller's exception, use with care, typically in a location + * near the original execution, otherwise it can get confusing. + * For instance someCallReturningMaybe().orThrowUnwrapped() is a nice idiom, + * but someMaybeVarReturnedEarlier.orThrowUnwrapped() is usually a bad idea. + *

+ * The benefit of this is simpler stack traces and preservation of original exception type. + */ + public T orThrowUnwrapped() { + return get(); + } public Set asSet() { if (isPresent()) return ImmutableSet.of(get()); @@ -272,9 +289,33 @@ public boolean isNull() { public T get() { throw getException(); } + public T orThrowUnwrapped() { + throw getException(); + } public RuntimeException getException() { return exception.get(); } + public Supplier getExceptionSupplier() { + return exception; + } + public static Maybe changeExceptionSupplier(Maybe original, final Class type) { + return changeExceptionSupplier(original, new Function,Supplier>() { + @SuppressWarnings("unchecked") + @Override + public Supplier apply(AnyExceptionSupplier input) { + if (type.isInstance(input)) return (Supplier) input; + return new AnyExceptionSupplier(type, input.getMessageSupplier(), input.getCause()); + } + }); + } + public static Maybe changeExceptionSupplier(Maybe original, Function,Supplier> transform) { + if (original.isPresent()) return original; + + final Supplier supplier = ((Maybe.Absent)original).getExceptionSupplier(); + if (!(supplier instanceof AnyExceptionSupplier)) return original; + + return Maybe.absent(transform.apply((AnyExceptionSupplier)supplier)); + } } public static class AbsentNull extends Absent { From 785342a4aebe0af47fac1e44205f85c28d320f79 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Fri, 24 Jun 2016 22:48:10 +0100 Subject: [PATCH 2/7] Maybe offers more clarity around whether it creates an exception on Absent and has a toOptional, and tests --- .../org/apache/brooklyn/util/guava/Maybe.java | 47 ++++- .../apache/brooklyn/util/guava/MaybeTest.java | 178 ++++++++++++++++++ 2 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 utils/common/src/test/java/org/apache/brooklyn/util/guava/MaybeTest.java diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java b/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java index f64d71238b..73c9761a23 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/guava/Maybe.java @@ -45,29 +45,54 @@ public abstract class Maybe implements Serializable, Supplier { private static final long serialVersionUID = -6372099069863179019L; + /** Returns an absent indicator. No message is available and access does not include any reference to this creation. + * Therefore it is fast and simple, but hard to work with if someone might {@link #get()} it and want a useful exception. + * See also {@link #absentNoTrace(String)} to include a message with very low overhead, + * or {@link #absentWithTrace(String)} or {@link #absent(Throwable)} for more control over the exception thrown. + */ public static Maybe absent() { - return new Absent(); + return new Maybe.Absent(); } - /** Creates an absent whose get throws an {@link IllegalStateException} with the indicated message. - * Both stack traces (the cause and the callers) are provided, which can be quite handy. */ + /** Convenience for {@link #absentWithTrace(String)}. */ public static Maybe absent(final String message) { + return absent(new IllegalStateException(message)); + } + + /** Creates an absent whose {@link #get()} throws an {@link IllegalStateException} with the indicated message. + * Both stack traces (the cause and the callers) are provided, which can be quite handy, + * but comparatively expensive as the cause stack trace has to generated when this method is invoked, + * even if it is never accessed. See also {@link #absentNoTrace(String)} and {@link #absent(Throwable)}. */ + public static Maybe absentWithTrace(final String message) { + return absent(new IllegalStateException(message)); + } + + /** Creates an absent whose get throws an {@link IllegalStateException} with the indicated message, + * but not a stack trace for the calling location. As stack traces can be comparatively expensive + * this is useful for efficiency, but it can make debugging harder as the origin of the absence is not kept, + * in contrast to {@link #absentWithTrace(String)} and {@link #absent(Throwable)}. */ + public static Maybe absentNoTrace(final String message) { return absent(new IllegalStateExceptionSupplier(message)); } - /** Creates an absent whose get throws an {@link IllegalStateException} with the indicated cause. - * Both stack traces (the cause and the callers) are provided, which can be quite handy. */ + /** As {@link #absentWithTrace(String)} but using the provided exception instead of this location + * as the cause, and a string based on this cause as the message on the {@link IllegalStateException} + * thrown if a user does a {@link #get()}. + * Useful if an {@link Exception} has already been generated (and no overhead) + * or if you want to supply a specific cause as in absent(new MyException(...)) + * (but there is the Exception creation overhead there). */ public static Maybe absent(final Throwable cause) { return absent(new IllegalStateExceptionSupplier(cause)); } - /** Creates an absent whose get throws an {@link IllegalStateException} with the indicated message and underlying cause. - * Both stack traces (the cause and the callers) are provided, which can be quite handy. */ + /** As {@link #absent(Throwable)} but using the given message as the message on the {@link IllegalStateException} + * thrown if a user does a {@link #get()}. */ public static Maybe absent(final String message, final Throwable cause) { return absent(new IllegalStateExceptionSupplier(message, cause)); } - /** Creates an absent whose get throws an {@link RuntimeException} generated on demand from the given supplier */ + /** Creates an absent whose {@link #get()} throws a {@link RuntimeException} + * generated on demand from the given supplier */ public static Maybe absent(final Supplier exceptionSupplier) { return new Absent(Preconditions.checkNotNull(exceptionSupplier)); } @@ -108,6 +133,12 @@ public static Maybe ofDisallowingNull(@Nullable T value) { public static Maybe of(@Nullable T value) { return ofAllowingNull(value); } + + /** Converts the given {@link Maybe} to {@link Optional}, failing if this {@link Maybe} contains null. */ + public Optional toOptional() { + if (isPresent()) return Optional.of(get()); + return Optional.absent(); + } /** Creates a new Maybe object using {@link #ofDisallowingNull(Object)} semantics. * It is recommended to use that method for clarity. diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/guava/MaybeTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/guava/MaybeTest.java new file mode 100644 index 0000000000..58703037f4 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/guava/MaybeTest.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.guava; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.exceptions.UserFacingException; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Optional; + +public class MaybeTest { + + @Test + public void testMaybeGet() { + Maybe m = Maybe.of("yes"); + Assert.assertTrue(m.isPresent()); + Assert.assertEquals(m.get(), "yes"); + } + + @Test + public void testMaybeToOptional() { + Maybe m = Maybe.of("yes"); + Optional o = m.toOptional(); + Assert.assertTrue(o.isPresent()); + Assert.assertEquals(o.get(), "yes"); + } + + @Test + public void testMaybeGetNull() { + Maybe m = Maybe.ofAllowingNull(null); + Assert.assertTrue(m.isPresent()); + Assert.assertEquals(m.get(), null); + } + + @Test + public void testMaybeDefaultAllowsNull() { + // but note you have to cast it if you try to do it explicitly + // (of course normally it's a variable...) + Maybe m = Maybe.of((Object)null); + Assert.assertTrue(m.isPresent()); + Assert.assertEquals(m.get(), null); + } + + @Test + public void testMaybeDisallowingNull() { + Maybe m = Maybe.ofDisallowingNull(null); + try { + m.get(); + Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "null"); + } + } + + @Test + public void testMaybeToOptionalFailsOnNull() { + Maybe m = Maybe.ofAllowingNull(null); + try { + m.toOptional(); + Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureOfType(e, NullPointerException.class); + } + } + + @Test + public void testMaybeAbsent() { + Maybe m = Maybe.absent("nope"); + Assert.assertFalse(m.isPresent()); + assertGetFailsContaining(m, "nope"); + } + + protected Exception assertGetFailsContaining(Maybe m, String phrase) { + try { + getInExplicitMethod(m); + throw Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureContains(e, phrase); + return e; + } + } + + @Test + public void testMaybeAbsentToOptional() { + Maybe m = Maybe.absent("nope"); + Optional o = m.toOptional(); + Assert.assertFalse(o.isPresent()); + try { + o.get(); + throw Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "absent"); + } + } + + // --- now check traces ---- + + /** an explicit method we can search for in the trace */ + protected T getInExplicitMethod(Maybe m) { + return m.get(); + } + + protected boolean containsClassAndMethod(StackTraceElement[] stackTrace, String className, String methodName) { + boolean methodFound = (methodName==null); + boolean classFound = (className==null); + for (StackTraceElement element: stackTrace) { + if (className!=null && className.equals(element.getClassName())) classFound = true; + if (methodName!=null && methodName.equals(element.getMethodName())) methodFound = true; + } + return methodFound && classFound; + } + protected boolean containsGetExplicitMethod(Throwable e) { + return containsClassAndMethod(e.getStackTrace(), MaybeTest.class.getName(), "getInExplicitMethod"); + } + protected boolean containsMaybeMethod(Throwable e, String methodName) { + return containsClassAndMethod(e.getStackTrace(), Maybe.class.getName(), methodName); + } + + @Test + public void testMaybeAbsentMessageIncludesSource() { + Maybe m = Maybe.absent("nope"); + Assert.assertFalse(m.isPresent()); + Exception e = assertGetFailsContaining(m, "nope"); + + e.printStackTrace(); + Assert.assertTrue(containsGetExplicitMethod(e), "Outer trace should be in explicit method"); + Assert.assertFalse(containsMaybeMethod(e, "absent"), "Outer trace should not be from 'absent'"); + + Assert.assertFalse(containsGetExplicitMethod(e.getCause()), "Inner trace should not be in explicit method"); + Assert.assertTrue(containsMaybeMethod(e.getCause(), "absent"), "Inner trace should be from 'absent'"); + } + + @Test + public void testMaybeAbsentMessageNoTraceDoesNotIncludeSource() { + Maybe m = Maybe.absentNoTrace("nope"); + Assert.assertFalse(m.isPresent()); + Exception e = assertGetFailsContaining(m, "nope"); + Assert.assertTrue(containsGetExplicitMethod(e), "Outer trace should be in explicit method"); + Assert.assertFalse(containsMaybeMethod(e, "absentNoTrace"), "Outer trace should not be from 'absentNoTrace'"); + Assert.assertNull(e.getCause()); + } + + protected Exception newRE(String message) { return new UserFacingException(message); } + + @Test + public void testMaybeAbsentThrowableIncludesGivenSourceOnly() { + Maybe m = Maybe.absent(newRE("nope")); + Assert.assertFalse(m.isPresent()); + Exception e = assertGetFailsContaining(m, "nope"); + Assert.assertTrue(e instanceof IllegalStateException, "Outer should be ISE, not "+e); + Assert.assertTrue(containsGetExplicitMethod(e), "Outer trace should be in explicit method"); + Assert.assertFalse(containsMaybeMethod(e, "absent"), "Outer trace should not be from 'absent'"); + Assert.assertFalse(containsMaybeMethod(e, "newRE"), "Outer trace should not be 'newRE'"); + + Assert.assertTrue(e.getCause() instanceof UserFacingException, "Inner should be UFE, not "+e.getCause()); + Assert.assertFalse(containsGetExplicitMethod(e.getCause()), "Inner trace should not be in explicit method"); + Assert.assertFalse(containsMaybeMethod(e.getCause(), "absent"), "Inner trace should not be from 'absent'"); + Assert.assertTrue(containsClassAndMethod(e.getCause().getStackTrace(), MaybeTest.class.getName(), "newRE"), "Inner trace SHOULD be from 'newRE'"); + } + +} From 864400306d0433e26a51e96639de2d2b08b9bc4f Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Fri, 24 Jun 2016 11:00:14 +0100 Subject: [PATCH 3/7] Jsonya allows primitives and lists at the root now bringing it in line with json which has always allowed lists and now allows primitives, as root objects --- .../brooklyn/util/collections/Jsonya.java | 68 +++++++-- .../brooklyn/util/collections/JsonyaTest.java | 130 +++++++++++++++++- 2 files changed, 179 insertions(+), 19 deletions(-) diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java index ef7f4510b1..6a13dbf3d2 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java @@ -66,7 +66,7 @@ public static > Navigator of(Navigator navigator) { /** creates a {@link Navigator} backed by a newly created map; * the map can be accessed by {@link Navigator#getMap()} */ public static Navigator> newInstance() { - return new Navigator>(new MutableMap(), MutableMap.class); + return new Navigator>(MutableMap.class); } /** convenience for {@link Navigator#at(Object, Object...)} on a {@link #newInstance()} */ public static Navigator> at(Object ...pathSegments) { @@ -113,7 +113,7 @@ public static boolean isJsonPrimitiveCompatible(Object x) { @SuppressWarnings({"rawtypes","unchecked"}) public static class Navigator> { - protected final Object root; + protected Object root; protected final Class mapType; protected Object focus; protected Stack focusStack = new Stack(); @@ -125,6 +125,19 @@ public Navigator(Object backingStore, Class mapType) { this.focus = backingStore; this.mapType = mapType; } + + public Navigator(Class mapType) { + this.root = null; + this.focus = null; + this.mapType = mapType; + this.creationInPreviousFocus = new Function() { + @Override + public Void apply(Object o) { + root = o; + return null; + } + }; + } // -------------- access and configuration @@ -227,16 +240,21 @@ public Navigator pop() { return this; } - /** returns the navigator moved to focus at the indicated key sequence in the given map */ + /** returns the navigator moved to focus at the indicated key sequence in the given map, creating the path needed */ public Navigator at(Object pathSegment, Object ...furtherPathSegments) { - down(pathSegment); + down(pathSegment, false); return atArray(furtherPathSegments); } public Navigator atArray(Object[] furtherPathSegments) { for (Object p: furtherPathSegments) - down(p); + down(p, false); return this; } + /** returns the navigator moved to focus at the indicated key sequence in the given map, failing if not available */ + public Navigator atExisting(Object pathSegment, Object ...furtherPathSegments) { + down(pathSegment, true); + return atArray(furtherPathSegments); + } /** ensures the given focus is a map, creating if needed (and creating inside the list if it is in a list) */ public Navigator map() { @@ -297,17 +315,17 @@ protected Map newMap() { } /** utility for {@link #at(Object, Object...)}, taking one argument at a time */ - protected Navigator down(final Object pathSegment) { + protected Navigator down(final Object pathSegment, boolean requireExisting) { if (focus instanceof List) { - return downList(pathSegment); + return downList(pathSegment, requireExisting); } if ((focus instanceof Map) || focus==null) { - return downMap(pathSegment); + return downMap(pathSegment, requireExisting); } throw new IllegalStateException("focus here is "+focus+"; cannot descend to '"+pathSegment+"'"); } - protected Navigator downMap(Object pathSegmentO) { + protected Navigator downMap(Object pathSegmentO, boolean requireExisting) { final Object pathSegment = translateKey(pathSegmentO); final Map givenParentMap = (Map)focus; if (givenParentMap!=null) { @@ -315,6 +333,9 @@ protected Navigator downMap(Object pathSegmentO) { focus = givenParentMap.get(pathSegment); } if (focus==null) { + if (requireExisting) { + throw new IllegalStateException("No key '"+pathSegmentO+"' found to descend"); + } final Function previousCreation = creationInPreviousFocus; creationInPreviousFocus = new Function() { public Void apply(Object input) { @@ -332,7 +353,7 @@ public Void apply(Object input) { return this; } - protected Navigator downList(final Object pathSegment) { + protected Navigator downList(final Object pathSegment, boolean requireExisting) { if (!(pathSegment instanceof Integer)) throw new IllegalStateException("focus here is a list ("+focus+"); cannot descend to '"+pathSegment+"'"); final List givenParentList = (List)focus; @@ -340,6 +361,10 @@ protected Navigator downList(final Object pathSegment) { creationInPreviousFocus = null; focus = givenParentList.get((Integer)pathSegment); if (focus==null) { + if (requireExisting) { + throw new IllegalStateException("No index '"+pathSegment+"' found to descend"); + } + // don't need to worry about creation here; we don't create list entries simply by navigating // TODO a nicer architecture would create a new object with focus for each traversal // in that case we could create, filling other positions with null; but is there a need? @@ -370,7 +395,9 @@ protected List newList() { } /** adds the given items to the focus, whether a list or a map, - * creating the focus as a map if it doesn't already exist. + * creating the focus if it doesn't already exist. + * if there is just one argument being added and the focus doesn't exist, that item is set as the focus. + * if there are more than one argument the focus is made as a map (and an even number of arguments is required). * to add items to a list which might not exist, precede by a call to {@link #list()}. *

* when adding items to a list, iterable and array arguments are flattened because @@ -387,7 +414,19 @@ protected List newList() { * auto-conversion to a list may be added in a future version * */ public Navigator add(Object o1, Object ...others) { - if (focus==null) map(); + if (focus==null) { + if (others.length>0) { + // default to map, but only if multiple args given + map(); + } else { + // if single arg and no focus, focus becomes the arg, and no need to add + focus = o1; + if (creationInPreviousFocus!=null) { + creationInPreviousFocus.apply(o1); + } + return this; + } + } addInternal(focus, focus, o1, others); return this; } @@ -404,12 +443,12 @@ protected void addInternal(Object initialFocus, Object currentFocus, Object o1, Map target = (Map)currentFocus; Map source; if (others.length==0) { - // add as a map if (o1==null) // ignore if null return ; - if (!(o1 instanceof Map)) + if (!(o1 instanceof Map)) { throw new IllegalStateException("cannot add: focus here is "+currentFocus+" (in "+initialFocus+"); expected a collection, or a map (with a map being added, not "+o1+")"); + } source = (Map)translate(o1); } else { // build a source map from the arguments as key-value pairs @@ -487,6 +526,7 @@ public static String render(Object focus) { for (Object entry: (Collection)focus) { if (!first) sb.append(","); else first = false; + sb.append( " " ); sb.append( render(entry) ); } sb.append(" ]"); diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/collections/JsonyaTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/collections/JsonyaTest.java index 9ce77d28c9..4bf94a6484 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/collections/JsonyaTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/collections/JsonyaTest.java @@ -22,9 +22,7 @@ import java.util.List; import java.util.Map; -import org.apache.brooklyn.util.collections.Jsonya; -import org.apache.brooklyn.util.collections.MutableMap; -import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.Jsonya.Navigator; import org.testng.Assert; import org.testng.annotations.Test; @@ -110,9 +108,14 @@ public void testJsonyaDeepSimple() { Assert.assertEquals( n.root().at("europe").getFocusMap().size(), 3 ); } - @Test(expectedExceptions=Exception.class) + @Test public void testJsonyaDeepSimpleFailure() { - Jsonya.of(europeMap()).at("euroope").add("spain"); + try { + Jsonya.of(europeMap()).atExisting("euroope"); + Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "euroope", "no", "found"); + } } @Test @@ -190,4 +193,121 @@ public void testJsonyaPushPop() { Assert.assertFalse(m.getFocusMap().containsKey("edinburgh")); } + @Test + public void testAddMapAddsReference() { + MutableMap map = MutableMap.of("a", 1, "b", 2); + Navigator> j = Jsonya.newInstance().add("root", map); + Assert.assertEquals( j.toString(), "{ \"root\": { \"a\": 1, \"b\": 2 } }"); + map.put("b", 1); + Assert.assertEquals( j.toString(), "{ \"root\": { \"a\": 1, \"b\": 1 } }"); + } + + @Test + public void testAddListAddsReference() { + MutableList list = MutableList.of("a", "b"); + Navigator> j = Jsonya.newInstance().add("root", list); + Assert.assertEquals( j.toString(), "{ \"root\": [ \"a\", \"b\" ] }"); + list.append("c"); + Assert.assertEquals( j.toString(), "{ \"root\": [ \"a\", \"b\", \"c\" ] }"); + } + + @Test + public void testAddListToExistingAddsCopy() { + MutableList list = MutableList.of("a", "b"); + Navigator> j = Jsonya.newInstance().at("root").list().add(list).root(); + Assert.assertEquals( j.toString(), "{ \"root\": [ \"a\", \"b\" ] }"); + list.append("c"); + Assert.assertEquals( j.toString(), "{ \"root\": [ \"a\", \"b\" ] }"); + } + + @Test + public void testAddMapToExistingAddsCopy() { + MutableMap map = MutableMap.of("a", 1, "b", 2); + Navigator> j = Jsonya.newInstance().at("root").map().add(map).root(); + Assert.assertEquals( j.toString(), "{ \"root\": { \"a\": 1, \"b\": 2 } }"); + map.put("b", 1); + Assert.assertEquals( j.toString(), "{ \"root\": { \"a\": 1, \"b\": 2 } }"); + } + + @Test + public void testAddMapToExistingRootAddsCopy() { + MutableMap map = MutableMap.of("a", 1, "b", 2); + Navigator> j = Jsonya.newInstance().map().add(map); + Assert.assertEquals( j.toString(), "{ \"a\": 1, \"b\": 2 }"); + map.put("b", 1); + Assert.assertEquals( j.toString(), "{ \"a\": 1, \"b\": 2 }"); + } + + @Test + public void testAddListToExistingRootAddsCopy() { + MutableList list = MutableList.of("a", "b"); + Navigator> j = Jsonya.newInstance().list().add(list); + Assert.assertEquals( j.toString(), "[ \"a\", \"b\" ]"); + list.append("c"); + Assert.assertEquals( j.toString(), "[ \"a\", \"b\" ]"); + } + + @Test + public void testAddMapAtRootAddsReference() { + MutableMap map = MutableMap.of("a", 1, "b", 2); + Navigator> j = Jsonya.newInstance().add(map); + Assert.assertEquals( j.toString(), "{ \"a\": 1, \"b\": 2 }"); + map.put("b", 1); + Assert.assertEquals( j.toString(), "{ \"a\": 1, \"b\": 1 }"); + } + + @Test + public void testAddListAtRootAddsReference() { + MutableList list = MutableList.of("a", "b"); + Navigator> j = Jsonya.newInstance().add(list); + Assert.assertEquals( j.toString(), "[ \"a\", \"b\" ]"); + list.append("c"); + Assert.assertEquals( j.toString(), "[ \"a\", \"b\", \"c\" ]"); + } + + @Test + public void testAddStringToList() { + Navigator> j = Jsonya.newInstance().at("root").list().add("a", "b").root(); + Assert.assertEquals( j.toString(), "{ \"root\": [ \"a\", \"b\" ] }"); + } + + @Test + public void testAddStringToListAtRoot() { + Navigator> j = Jsonya.newInstance().list().add("a", "b").root(); + Assert.assertEquals( j.toString(), "[ \"a\", \"b\" ]" ); + Assert.assertEquals( j.get(), MutableList.of("a", "b") ); + } + + @Test + public void testAddStringToRoot() { + Navigator> j = Jsonya.newInstance().add("a"); + Assert.assertEquals( j.toString(), "\"a\""); + Assert.assertEquals( j.get(), "a"); + } + + @Test + public void testAddStringsAtRootDefaultsToMap() { + Navigator> j = Jsonya.newInstance().add("a", 1); + Assert.assertEquals( j.toString(), "{ \"a\": 1 }"); + } + + @Test + public void testAddOddStringsAtRootIsError() { + try { + Jsonya.newInstance().add("a", 1, "b"); + Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "odd"); + } + } + + @Test + public void testAddStringAtKey() { + Navigator> j = Jsonya.newInstance().at("root").add("value").root(); + Assert.assertEquals( j.get(), MutableMap.of("root", "value")); + } + public void testAddStringAtKeySequence() { + Navigator> j = Jsonya.newInstance().at("1", "2").add("value"); + Assert.assertEquals( j.get(), MutableMap.of("1", MutableMap.of("2", "value"))); + } } From 23ab1c506bdda9a3ac79199146ea9750d18d9005 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Fri, 24 Jun 2016 11:13:18 +0100 Subject: [PATCH 4/7] tidy/enhancements to javalang utils a few more methods which were elsewhere or not present, better methods using Maybe, and callers updated to use them --- .../BrooklynYamlTypeInstantiator.java | 6 +- .../spi/dsl/methods/BrooklynDslCommon.java | 2 +- .../core/mgmt/entitlement/Entitlements.java | 6 +- .../BasicExternalConfigSupplierRegistry.java | 6 +- .../core/mgmt/internal/LocalUsageManager.java | 4 +- .../core/mgmt/rebind/RebindIteration.java | 3 +- .../core/objs/proxy/InternalFactory.java | 7 +- .../entity/group/DynamicClusterImpl.java | 4 +- .../location/jclouds/JcloudsLocation.java | 15 +- .../org/apache/brooklyn/test/Asserts.java | 8 +- .../util/guava/AnyExceptionSupplier.java | 8 +- .../apache/brooklyn/util/javalang/Boxing.java | 29 +++ .../apache/brooklyn/util/javalang/Enums.java | 4 +- .../util/javalang/FieldOrderings.java | 82 ++++++ .../util/javalang/JavaClassNames.java | 18 +- .../util/javalang/ReflectionPredicates.java | 71 ++++++ .../brooklyn/util/javalang/Reflections.java | 239 +++++++++++++----- .../brooklyn/util/javalang/Threads.java | 1 - .../brooklyn/util/javalang/BoxingTest.java | 5 + .../util/javalang/ReflectionsTest.java | 13 +- 20 files changed, 422 insertions(+), 109 deletions(-) create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/FieldOrderings.java create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynYamlTypeInstantiator.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynYamlTypeInstantiator.java index 01bfaa2c20..a8f8c5a9bd 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynYamlTypeInstantiator.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynYamlTypeInstantiator.java @@ -129,17 +129,17 @@ public Object newInstance() { public T newInstance(@Nullable Class supertype) { Class type = getType(supertype); Map cfg = getConfigMap(); - Optional result = Reflections.invokeConstructorWithArgs(type, cfg); + Maybe result = Reflections.invokeConstructorFromArgs(type, cfg); if (result.isPresent()) return result.get(); ConfigBag cfgBag = ConfigBag.newInstance(cfg); - result = Reflections.invokeConstructorWithArgs(type, cfgBag); + result = Reflections.invokeConstructorFromArgs(type, cfgBag); if (result.isPresent()) return result.get(); if (cfg.isEmpty()) { - result = Reflections.invokeConstructorWithArgs(type); + result = Reflections.invokeConstructorFromArgs(type); if (result.isPresent()) return result.get(); } diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java index 48ea03ffea..b88829b7e2 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java @@ -359,7 +359,7 @@ public static T create(Class type, Map fields, Map co try { bean = (T) TypeCoercions.coerce(fields, type); } catch (ClassCoercionException ex) { - bean = Reflections.invokeConstructorWithArgs(type).get(); + bean = Reflections.invokeConstructorFromArgs(type).get(); BeanUtils.populate(bean, fields); } if (bean instanceof Configurable && config.size() > 0) { diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java index 7199267359..df3f7a893e 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.List; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.brooklyn.api.entity.Entity; @@ -35,10 +34,10 @@ import org.apache.brooklyn.core.config.Sanitizer; import org.apache.brooklyn.core.internal.BrooklynProperties; import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; -import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.persist.DeserializingClassRenamesProvider; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; @@ -47,7 +46,6 @@ import com.google.common.annotations.Beta; import com.google.common.base.Joiner; import com.google.common.base.Objects; -import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; @@ -474,7 +472,7 @@ public static EntitlementManager load(@Nullable ManagementContext mgmt, Brooklyn private static Object instantiate(Class clazz, List constructorArgOptions) { try { for (Object[] constructorArgOption : constructorArgOptions) { - Optional result = Reflections.invokeConstructorWithArgs(clazz, constructorArgOption); + Maybe result = Reflections.invokeConstructorFromArgs(clazz, constructorArgOption); if (result.isPresent()) return result.get(); } } catch (Exception e) { diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/BasicExternalConfigSupplierRegistry.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/BasicExternalConfigSupplierRegistry.java index 6b261db25f..ac67f22aa2 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/BasicExternalConfigSupplierRegistry.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/BasicExternalConfigSupplierRegistry.java @@ -27,11 +27,11 @@ import org.apache.brooklyn.core.config.ConfigUtils; import org.apache.brooklyn.core.config.external.ExternalConfigSupplier; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; import com.google.common.collect.Maps; /** @@ -102,9 +102,9 @@ private void updateFromBrooklynProperties(ManagementContext mgmt) { Map config = ConfigUtils.filterForPrefixAndStrip(externalProviderProperties, key + "."); try { - Optional configSupplier = Reflections.invokeConstructorWithArgs(classloader, providerClassname, mgmt, name, config); + Maybe configSupplier = Reflections.invokeConstructorFromArgs(classloader, ExternalConfigSupplier.class, providerClassname, mgmt, name, config); if (!configSupplier.isPresent()) { - configSupplier = Reflections.invokeConstructorWithArgs(classloader, providerClassname, mgmt, name); + configSupplier = Reflections.invokeConstructorFromArgs(classloader, ExternalConfigSupplier.class, providerClassname, mgmt, name); } if (!configSupplier.isPresent()) { throw new IllegalStateException("No matching constructor found in "+providerClassname); diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/LocalUsageManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/LocalUsageManager.java index 7caf958ba9..50df5c84d5 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/LocalUsageManager.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/internal/LocalUsageManager.java @@ -50,6 +50,7 @@ import org.apache.brooklyn.core.mgmt.usage.UsageManager; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.time.Duration; import org.slf4j.Logger; @@ -57,7 +58,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; -import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @@ -142,7 +142,7 @@ private static class LocationMetadataImpl implements UsageListener.LocationMetad @Override public UsageListener apply(String input) { // TODO Want to use classLoader = mgmt.getCatalog().getRootClassLoader(); ClassLoader classLoader = LocalUsageManager.class.getClassLoader(); - Optional result = Reflections.invokeConstructorWithArgs(classLoader, input); + Maybe result = Reflections.invokeConstructorFromArgs(classLoader, input); if (result.isPresent()) { return (UsageListener) result.get(); } else { diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java index 5c77753211..6c0bd9dbf8 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java @@ -101,7 +101,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableMap; @@ -1118,7 +1117,7 @@ protected Feed newFeed(FeedMemento memento) { protected T invokeConstructor(Reflections reflections, Class clazz, Object[]... possibleArgs) { for (Object[] args : possibleArgs) { try { - Optional v = Reflections.invokeConstructorWithArgs(clazz, args, true); + Maybe v = Reflections.invokeConstructorFromArgs(clazz, args, true); if (v.isPresent()) { return v.get(); } diff --git a/core/src/main/java/org/apache/brooklyn/core/objs/proxy/InternalFactory.java b/core/src/main/java/org/apache/brooklyn/core/objs/proxy/InternalFactory.java index f5454fe8cb..0f7e79eac5 100644 --- a/core/src/main/java/org/apache/brooklyn/core/objs/proxy/InternalFactory.java +++ b/core/src/main/java/org/apache/brooklyn/core/objs/proxy/InternalFactory.java @@ -28,10 +28,9 @@ import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Reflections; -import com.google.common.base.Optional; - /** */ public class InternalFactory { @@ -142,9 +141,9 @@ protected T constructNewStyle(Class clazz) { protected T constructOldStyle(Class clazz, Map flags) throws InstantiationException, IllegalAccessException, InvocationTargetException { FactoryConstructionTracker.setConstructing(); - Optional v; + Maybe v; try { - v = Reflections.invokeConstructorWithArgs(clazz, new Object[] {MutableMap.copyOf(flags)}, true); + v = Reflections.invokeConstructorFromArgs(clazz, new Object[] {MutableMap.copyOf(flags)}, true); } finally { FactoryConstructionTracker.reset(); } diff --git a/core/src/main/java/org/apache/brooklyn/entity/group/DynamicClusterImpl.java b/core/src/main/java/org/apache/brooklyn/entity/group/DynamicClusterImpl.java index d4d5b1d6be..0271134171 100644 --- a/core/src/main/java/org/apache/brooklyn/entity/group/DynamicClusterImpl.java +++ b/core/src/main/java/org/apache/brooklyn/entity/group/DynamicClusterImpl.java @@ -115,7 +115,7 @@ public class DynamicClusterImpl extends AbstractGroupImpl implements DynamicClus @Override public NodePlacementStrategy apply(final String input) { ClassLoader classLoader = NodePlacementStrategy.class.getClassLoader(); - Optional strategy = Reflections.invokeConstructorWithArgs(classLoader, input); + Maybe strategy = Reflections.invokeConstructorFromArgs(classLoader, NodePlacementStrategy.class, input); if (strategy.isPresent()) { return strategy.get(); } else { @@ -127,7 +127,7 @@ public NodePlacementStrategy apply(final String input) { @Override public ZoneFailureDetector apply(final String input) { ClassLoader classLoader = ZoneFailureDetector.class.getClassLoader(); - Optional detector = Reflections.invokeConstructorWithArgs(classLoader, input); + Maybe detector = Reflections.invokeConstructorFromArgs(classLoader, ZoneFailureDetector.class, input); if (detector.isPresent()) { return detector.get(); } else { diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java index 51e9d6aec2..58895a832a 100644 --- a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java @@ -58,7 +58,6 @@ import org.apache.brooklyn.api.location.PortRange; import org.apache.brooklyn.api.mgmt.AccessController; import org.apache.brooklyn.api.mgmt.Task; -import org.apache.brooklyn.api.mgmt.TaskAdaptable; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.config.ConfigKey.HasConfigKey; import org.apache.brooklyn.core.BrooklynVersion; @@ -107,8 +106,6 @@ import org.apache.brooklyn.util.core.task.TaskInternal; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.core.task.ssh.SshTasks; -import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory; -import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; import org.apache.brooklyn.util.core.text.TemplateProcessor; import org.apache.brooklyn.util.exceptions.CompoundRuntimeException; import org.apache.brooklyn.util.exceptions.Exceptions; @@ -400,7 +397,7 @@ protected Semaphore getMachineCreationSemaphore() { protected CloudMachineNamer getCloudMachineNamer(ConfigBag config) { String namerClass = config.get(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS); if (Strings.isNonBlank(namerClass)) { - Optional cloudNamer = Reflections.invokeConstructorWithArgs(getManagementContext().getCatalogClassLoader(), namerClass); + Maybe cloudNamer = Reflections.invokeConstructorFromArgs(getManagementContext().getCatalogClassLoader(), CloudMachineNamer.class, namerClass); if (cloudNamer.isPresent()) { return cloudNamer.get(); } else { @@ -425,11 +422,11 @@ protected Collection getCustomizers(ConfigBag setup) if (customizer != null) result.add(customizer); if (customizers != null) result.addAll(customizers); if (Strings.isNonBlank(customizerType)) { - Optional customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType, setup); + Maybe customizerByType = Reflections.invokeConstructorFromArgs(catalogClassLoader, JcloudsLocationCustomizer.class, customizerType, setup); if (customizerByType.isPresent()) { result.add(customizerByType.get()); } else { - customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType); + customizerByType = Reflections.invokeConstructorFromArgs(catalogClassLoader, JcloudsLocationCustomizer.class, customizerType); if (customizerByType.isPresent()) { result.add(customizerByType.get()); } else { @@ -438,11 +435,11 @@ protected Collection getCustomizers(ConfigBag setup) } } if (Strings.isNonBlank(customizersSupplierType)) { - Optional>> supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType, setup); + Maybe>> supplier = Reflections.>>invokeConstructorFromArgsUntyped(catalogClassLoader, customizersSupplierType, setup); if (supplier.isPresent()) { result.addAll(supplier.get().get()); } else { - supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType); + supplier = Reflections.>>invokeConstructorFromArgsUntyped(catalogClassLoader, customizersSupplierType); if (supplier.isPresent()) { result.addAll(supplier.get().get()); } else { @@ -1566,7 +1563,7 @@ protected Function, Image> getImageChooser(ComputeServ Object rawVal = config.getStringKey(JcloudsLocationConfig.IMAGE_CHOOSER.getName()); if (rawVal instanceof String && Strings.isNonBlank((String)rawVal)) { // Configured with a string: it could be a class that we need to instantiate - Optional instance = Reflections.invokeConstructorWithArgs(getManagementContext().getCatalogClassLoader(), (String)rawVal); + Maybe instance = Reflections.invokeConstructorFromArgs(getManagementContext().getCatalogClassLoader(), (String)rawVal); if (!instance.isPresent()) { throw new IllegalStateException("Failed to create ImageChooser "+rawVal+" for location "+this); } else if (!(instance.get() instanceof Function)) { diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java index aa74d1b058..a544f0189b 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java +++ b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java @@ -1161,8 +1161,12 @@ public static class ShouldHaveFailedPreviouslyAssertionError extends AssertionEr /** Throws a {@link ShouldHaveFailedPreviouslyAssertionError} exception, * to more easily distinguish this failure from other fails. * In particular, use one of the expectedFailure methods - * in the surrounding catch block and this error will pass through it. */ - public static void shouldHaveFailedPreviously() { + * in the surrounding catch block and this error will pass through it. + *

+ * This method throws, never returning normally, but declares a return type + * so you can pretend to throw the result, + * for instance if your calling code otherwise warns about needing to return something. */ + public static RuntimeException shouldHaveFailedPreviously() { throw new ShouldHaveFailedPreviouslyAssertionError(); } /** As {@link #shouldHaveFailedPreviously()} but allowing detail, diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java b/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java index 7111c39fe9..a194ac0395 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/guava/AnyExceptionSupplier.java @@ -44,10 +44,10 @@ public AnyExceptionSupplier(Class type, Supplier message, T public T get() { String msg = message==null ? null : message.get(); Maybe result = Maybe.absent(); - if (result.isAbsent() && msg==null && cause==null) result = Reflections.invokeConstructorWithArgs(type); - if (result.isAbsent() && cause==null) result = Reflections.invokeConstructorWithArgs(type, msg); - if (result.isAbsent() && msg==null) result = Reflections.invokeConstructorWithArgs(type, cause); - if (result.isAbsent()) result = Reflections.invokeConstructorWithArgs(type, msg, cause); + if (result.isAbsent() && msg==null && cause==null) result = Reflections.invokeConstructorFromArgs(type); + if (result.isAbsent() && cause==null) result = Reflections.invokeConstructorFromArgs(type, msg); + if (result.isAbsent() && msg==null) result = Reflections.invokeConstructorFromArgs(type, cause); + if (result.isAbsent()) result = Reflections.invokeConstructorFromArgs(type, msg, cause); if (result.isAbsent()) { throw new IllegalStateException("Cannot create desired "+type+" (missing constructor)", new IllegalStateException(message==null ? null : message.get(), cause)); diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Boxing.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Boxing.java index 79a18308bb..87529ad37d 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Boxing.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Boxing.java @@ -23,7 +23,11 @@ import org.apache.brooklyn.util.guava.Maybe; import com.google.common.collect.ImmutableBiMap; +import com.google.common.primitives.Primitives; +/** Conveniences for working with primitives and their boxed (wrapper) types. + * NB: there is redundancy with {@link Primitives} + */ public class Boxing { public static boolean unboxSafely(Boolean ref, boolean valueIfNull) { @@ -56,6 +60,17 @@ public static Maybe> getPrimitiveType(String typeName) { } return Maybe.absent("Not a primitive: "+typeName); } + + /** returns name of primitive corresponding to the given (boxed or unboxed) type */ + public static Maybe getPrimitiveName(Class type) { + if (type!=null) { + if (PRIMITIVE_TO_BOXED.containsKey(type)) return Maybe.of(type.getName()); + if (PRIMITIVE_TO_BOXED.containsValue(type)) { + return Maybe.of(PRIMITIVE_TO_BOXED.inverse().get(type).getName()); + } + } + return Maybe.absent("Not a primitive or boxed primitive: "+type); + } public static Class boxedType(Class type) { if (PRIMITIVE_TO_BOXED.containsKey(type)) @@ -63,6 +78,20 @@ public static Class boxedType(Class type) { return type; } + public static boolean isPrimitiveOrBoxedObject(Object o) { + if (o==null) return false; + return isPrimitiveOrBoxedClass(o.getClass()); + } + public static boolean isPrimitiveOrBoxedClass(Class t) { + // TODO maybe better to switch to using Primitives, eg: + // return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type); + + if (t==null) return false; + if (t.isPrimitive()) return true; + if (PRIMITIVE_TO_BOXED.containsValue(t)) return true; + return false; + } + /** sets the given element in an array to the indicated value; * if the type is a primitive type, the appropriate primitive method is used *

diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Enums.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Enums.java index d3bfd833e3..a9305a83b6 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Enums.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Enums.java @@ -71,8 +71,10 @@ public static > Function fromStringFunction(Class return new EnumFromStringFunction(type); } + /** returns array of values in the given enum, or null if it is not an enum */ @SuppressWarnings("unchecked") - private static > T[] values(Class type) { + public static > T[] values(Class type) { + if (type==null || !type.isEnum()) return null; try { return (T[]) type.getMethod("values").invoke(null); } catch (Exception e) { diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/FieldOrderings.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/FieldOrderings.java new file mode 100644 index 0000000000..ceb33e895b --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/FieldOrderings.java @@ -0,0 +1,82 @@ +/* + * 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.javalang; + +import java.lang.reflect.Field; +import java.util.Comparator; + +import org.apache.brooklyn.util.collections.MutableList; + +import com.google.common.collect.Ordering; + +public class FieldOrderings { + public static class FieldNameComparator implements Comparator { + private final Comparator nameComparator; + public FieldNameComparator(Comparator nameComparator) { this.nameComparator = nameComparator; } + @Override + public int compare(Field o1, Field o2) { + return nameComparator.compare(o1.getName(), o2.getName()); + } + } + public static class FieldClassComparator implements Comparator { + private final Comparator> classComparator; + public FieldClassComparator(Comparator> classComparator) { this.classComparator = classComparator; } + @Override + public int compare(Field o1, Field o2) { + return classComparator.compare(o1.getDeclaringClass(), o2.getDeclaringClass()); + } + } + + public static Comparator> SUB_BEST_CLASS_COMPARATOR = new SubbestClassComparator(); + private static class SubbestClassComparator implements Comparator> { + @Override + public int compare(Class c1, Class c2) { + Class cS = Reflections.inferSubbest(c1, c2); + return (cS==c1 ? -1 : cS==c2 ? 1 : 0); + } + } + + /** Puts fields lower in the hierarchy first, and otherwise leaves fields in order */ + public static Comparator SUB_BEST_FIELD_FIST_THEN_DEFAULT = + new FieldClassComparator(SUB_BEST_CLASS_COMPARATOR); + /** Puts fields higher in the hierarchy first, and otherwise leaves fields in order */ + public static Comparator SUB_BEST_FIELD_LAST_THEN_DEFAULT = + new FieldClassComparator(Ordering.from(SUB_BEST_CLASS_COMPARATOR).reverse()); + /** Puts fields that are lower down the hierarchy first, and then sorts those alphabetically */ + @SuppressWarnings("unchecked") + public static Comparator SUB_BEST_FIELD_FIRST_THEN_ALPHABETICAL = Ordering.compound(MutableList.of( + new FieldClassComparator(SUB_BEST_CLASS_COMPARATOR), + new FieldNameComparator(Ordering.natural()))); + /** Puts fields that are higher up in the hierarchy first, and then sorts those alphabetically */ + @SuppressWarnings("unchecked") + public static Comparator SUB_BEST_FIELD_LAST_THEN_ALPHABETICAL = Ordering.compound(MutableList.of( + new FieldClassComparator(Ordering.from(SUB_BEST_CLASS_COMPARATOR).reverse()), + new FieldNameComparator(Ordering.natural()))); + /** Puts fields in alpha order, but in cases of duplicate those lower down the hierarchy are first */ + @SuppressWarnings("unchecked") + public static Comparator ALPHABETICAL_FIELD_THEN_SUB_BEST_FIRST = Ordering.compound(MutableList.of( + new FieldNameComparator(Ordering.natural()), + new FieldClassComparator(SUB_BEST_CLASS_COMPARATOR))); + /** Puts fields in alpha order, but in cases of duplicate those higher up in the hierarchy are first + * (potentially confusing, as this will put masked fields first) */ + @SuppressWarnings("unchecked") + public static Comparator ALPHABETICAL_FIELD_THEN_SUB_BEST_LAST = Ordering.compound(MutableList.of( + new FieldNameComparator(Ordering.natural()), + new FieldClassComparator(Ordering.from(SUB_BEST_CLASS_COMPARATOR).reverse()))); +} \ No newline at end of file diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/JavaClassNames.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/JavaClassNames.java index e9918dffe3..fe8dd0c344 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/JavaClassNames.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/JavaClassNames.java @@ -79,15 +79,23 @@ public static String simpleClassName(Class t) { String result = ct.getSimpleName(); if (Strings.isBlank(result) || result.length()<=4) { - if (ct.isPrimitive()) { - // TODO unbox - } else { - result = ct.getName(); - } + result = ct.getName(); } return result+Strings.repeat("[]", arrayCount); } + /** as {@link #simpleClassName(Class)} but if something is an inner class it drops everything before the $ */ + public static String verySimpleClassName(Class t) { + t = componentType(t); + String result = t.getSimpleName(); + result = result.substring(result.lastIndexOf('.')+1); + result = result.substring(result.lastIndexOf('$')+1); + if (Strings.isBlank(result)) { + result = t.getName(); + } + return result; + } + /** as {@link #simpleClassName(Class)} but taking the type of the object if it is not already a class * or a type-token; callers should usually do the getClass themselves, unless they aren't sure whether * it is already a Class-type object */ diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java new file mode 100644 index 0000000000..33952964e7 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java @@ -0,0 +1,71 @@ +/* + * 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.javalang; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; + +public class ReflectionPredicates { + + public static Predicate MODIFIERS_PRIVATE = new ModifiersPrivate(); + private static class ModifiersPrivate implements Predicate { + @Override public boolean apply(Integer modifiers) { return Modifier.isPrivate(modifiers); } + } + public static Predicate MODIFIERS_PUBLIC = new ModifiersPublic(); + private static class ModifiersPublic implements Predicate { + @Override public boolean apply(Integer modifiers) { return Modifier.isPublic(modifiers); } + } + public static Predicate MODIFIERS_PROTECTED = new ModifiersProtected(); + private static class ModifiersProtected implements Predicate { + @Override public boolean apply(Integer modifiers) { return Modifier.isProtected(modifiers); } + } + + public static Predicate MODIFIERS_TRANSIENT = new ModifiersTransient(); + private static class ModifiersTransient implements Predicate { + @Override public boolean apply(Integer modifiers) { return Modifier.isTransient(modifiers); } + } + public static Predicate MODIFIERS_STATIC = new ModifiersStatic(); + private static class ModifiersStatic implements Predicate { + @Override public boolean apply(Integer modifiers) { return Modifier.isStatic(modifiers); } + } + + public static Predicate fieldModifiers(Predicate modifiersCheck) { return new FieldModifiers(modifiersCheck); } + private static class FieldModifiers implements Predicate { + private Predicate modifiersCheck; + private FieldModifiers(Predicate modifiersCheck) { this.modifiersCheck = modifiersCheck; } + @Override public boolean apply(Field f) { return modifiersCheck.apply(f.getModifiers()); } + } + public static Predicate IS_FIELD_PUBLIC = fieldModifiers(MODIFIERS_PUBLIC); + public static Predicate IS_FIELD_TRANSIENT = fieldModifiers(MODIFIERS_TRANSIENT); + public static Predicate IS_FIELD_NON_TRANSIENT = Predicates.not(IS_FIELD_TRANSIENT); + public static Predicate IS_FIELD_STATIC = fieldModifiers(MODIFIERS_STATIC); + public static Predicate IS_FIELD_NON_STATIC = Predicates.not(IS_FIELD_STATIC); + + public static Predicate methodModifiers(Predicate modifiersCheck) { return new MethodModifiers(modifiersCheck); } + private static class MethodModifiers implements Predicate { + private Predicate modifiersCheck; + private MethodModifiers(Predicate modifiersCheck) { this.modifiersCheck = modifiersCheck; } + @Override public boolean apply(Method m) { return modifiersCheck.apply(m.getModifiers()); } + } + +} \ No newline at end of file diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java index 0f31882865..7b2cba4d42 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java @@ -47,18 +47,20 @@ import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; /** - * Reflection utilities ("borrowed" from cloudsoft monterey). + * Reflection utilities * * @author aled */ @@ -88,7 +90,7 @@ public ReflectionAccessException(String message, Throwable cause) { private final Map classRenameMap = MutableMap.of(); public Reflections(ClassLoader classLoader) { - this.classLoader = checkNotNull(classLoader); + this.classLoader = classLoader!=null ? classLoader : getClass().getClassLoader(); } /** supply a map of known renames, of the form "old-class -> new-class" */ @@ -99,9 +101,9 @@ public Reflections applyClassRenames(Map newClassRenames) { public Object loadInstance(String classname, Object...argValues) throws ReflectionNotFoundException, ReflectionAccessException { Class clazz = loadClass(classname); - Optional v = null; + Maybe v = null; try { - v = invokeConstructorWithArgs(clazz, argValues); + v = invokeConstructorFromArgs(clazz, argValues); if (v.isPresent()) return v.get(); } catch (Exception e) { throw new IllegalStateException("Error invoking constructor for "+clazz+Arrays.toString(argValues) + ": " + Exceptions.collapseText(e)); @@ -221,41 +223,80 @@ public Constructor loadConstructor(Class clazz, Class[] argTypes) throw } } - /** Invokes a suitable constructor, supporting varargs and primitives */ + /** @deprecated since 0.10.0 use {@link #invokeConstructorFromArgs(Class, Object...)} or one of the variants; + * this allows null field values */ @Deprecated public static Optional invokeConstructorWithArgs(ClassLoader classLoader, String className, Object...argsArray) { + return Reflections.invokeConstructorFromArgsUntyped(classLoader, className, argsArray).toOptional(); + } + /** @deprecated since 0.10.0 use {@link #invokeConstructorFromArgs(Class, Object...)} or one of the variants */ @Deprecated + public static Optional invokeConstructorWithArgs(ClassLoader classLoader, Class clazz, Object[] argsArray, boolean setAccessible) { + return invokeConstructorFromArgs(classLoader, clazz, argsArray, setAccessible).toOptional(); + } + /** @deprecated since 0.10.0 use {@link #invokeConstructorFromArgs(Class, Object...)} or one of the variants */ @Deprecated + public static Optional invokeConstructorWithArgs(Class clazz, Object...argsArray) { + return invokeConstructorFromArgs(clazz, argsArray).toOptional(); + } + /** @deprecated since 0.10.0 use {@link #invokeConstructorFromArgs(Class, Object...)} or one of the variants */ @Deprecated + public static Optional invokeConstructorWithArgs(Class clazz, Object[] argsArray, boolean setAccessible) { + return invokeConstructorFromArgs(clazz, argsArray, setAccessible).toOptional(); + } + /** @deprecated since 0.10.0 use {@link #invokeConstructorFromArgs(Class, Object...)} or one of the variants */ @Deprecated + public static Optional invokeConstructorWithArgs(Reflections reflections, Class clazz, Object[] argsArray, boolean setAccessible) { + return invokeConstructorFromArgs(reflections, clazz, argsArray, setAccessible).toOptional(); + } + + /** Finds and invokes a suitable constructor, supporting varargs and primitives, boxing and looking at compatible supertypes in the constructor's signature */ + public static Maybe invokeConstructorFromArgs(Class clazz, Object...argsArray) { + return invokeConstructorFromArgs(clazz, argsArray, false); + } + + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but allowing more configurable input */ + public static Maybe invokeConstructorFromArgs(ClassLoader classLoader, String className, Object...argsArray) { + return invokeConstructorFromArgs(classLoader, null, className, argsArray); + } + + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but allowing more configurable input */ + @SuppressWarnings("unchecked") + public static Maybe invokeConstructorFromArgs(ClassLoader classLoader, Class optionalSupertype, String className, Object...argsArray) { Reflections reflections = new Reflections(classLoader); - @SuppressWarnings("unchecked") - Class clazz = (Class) reflections.loadClass(className); - return invokeConstructorWithArgs(reflections, clazz, argsArray, false); + Class clazz = reflections.loadClass(className); + if (optionalSupertype!=null && !optionalSupertype.isAssignableFrom(clazz)) { + return Maybe.absent("The type requested '"+className+"' is not assignable to "+optionalSupertype); + } + return invokeConstructorFromArgs(reflections, (Class)clazz, argsArray, false); } - /** Invokes a suitable constructor, supporting varargs and primitives */ - public static Optional invokeConstructorWithArgs(ClassLoader classLoader, Class clazz, Object[] argsArray, boolean setAccessible) { + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but allowing more configurable input */ + public static Maybe invokeConstructorFromArgsUntyped(ClassLoader classLoader, String className, Object...argsArray) { Reflections reflections = new Reflections(classLoader); - return invokeConstructorWithArgs(reflections, clazz, argsArray, setAccessible); + @SuppressWarnings("unchecked") + Class clazz = (Class)reflections.loadClass(className); + return invokeConstructorFromArgs(reflections, clazz, argsArray, false); } - /** Invokes a suitable constructor, supporting varargs and primitives */ - public static Optional invokeConstructorWithArgs(Class clazz, Object...argsArray) { - return invokeConstructorWithArgs(clazz, argsArray, false); + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but allowing more configurable input; + * in particular setAccessible allows private constructors to be used (not the default) */ + public static Maybe invokeConstructorFromArgs(ClassLoader classLoader, Class clazz, Object[] argsArray, boolean setAccessible) { + Reflections reflections = new Reflections(classLoader); + return invokeConstructorFromArgs(reflections, clazz, argsArray, setAccessible); } - /** Invokes a suitable constructor, supporting varargs and primitives */ - public static Optional invokeConstructorWithArgs(Class clazz, Object[] argsArray, boolean setAccessible) { - ClassLoader cl = clazz.getClassLoader(); - // if bootstrap class loader - if (cl == null) { - // The classloader isn't actually used so anything non-null will work - cl = ClassLoader.getSystemClassLoader(); - } - Reflections reflections = new Reflections(cl); - return invokeConstructorWithArgs(reflections, clazz, argsArray, setAccessible); + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but allowing more configurable input; + * in particular setAccessible allows private constructors to be used (not the default) */ + public static Maybe invokeConstructorFromArgs(Class clazz, Object[] argsArray, boolean setAccessible) { + Reflections reflections = new Reflections(clazz.getClassLoader()); + return invokeConstructorFromArgs(reflections, clazz, argsArray, setAccessible); } - /** Invokes a suitable constructor, supporting varargs and primitives, additionally supporting setAccessible */ + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but will use private constructors (with setAccessible = true) */ + public static Maybe invokeConstructorFromArgsIncludingPrivate(Class clazz, Object ...argsArray) { + return Reflections.invokeConstructorFromArgs(new Reflections(clazz.getClassLoader()), clazz, argsArray, true); + } + /** As {@link #invokeConstructorFromArgs(Class, Object...)} but allowing more configurable input; + * in particular setAccessible allows private constructors to be used (not the default) */ @SuppressWarnings("unchecked") - public static Optional invokeConstructorWithArgs(Reflections reflections, Class clazz, Object[] argsArray, boolean setAccessible) { - for (Constructor constructor : clazz.getConstructors()) { + public static Maybe invokeConstructorFromArgs(Reflections reflections, Class clazz, Object[] argsArray, boolean setAccessible) { + for (Constructor constructor : MutableList.>of().appendAll(Arrays.asList(clazz.getConstructors())).appendAll(Arrays.asList(clazz.getDeclaredConstructors()))) { Class[] parameterTypes = constructor.getParameterTypes(); if (constructor.isVarArgs()) { if (typesMatchUpTo(argsArray, parameterTypes, parameterTypes.length-1)) { @@ -277,16 +318,16 @@ public static Optional invokeConstructorWithArgs(Reflections reflections, System.arraycopy(argsArray, 0, newArgsArray, 0, parameterTypes.length-1); newArgsArray[parameterTypes.length-1] = varargs; if (setAccessible) constructor.setAccessible(true); - return (Optional) Optional.of(reflections.loadInstance(constructor, newArgsArray)); + return Maybe.of((T)reflections.loadInstance(constructor, newArgsArray)); } } } if (typesMatch(argsArray, parameterTypes)) { if (setAccessible) constructor.setAccessible(true); - return (Optional) Optional.of(reflections.loadInstance(constructor, argsArray)); + return Maybe.of((T) reflections.loadInstance(constructor, argsArray)); } } - return Optional.absent(); + return Maybe.absent("Constructor not found"); } @@ -560,25 +601,86 @@ public static Method findMethod(Class clazz, String name, Class... paramet throw toThrowIfFails; } + /** Finds the field with the given name declared on the given class or any superclass, + * using {@link Class#getDeclaredField(String)}. + *

+ * If the field name contains a '.' the field is interpreted as having + * DeclaringClassCanonicalName.FieldName format, + * allowing a way to set a field unambiguously if some are masked. + *

+ * @throws NoSuchFieldException if not found + */ public static Field findField(Class clazz, String name) throws NoSuchFieldException { + return findFieldMaybe(clazz, name).orThrowUnwrapped(); + } + public static Maybe findFieldMaybe(Class clazz, String originalName) { + String name = originalName; if (clazz == null || name == null) { throw new NullPointerException("Must not be null: clazz="+clazz+"; name="+name); } Class clazzToInspect = clazz; NoSuchFieldException toThrowIfFails = null; + + String clazzRequired = null; + if (name.indexOf('.')>=0) { + int lastDotIndex = name.lastIndexOf('.'); + clazzRequired = name.substring(0, lastDotIndex); + name = name.substring(lastDotIndex+1); + } while (clazzToInspect != null) { try { - return clazzToInspect.getDeclaredField(name); + if (clazzRequired==null || clazzRequired.equals(clazzToInspect.getCanonicalName())) { + return Maybe.of(clazzToInspect.getDeclaredField(name)); + } } catch (NoSuchFieldException e) { if (toThrowIfFails == null) toThrowIfFails = e; - clazzToInspect = clazzToInspect.getSuperclass(); } + clazzToInspect = clazzToInspect.getSuperclass(); } - throw toThrowIfFails; + if (toThrowIfFails==null) return Maybe.absent("Field '"+originalName+"' not found"); + return Maybe.absent(toThrowIfFails); } + public static Maybe getFieldValueMaybe(Object instance, String fieldName) { + try { + if (instance==null) return null; + Field f = findField(instance.getClass(), fieldName); + return getFieldValueMaybe(instance, f); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + return Maybe.absent(e); + } + } + + public static Maybe getFieldValueMaybe(Object instance, Field field) { + try { + if (instance==null) return null; + if (field==null) return null; + field.setAccessible(true); + return Maybe.of(field.get(instance)); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + return Maybe.absent(e); + } + } + + /** Lists all public fields declared on the class or any ancestor, with those HIGHEST in the hierarchy first */ public static List findPublicFieldsOrderedBySuper(Class clazz) { + return findFields(clazz, new Predicate() { + @Override public boolean apply(Field input) { + return Modifier.isPublic(input.getModifiers()); + }}, FieldOrderings.SUB_BEST_FIELD_LAST_THEN_DEFAULT); + } + + /** Lists all fields declared on the class, with those lowest in the hierarchy first, + * filtered and ordered as requested. + *

+ * See {@link ReflectionPredicates} and {@link FieldOrderings} for conveniences. + *

+ * Default is no filter and {@link FieldOrderings#SUB_BEST_FIELD_LAST_THEN_ALPHABETICAL} + * */ + public static List findFields(final Class clazz, Predicate filter, Comparator fieldOrdering) { checkNotNull(clazz, "clazz"); MutableList.Builder result = MutableList.builder(); Stack> tovisit = new Stack>(); @@ -593,19 +695,12 @@ public static List findPublicFieldsOrderedBySuper(Class clazz) { if (nextclazz.getSuperclass() != null) tovisit.add(nextclazz.getSuperclass()); tovisit.addAll(Arrays.asList(nextclazz.getInterfaces())); - result.addAll(Iterables.filter(Arrays.asList(nextclazz.getDeclaredFields()), new Predicate() { - @Override public boolean apply(Field input) { - return Modifier.isPublic(input.getModifiers()); - }})); - + result.addAll(Iterables.filter(Arrays.asList(nextclazz.getDeclaredFields()), + filter!=null ? filter : Predicates.alwaysTrue())); } List resultList = result.build(); - Collections.sort(resultList, new Comparator() { - @Override public int compare(Field f1, Field f2) { - Field fsubbest = inferSubbestField(f1, f2); - return (fsubbest == null) ? 0 : (fsubbest == f1 ? 1 : -1); - }}); + Collections.sort(resultList, fieldOrdering != null ? fieldOrdering : FieldOrderings.SUB_BEST_FIELD_LAST_THEN_ALPHABETICAL); return resultList; } @@ -645,18 +740,25 @@ public static List findPublicMethodsOrderedBySuper(Class clazz) { } /** - * Gets the field that is in the sub-class; or null if one field does not come from a sub-class of the other field's class + * If the classes of the fields satisfy {@link #inferSubbest(Class, Class)} + * return the field in the lower (sub-best) class, otherwise null. */ public static Field inferSubbestField(Field f1, Field f2) { Class c1 = f1.getDeclaringClass(); Class c2 = f2.getDeclaringClass(); boolean isSuper1 = c1.isAssignableFrom(c2); boolean isSuper2 = c2.isAssignableFrom(c1); - return (isSuper1) ? (isSuper2 ? null : f2) : (isSuper2 ? f1 : null); + return (isSuper1) ? (isSuper2 ? + /* same field */ null : + /* f1 from super */ f2) : + (isSuper2 ? + /* f2 from super of f1 */ f1 : + /* fields are from different hierarchies */ null); } /** - * Gets the method that is in the sub-class; or null if one method does not come from a sub-class of the other method's class + * If the classes of the methods satisfy {@link #inferSubbest(Class, Class)} + * return the field in the lower (sub-best) class, otherwise null. */ public static Method inferSubbestMethod(Method m1, Method m2) { Class c1 = m1.getDeclaringClass(); @@ -667,7 +769,8 @@ public static Method inferSubbestMethod(Method m1, Method m2) { } /** - * Gets the class that is in the sub-class; or null if neither is a sub-class of the other. + * If one class is a subclass of the other, return that (the lower in the type hierarchy); + * otherwise return null (if they are the same or neither is a subclass of the other). */ public static Class inferSubbest(Class c1, Class c2) { boolean isSuper1 = c1.isAssignableFrom(c2); @@ -684,14 +787,25 @@ public static T cast(Object candidate, Class type) { return (T)candidate; } + /** @deprecated since 0.10.0 use {@link #invokeMethodFromArgs(Object, String, List)}; + * this allows null return values */ @Deprecated + public static Optional invokeMethodWithArgs(Object clazzOrInstance, String method, List args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + return invokeMethodWithArgs(clazzOrInstance, method, args, false); + } + /** @deprecated since 0.10.0 use {@link #invokeMethodFromArgs(Object, String, List)} */ @Deprecated + public static Optional invokeMethodWithArgs(Object clazzOrInstance, String method, List args, boolean setAccessible) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + return invokeMethodFromArgs(clazzOrInstance, method, args, setAccessible).toOptional(); + } + /** invokes the given method on the given clazz or instance, doing reasonably good matching on args etc * @throws InvocationTargetException * @throws IllegalAccessException * @throws IllegalArgumentException */ - public static Optional invokeMethodWithArgs(Object clazzOrInstance, String method, List args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { - return invokeMethodWithArgs(clazzOrInstance, method, args, false); + public static Maybe invokeMethodFromArgs(Object clazzOrInstance, String method, List args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + return invokeMethodFromArgs(clazzOrInstance, method, args, false); } - public static Optional invokeMethodWithArgs(Object clazzOrInstance, String method, List args, boolean setAccessible) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + /** as {@link #invokeMethodFromArgs(Object, String, List)} but giving control over whether to set it accessible */ + public static Maybe invokeMethodFromArgs(Object clazzOrInstance, String method, List args, boolean setAccessible) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { Preconditions.checkNotNull(clazzOrInstance, "clazz or instance"); Preconditions.checkNotNull(method, "method"); Preconditions.checkNotNull(args, "args to "+method); @@ -731,18 +845,18 @@ public static Optional invokeMethodWithArgs(Object clazzOrInstance, Stri System.arraycopy(argsArray, 0, newArgsArray, 0, parameterTypes.length-1); newArgsArray[parameterTypes.length-1] = varargs; if (setAccessible) m.setAccessible(true); - return Optional.of(m.invoke(instance, newArgsArray)); + return Maybe.of(m.invoke(instance, newArgsArray)); } } } if (typesMatch(argsArray, parameterTypes)) { if (setAccessible) m.setAccessible(true); - return Optional.of(m.invoke(instance, argsArray)); + return Maybe.of(m.invoke(instance, argsArray)); } } } - return Optional.absent(); + return Maybe.absent("Method not found matching given args"); } /** true iff all args match the corresponding types */ @@ -802,13 +916,18 @@ public static boolean hasNoNonObjectFields(Class clazz) { return hasNoNonObjectFields(clazz.getSuperclass()); } - /** Takes a map of old-class-names to renames classes, and returns the mapped name if matched, or absent */ + /** @deprecated since 0.10.0 use {@link #findMappedNameMaybe(Map, String)} */ @Deprecated public static Optional tryFindMappedName(Map renames, String name) { - if (renames==null) return Optional.absent(); + return findMappedNameMaybe(renames, name).toOptional(); + } + + /** Takes a map of old-class-names to renames classes, and returns the mapped name if matched, or absent */ + public static Maybe findMappedNameMaybe(Map renames, String name) { + if (renames==null) return Maybe.absent("no renames supplied"); String mappedName = renames.get(name); if (mappedName != null) { - return Optional.of(mappedName); + return Maybe.of(mappedName); } // look for inner classes by mapping outer class @@ -816,20 +935,20 @@ public static Optional tryFindMappedName(Map renames, St String outerClassName = name.substring(0, name.indexOf('$')); mappedName = renames.get(outerClassName); if (mappedName != null) { - return Optional.of(mappedName + name.substring(name.indexOf('$'))); + return Maybe.of(mappedName + name.substring(name.indexOf('$'))); } } - return Optional.absent(); + return Maybe.absent("mapped name not present"); } public static String findMappedNameAndLog(Map renames, String name) { - Optional rename = Reflections.tryFindMappedName(renames, name); + Maybe rename = Reflections.findMappedNameMaybe(renames, name); if (rename.isPresent()) { LOG.debug("Mapping class '"+name+"' to '"+rename.get()+"'"); return rename.get(); } return name; } - + } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Threads.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Threads.java index 8e1b3d2dea..9d4625aedc 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Threads.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Threads.java @@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.brooklyn.util.exceptions.Exceptions; -import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException; import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/BoxingTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/BoxingTest.java index 8b5a362344..802fb22250 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/BoxingTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/BoxingTest.java @@ -35,4 +35,9 @@ public static void testIntPrimitiveAndBoxed() { Assert.assertEquals(bt, Integer.class); } + @Test + public static void getPrimitive() { + Assert.assertEquals(Boxing.getPrimitiveName(Integer.class).get(), "int"); + } + } diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java index 2f0537c035..b6bb63c798 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java @@ -58,7 +58,8 @@ public void testFindPublicFieldsOrdereBySuper() throws Exception { @Test public void testConstructLangObject() { - Reflections.invokeConstructorWithArgs(java.util.Date.class); + // special test for this because the lang object might have classloader null + Assert.assertTrue(Reflections.invokeConstructorFromArgs(java.util.Date.class).get() instanceof java.util.Date); } public static interface MyInterface { @@ -123,15 +124,15 @@ public void testInvocation() throws Exception { Method m = CI1.class.getMethod("m1", String.class, int.class, int.class, int[].class); Assert.assertEquals(m.invoke(null, "hello", 1, 2, new int[] { 3, 4}), "hello10"); - Assert.assertEquals(Reflections.invokeMethodWithArgs(CI1.class, "m1", Arrays.asList("hello", 3)).get(), "hello3"); - Assert.assertEquals(Reflections.invokeMethodWithArgs(CI1.class, "m1", Arrays.asList("hello", 3, 4, 5)).get(), "hello12"); + Assert.assertEquals(Reflections.invokeMethodFromArgs(CI1.class, "m1", Arrays.asList("hello", 3)).get(), "hello3"); + Assert.assertEquals(Reflections.invokeMethodFromArgs(CI1.class, "m1", Arrays.asList("hello", 3, 4, 5)).get(), "hello12"); } @Test public void testConstruction() throws Exception { - Assert.assertEquals(Reflections.invokeConstructorWithArgs(CI1.class, new Object[] {"hello", 3}).get().constructorArgs, ImmutableList.of("hello", 3)); - Assert.assertEquals(Reflections.invokeConstructorWithArgs(CI1.class, new Object[] {"hello", 3, 4, 5}).get().constructorArgs, ImmutableList.of("hello", 3, 4, 5)); - Assert.assertFalse(Reflections.invokeConstructorWithArgs(CI1.class, new Object[] {"wrong", "args"}).isPresent()); + Assert.assertEquals(Reflections.invokeConstructorFromArgs(CI1.class, new Object[] {"hello", 3}).get().constructorArgs, ImmutableList.of("hello", 3)); + Assert.assertEquals(Reflections.invokeConstructorFromArgs(CI1.class, new Object[] {"hello", 3, 4, 5}).get().constructorArgs, ImmutableList.of("hello", 3, 4, 5)); + Assert.assertFalse(Reflections.invokeConstructorFromArgs(CI1.class, new Object[] {"wrong", "args"}).isPresent()); } interface I { }; From ec4da197d5caf3ac9bb63ab94924718aae83c615 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Fri, 24 Jun 2016 11:02:52 +0100 Subject: [PATCH 5/7] refactor TypeCoercions so that most is in utils with an interface with the Brooklyn TypeCoercions referring to and extending that --- .../spi/dsl/methods/BrooklynDslCommon.java | 2 +- .../util/core/flags/TypeCoercions.java | 908 +++--------------- .../util/core/internal/TypeCoercionsTest.java | 11 +- .../rest/util/DefaultExceptionMapper.java | 2 +- .../coerce}/ClassCoercionException.java | 23 +- .../javalang/coerce/CoerceFunctionals.java | 41 + .../coerce/CommonAdaptorTypeCoercions.java | 380 ++++++++ .../javalang/coerce/EnumTypeCoercions.java | 104 ++ .../coerce/PrimitiveStringTypeCoercions.java | 208 ++++ .../util/javalang/coerce/TypeCoercer.java | 31 + .../coerce/TypeCoercerExtensible.java | 296 ++++++ .../javalang/coerce/TypeCoercionsTest.java | 379 ++++++++ 12 files changed, 1592 insertions(+), 793 deletions(-) rename {core/src/main/java/org/apache/brooklyn/util/core/flags => utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce}/ClassCoercionException.java (78%) create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CoerceFunctionals.java create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/EnumTypeCoercions.java create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java create mode 100644 utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java create mode 100644 utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java index b88829b7e2..309785dd3b 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java @@ -46,13 +46,13 @@ import org.apache.brooklyn.core.sensor.DependentConfiguration; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.config.ConfigBag; -import org.apache.brooklyn.util.core.flags.ClassCoercionException; import org.apache.brooklyn.util.core.flags.FlagUtils; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.core.task.DeferredSupplier; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.javalang.coerce.ClassCoercionException; import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; import org.apache.brooklyn.util.text.Strings; import org.apache.commons.beanutils.BeanUtils; diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java index 65f85d61ca..01698272b9 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java @@ -22,26 +22,9 @@ import groovy.time.TimeDuration; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.URI; -import java.net.URL; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.sensor.AttributeSensor; @@ -53,443 +36,177 @@ import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.util.JavaGroovyEquivalents; -import org.apache.brooklyn.util.collections.MutableSet; -import org.apache.brooklyn.util.collections.QuorumCheck; -import org.apache.brooklyn.util.collections.QuorumCheck.QuorumChecks; import org.apache.brooklyn.util.core.task.Tasks; -import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; -import org.apache.brooklyn.util.javalang.Enums; +import org.apache.brooklyn.util.javalang.Boxing; import org.apache.brooklyn.util.javalang.JavaClassNames; -import org.apache.brooklyn.util.net.Cidr; -import org.apache.brooklyn.util.net.Networking; -import org.apache.brooklyn.util.net.UserAndHostAndPort; -import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; -import org.apache.brooklyn.util.text.Strings; -import org.apache.brooklyn.util.time.Duration; -import org.apache.brooklyn.util.time.Time; -import org.apache.brooklyn.util.yaml.Yamls; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.javalang.coerce.CommonAdaptorTypeCoercions; +import org.apache.brooklyn.util.javalang.coerce.EnumTypeCoercions; +import org.apache.brooklyn.util.javalang.coerce.PrimitiveStringTypeCoercions; +import org.apache.brooklyn.util.javalang.coerce.TypeCoercer; +import org.apache.brooklyn.util.javalang.coerce.TypeCoercerExtensible; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.CaseFormat; import com.google.common.base.Function; -import com.google.common.base.Objects; -import com.google.common.base.Preconditions; import com.google.common.base.Predicate; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.collect.Table; -import com.google.common.net.HostAndPort; -import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; -@SuppressWarnings("rawtypes") +/** Static class providing a shared {@link TypeCoercer} for all of Brooklyn */ public class TypeCoercions { private static final Logger log = LoggerFactory.getLogger(TypeCoercions.class); private TypeCoercions() {} - /** Store the coercion {@link Function functions} in a {@link Table table}. */ - @GuardedBy("TypeCoercions.class") - private static Table registry = HashBasedTable.create(); - - /** - * Attempts to coerce {@code value} to {@code targetType}. - *

- * Maintains a registry of adapter functions for type pairs in a {@link Table} which - * is searched after checking various strategies, including the following: - *

    - *
  • {@code value.asTargetType()} - *
  • {@code TargetType.fromType(value)} (if {@code value instanceof Type}) - *
  • {@code value.targetTypeValue()} (handy for primitives) - *
  • {@code TargetType.valueOf(value)} (for enums) - *
- *

- * A default set of adapters will handle most common Java-type coercions - * as well as String coercion to: - *

    - *
  • {@link Set}, {@link List}, {@link Map} and similar -- parses as YAML - *
  • {@link Date} -- parses using {@link Time#parseDate(String)} - *
  • {@link Duration} -- parses using {@link Duration#parse(String)} - *
- */ - public static T coerce(Object value, Class targetType) { - return coerce(value, TypeToken.of(targetType)); + static TypeCoercerExtensible coercer; + static { + coercer = TypeCoercerExtensible.newEmpty(); + BrooklynInitialization.initTypeCoercionStandardAdapters(); + } + + public static void initStandardAdapters() { + new CommonAdaptorTypeCoercions(coercer).registerAllAdapters(); + registerDeprecatedBrooklynAdapters(); + registerBrooklynAdapters(); + registerGroovyAdapters(); } + + public static T coerce(Object input, Class type) { return coercer.coerce(input, type); } + public static T coerce(Object input, TypeToken type) { return coercer.coerce(input, type); } + public static Maybe tryCoerce(Object input, Class type) { return coercer.tryCoerce(input, type); } + public static Maybe tryCoerce(Object input, TypeToken type) { return coercer.tryCoerce(input, type); } - /** @see #coerce(Object, Class); allows a null value in the contents of the Maybe */ - public static Maybe tryCoerce(Object value, TypeToken targetTypeToken) { - try { - return Maybe.ofAllowingNull( coerce(value, targetTypeToken) ); - } catch (Throwable t) { - Exceptions.propagateIfFatal(t); - return Maybe.absent(t); - } + public static Function registerAdapter(Class sourceType, Class targetType, Function fn) { + return coercer.registerAdapter(sourceType, targetType, fn); } - /** @see #coerce(Object, Class) */ - @SuppressWarnings({ "unchecked" }) - public static T coerce(Object value, TypeToken targetTypeToken) { - if (value==null) return null; - Class targetType = targetTypeToken.getRawType(); + public static Function function(final Class type) { + return coercer.function(type); + } - //recursive coercion of parameterized collections and map entries - if (targetTypeToken.getType() instanceof ParameterizedType) { - if (value instanceof Collection && Collection.class.isAssignableFrom(targetType)) { - Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments(); - if (arguments.length != 1) { - throw new IllegalStateException("Unexpected number of parameters in collection type: " + arguments); - } - Collection coerced = Lists.newLinkedList(); - TypeToken listEntryType = TypeToken.of(arguments[0]); - for (Object entry : (Iterable) value) { - coerced.add(coerce(entry, listEntryType)); - } - if (Set.class.isAssignableFrom(targetType)) { - return (T) Sets.newLinkedHashSet(coerced); - } else { - return (T) Lists.newArrayList(coerced); - } - } else if (value instanceof Map && Map.class.isAssignableFrom(targetType)) { - Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments(); - if (arguments.length != 2) { - throw new IllegalStateException("Unexpected number of parameters in map type: " + arguments); - } - Map coerced = Maps.newLinkedHashMap(); - TypeToken mapKeyType = TypeToken.of(arguments[0]); - TypeToken mapValueType = TypeToken.of(arguments[1]); - for (Map.Entry entry : ((Map) value).entrySet()) { - coerced.put(coerce(entry.getKey(), mapKeyType), coerce(entry.getValue(), mapValueType)); - } - return (T) Maps.newLinkedHashMap(coerced); + @SuppressWarnings({"unused", "deprecation", "unchecked", "rawtypes"}) + public static void registerDeprecatedBrooklynAdapters() { + registerAdapter(Closure.class, ConfigurableEntityFactory.class, new Function() { + @Override + public ConfigurableEntityFactory apply(Closure input) { + return new ClosureEntityFactory(input); } - } - - if (targetType.isInstance(value)) return (T) value; - - // TODO use registry first? - - //deal with primitive->primitive casting - if (isPrimitiveOrBoxer(targetType) && isPrimitiveOrBoxer(value.getClass())) { - // Don't just rely on Java to do its normal casting later; if caller writes - // long `l = coerce(new Integer(1), Long.class)` then letting java do its casting will fail, - // because an Integer will not automatically be unboxed and cast to a long - return castPrimitive(value, (Class)targetType); - } - - //deal with string->primitive - if (value instanceof String && isPrimitiveOrBoxer(targetType)) { - return stringToPrimitive((String)value, (Class)targetType); - } - - //deal with primitive->string - if (isPrimitiveOrBoxer(value.getClass()) && targetType.equals(String.class)) { - return (T) value.toString(); - } - - //look for value.asType where Type is castable to targetType - String targetTypeSimpleName = getVerySimpleName(targetType); - if (targetTypeSimpleName!=null && targetTypeSimpleName.length()>0) { - for (Method m: value.getClass().getMethods()) { - if (m.getName().startsWith("as") && m.getParameterTypes().length==0 && - targetType.isAssignableFrom(m.getReturnType()) ) { - if (m.getName().equals("as"+getVerySimpleName(m.getReturnType()))) { - try { - return (T) m.invoke(value); - } catch (Exception e) { - throw new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed, "+e); - } - } + }); + Function ignoredVarHereToAllowSuppressDeprecationWarning1 = registerAdapter(org.apache.brooklyn.core.entity.factory.EntityFactory.class, ConfigurableEntityFactory.class, new Function() { + @Override + public ConfigurableEntityFactory apply(org.apache.brooklyn.core.entity.factory.EntityFactory input) { + if (input instanceof ConfigurableEntityFactory) return (ConfigurableEntityFactory)input; + return new ConfigurableEntityFactoryFromEntityFactory(input); + } + }); + Function ignoredVarHereToAllowSuppressDeprecationWarning2 = registerAdapter(Closure.class, org.apache.brooklyn.core.entity.factory.EntityFactory.class, new Function() { + @Override + public org.apache.brooklyn.core.entity.factory.EntityFactory apply(Closure input) { + return new ClosureEntityFactory(input); + } + }); + } + + @SuppressWarnings("rawtypes") + public static void registerBrooklynAdapters() { + registerAdapter(String.class, AttributeSensor.class, new Function() { + @Override + public AttributeSensor apply(final String input) { + Entity entity = BrooklynTaskTags.getContextEntity(Tasks.current()); + if (entity!=null) { + Sensor result = entity.getEntityType().getSensor(input); + if (result instanceof AttributeSensor) + return (AttributeSensor) result; } + return Sensors.newSensor(Object.class, input); } - } - - //now look for static TargetType.fromType(Type t) where value instanceof Type - for (Method m: targetType.getMethods()) { - if (((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC) && - m.getName().startsWith("from") && m.getParameterTypes().length==1 && - m.getParameterTypes()[0].isInstance(value)) { - if (m.getName().equals("from"+getVerySimpleName(m.getParameterTypes()[0]))) { - try { - return (T) m.invoke(null, value); - } catch (Exception e) { - throw new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed, "+e); - } + }); + registerAdapter(String.class, Sensor.class, new Function() { + @Override + public AttributeSensor apply(final String input) { + Entity entity = BrooklynTaskTags.getContextEntity(Tasks.current()); + if (entity!=null) { + Sensor result = entity.getEntityType().getSensor(input); + if (result != null) + return (AttributeSensor) result; } + return Sensors.newSensor(Object.class, input); } - } - - //ENHANCEMENT could look in type hierarchy of both types for a conversion method... - - //primitives get run through again boxed up - Class boxedT = UNBOXED_TO_BOXED_TYPES.get(targetType); - Class boxedVT = UNBOXED_TO_BOXED_TYPES.get(value.getClass()); - if (boxedT!=null || boxedVT!=null) { - try { - if (boxedT==null) boxedT=targetType; - Object boxedV; - if (boxedVT==null) { boxedV = value; } - else { boxedV = boxedVT.getConstructor(value.getClass()).newInstance(value); } - return (T) coerce(boxedV, boxedT); - } catch (Exception e) { - throw new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): unboxing failed, "+e); + }); + } + + @SuppressWarnings("rawtypes") + public static void registerGroovyAdapters() { + registerAdapter(Closure.class, Predicate.class, new Function() { + @Override + public Predicate apply(final Closure closure) { + return new Predicate() { + @Override public boolean apply(Object input) { + return (Boolean) closure.call(input); + } + }; } - } - - //for enums call valueOf with the string representation of the value - if (targetType.isEnum()) { - T result = (T) stringToEnum((Class) targetType, null).apply(String.valueOf(value)); - if (result != null) return result; - } - - //now look in registry - synchronized (TypeCoercions.class) { - Map adapters = registry.row(targetType); - for (Map.Entry entry : adapters.entrySet()) { - if (entry.getKey().isInstance(value)) { - T result = (T) entry.getValue().apply(value); - - // Check if need to unwrap again (e.g. if want List and are given a String "1,2,3" - // then we'll have so far converted to List.of("1", "2", "3"). Call recursively. - // First check that value has changed, to avoid stack overflow! - if (!Objects.equal(value, result) && targetTypeToken.getType() instanceof ParameterizedType) { - // Could duplicate check for `result instanceof Collection` etc; but recursive call - // will be fine as if that doesn't match we'll safely reach `targetType.isInstance(value)` - // and just return the result. - return coerce(result, targetTypeToken); + }); + registerAdapter(Closure.class, Function.class, new Function() { + @Override + public Function apply(final Closure closure) { + return new Function() { + @Override public Object apply(Object input) { + return closure.call(input); } - return result; - } + }; } - } - - //not found - if (targetType.isEnum()) { - try { - throw new ClassCoercionException("Invalid value '"+value+"' for "+JavaClassNames.simpleClassName(targetType)+"; expected one of "+ - Arrays.asList((Object[])targetType.getMethod("values").invoke(null))); - } catch (ClassCoercionException e) { - throw e; - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - // fall back to below + }); + registerAdapter(Object.class, TimeDuration.class, new Function() { + @SuppressWarnings("deprecation") + @Override + public TimeDuration apply(final Object input) { + log.warn("deprecated automatic coercion of Object to TimeDuration (set breakpoint in TypeCoercions to inspect, convert to Duration)"); + return JavaGroovyEquivalents.toTimeDuration(input); } - } - throw new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetType.getCanonicalName()+" ("+value+"): no adapter known"); - } - - /** - * Returns a function that does a type coercion to the given type. For example, - * {@code TypeCoercions.function(Double.class)} will return a function that will - * coerce its input value to a {@link Double} (or throw a {@link ClassCoercionException} - * if that is not possible). - */ - public static Function function(final Class type) { - return new CoerceFunction(type); + }); + registerAdapter(TimeDuration.class, Long.class, new Function() { + @Override + public Long apply(final TimeDuration input) { + log.warn("deprecated automatic coercion of TimeDuration to Long (set breakpoint in TypeCoercions to inspect, use Duration instead of Long!)"); + return input.toMilliseconds(); + } + }); } - - private static class CoerceFunction implements Function { - private final Class type; - public CoerceFunction(Class type) { - this.type = type; - } - @Override - public T apply(Object input) { - return coerce(input, type); - } - } + // ---- legacy compatibility - /** - * Type coercion {@link Function function} for {@link Enum enums}. - *

- * Tries to convert the string to {@link CaseFormat#UPPER_UNDERSCORE} first, - * handling all of the different {@link CaseFormat format} possibilites. Failing - * that, it tries a case-insensitive comparison with the valid enum values. - *

- * Returns {@code defaultValue} if the string cannot be converted. - * - * @see TypeCoercions#coerce(Object, Class) - * @see Enum#valueOf(Class, String) - */ + /** @deprecated since 0.10.0 see method in {@link EnumTypeCoercions} */ @Deprecated public static > Function stringToEnum(final Class type, @Nullable final E defaultValue) { - return new StringToEnumFunction(type, defaultValue); + return EnumTypeCoercions.stringToEnum(type, defaultValue); } - - private static class StringToEnumFunction> implements Function { - private final Class type; - private final E defaultValue; - public StringToEnumFunction(Class type, @Nullable E defaultValue) { - this.type = type; - this.defaultValue = defaultValue; - } - @Override - public E apply(String input) { - Preconditions.checkNotNull(input, "input"); - List options = ImmutableList.of( - input, - CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, input), - CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, input), - CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, input), - CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, input)); - for (String value : options) { - try { - return Enum.valueOf(type, value); - } catch (IllegalArgumentException iae) { - continue; - } - } - Maybe result = Enums.valueOfIgnoreCase(type, input); - return (result.isPresent()) ? result.get() : defaultValue; - } - } - - /** - * Sometimes need to explicitly cast primitives, rather than relying on Java casting. - * For example, when using generics then type-erasure means it doesn't actually cast, - * which causes tests to fail with 0 != 0.0 - */ - @SuppressWarnings("unchecked") + /** @deprecated since 0.10.0 see method in {@link PrimitiveStringTypeCoercions} */ @Deprecated public static T castPrimitive(Object value, Class targetType) { - if (value==null) return null; - assert isPrimitiveOrBoxer(targetType) : "targetType="+targetType; - assert isPrimitiveOrBoxer(value.getClass()) : "value="+targetType+"; valueType="+value.getClass(); - - Class sourceWrapType = Primitives.wrap(value.getClass()); - Class targetWrapType = Primitives.wrap(targetType); - - // optimization, for when already correct type - if (sourceWrapType == targetWrapType) { - return (T) value; - } - - if (targetWrapType == Boolean.class) { - // only char can be mapped to boolean - // (we could say 0=false, nonzero=true, but there is no compelling use case so better - // to encourage users to write as boolean) - if (sourceWrapType == Character.class) - return (T) stringToPrimitive(value.toString(), targetType); - - throw new ClassCoercionException("Cannot cast "+sourceWrapType+" ("+value+") to "+targetType); - } else if (sourceWrapType == Boolean.class) { - // boolean can't cast to anything else - - throw new ClassCoercionException("Cannot cast "+sourceWrapType+" ("+value+") to "+targetType); - } - - // for whole-numbers (where casting to long won't lose anything)... - long v = 0; - boolean islong = true; - if (sourceWrapType == Character.class) { - v = (long) ((Character)value).charValue(); - } else if (sourceWrapType == Byte.class) { - v = (long) ((Byte)value).byteValue(); - } else if (sourceWrapType == Short.class) { - v = (long) ((Short)value).shortValue(); - } else if (sourceWrapType == Integer.class) { - v = (long) ((Integer)value).intValue(); - } else if (sourceWrapType == Long.class) { - v = ((Long)value).longValue(); - } else { - islong = false; - } - if (islong) { - if (targetWrapType == Character.class) return (T) Character.valueOf((char)v); - if (targetWrapType == Byte.class) return (T) Byte.valueOf((byte)v); - if (targetWrapType == Short.class) return (T) Short.valueOf((short)v); - if (targetWrapType == Integer.class) return (T) Integer.valueOf((int)v); - if (targetWrapType == Long.class) return (T) Long.valueOf((long)v); - if (targetWrapType == Float.class) return (T) Float.valueOf((float)v); - if (targetWrapType == Double.class) return (T) Double.valueOf((double)v); - throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType); - } - - // for real-numbers (cast to double)... - double d = 0; - boolean isdouble = true; - if (sourceWrapType == Float.class) { - d = (double) ((Float)value).floatValue(); - } else if (sourceWrapType == Double.class) { - d = (double) ((Double)value).doubleValue(); - } else { - isdouble = false; - } - if (isdouble) { - if (targetWrapType == Character.class) return (T) Character.valueOf((char)d); - if (targetWrapType == Byte.class) return (T) Byte.valueOf((byte)d); - if (targetWrapType == Short.class) return (T) Short.valueOf((short)d); - if (targetWrapType == Integer.class) return (T) Integer.valueOf((int)d); - if (targetWrapType == Long.class) return (T) Long.valueOf((long)d); - if (targetWrapType == Float.class) return (T) Float.valueOf((float)d); - if (targetWrapType == Double.class) return (T) Double.valueOf((double)d); - throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType); - } else { - throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType); - } + return PrimitiveStringTypeCoercions.castPrimitive(value, targetType); } + /** @deprecated since 0.10.0 see method in {@link PrimitiveStringTypeCoercions} */ @Deprecated public static boolean isPrimitiveOrBoxer(Class type) { - return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type); + return PrimitiveStringTypeCoercions.isPrimitiveOrBoxer(type); } - - @SuppressWarnings("unchecked") + + /** @deprecated since 0.10.0 see method in {@link PrimitiveStringTypeCoercions} */ @Deprecated public static T stringToPrimitive(String value, Class targetType) { - assert Primitives.allPrimitiveTypes().contains(targetType) || Primitives.allWrapperTypes().contains(targetType) : "targetType="+targetType; - // If char, then need to do explicit conversion - if (targetType == Character.class || targetType == char.class) { - if (value.length() == 1) { - return (T) (Character) value.charAt(0); - } else if (value.length() != 1) { - throw new ClassCoercionException("Cannot coerce type String to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); - } - } - value = value.trim(); - // For boolean we could use valueOf, but that returns false whereas we'd rather throw errors on bad values - if (targetType == Boolean.class || targetType == boolean.class) { - if ("true".equalsIgnoreCase(value)) return (T) Boolean.TRUE; - if ("false".equalsIgnoreCase(value)) return (T) Boolean.FALSE; - if ("yes".equalsIgnoreCase(value)) return (T) Boolean.TRUE; - if ("no".equalsIgnoreCase(value)) return (T) Boolean.FALSE; - if ("t".equalsIgnoreCase(value)) return (T) Boolean.TRUE; - if ("f".equalsIgnoreCase(value)) return (T) Boolean.FALSE; - if ("y".equalsIgnoreCase(value)) return (T) Boolean.TRUE; - if ("n".equalsIgnoreCase(value)) return (T) Boolean.FALSE; - - throw new ClassCoercionException("Cannot coerce type String to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); - } - - // Otherwise can use valueOf reflectively - Class wrappedType; - if (Primitives.allPrimitiveTypes().contains(targetType)) { - wrappedType = Primitives.wrap(targetType); - } else { - wrappedType = targetType; - } - - try { - return (T) wrappedType.getMethod("valueOf", String.class).invoke(null, value); - } catch (Exception e) { - ClassCoercionException tothrow = new ClassCoercionException("Cannot coerce "+JavaStringEscapes.wrapJavaString(value)+" to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); - tothrow.initCause(e); - throw tothrow; - } + return PrimitiveStringTypeCoercions.stringToPrimitive(value, targetType); } - /** returns the simple class name, and for any inner class the portion after the $ */ + /** @deprecated since 0.10.0 see {@link JavaClassNames#verySimpleClassName(Class)} */ @Deprecated + @SuppressWarnings("rawtypes") public static String getVerySimpleName(Class c) { - String s = c.getSimpleName(); - if (s.indexOf('$')>=0) - s = s.substring(s.lastIndexOf('$')+1); - return s; + return JavaClassNames.verySimpleClassName(c); } + + /** @deprecated since 0.10.0 see {@link Boxing#PRIMITIVE_TO_BOXED} and its inverse() method */ + @SuppressWarnings("rawtypes") public static final Map BOXED_TO_UNBOXED_TYPES = ImmutableMap.builder(). put(Integer.class, Integer.TYPE). put(Long.class, Long.TYPE). @@ -500,6 +217,8 @@ public static String getVerySimpleName(Class c) { put(Character.class, Character.TYPE). put(Short.class, Short.TYPE). build(); + /** @deprecated since 0.10.0 see {@link Boxing#PRIMITIVE_TO_BOXED} */ @Deprecated + @SuppressWarnings("rawtypes") public static final Map UNBOXED_TO_BOXED_TYPES = ImmutableMap.builder(). put(Integer.TYPE, Integer.class). put(Long.TYPE, Long.class). @@ -511,7 +230,10 @@ public static String getVerySimpleName(Class c) { put(Short.TYPE, Short.class). build(); - /** for automatic conversion */ + /** for automatic conversion; + * @deprecated since 0.10.0 not used; there may be something similar in {@link Reflections} */ + @Deprecated + @SuppressWarnings("rawtypes") public static Object getMatchingConstructor(Class target, Object ...arguments) { Constructor[] cc = target.getConstructors(); for (Constructor c: cc) { @@ -530,374 +252,4 @@ public static Object getMatchingConstructor(Class target, Object ...arguments) { } return null; } - - /** Registers an adapter for use with type coercion. Returns any old adapter. */ - public synchronized static Function registerAdapter(Class sourceType, Class targetType, Function fn) { - return registry.put(targetType, sourceType, fn); - } - - static { BrooklynInitialization.initTypeCoercionStandardAdapters(); } - - public static void initStandardAdapters() { - registerAdapter(CharSequence.class, String.class, new Function() { - @Override - public String apply(CharSequence input) { - return input.toString(); - } - }); - registerAdapter(byte[].class, String.class, new Function() { - @Override - public String apply(byte[] input) { - return new String(input); - } - }); - registerAdapter(Collection.class, Set.class, new Function() { - @SuppressWarnings("unchecked") - @Override - public Set apply(Collection input) { - return Sets.newLinkedHashSet(input); - } - }); - registerAdapter(Collection.class, List.class, new Function() { - @SuppressWarnings("unchecked") - @Override - public List apply(Collection input) { - return Lists.newArrayList(input); - } - }); - registerAdapter(String.class, InetAddress.class, new Function() { - @Override - public InetAddress apply(String input) { - return Networking.getInetAddressWithFixedName(input); - } - }); - registerAdapter(String.class, HostAndPort.class, new Function() { - @Override - public HostAndPort apply(String input) { - return HostAndPort.fromString(input); - } - }); - registerAdapter(String.class, UserAndHostAndPort.class, new Function() { - @Override - public UserAndHostAndPort apply(String input) { - return UserAndHostAndPort.fromString(input); - } - }); - registerAdapter(String.class, Cidr.class, new Function() { - @Override - public Cidr apply(String input) { - return new Cidr(input); - } - }); - registerAdapter(String.class, URL.class, new Function() { - @Override - public URL apply(String input) { - try { - return new URL(input); - } catch (Exception e) { - throw Exceptions.propagate(e); - } - } - }); - registerAdapter(URL.class, String.class, new Function() { - @Override - public String apply(URL input) { - return input.toString(); - } - }); - registerAdapter(String.class, URI.class, new Function() { - @Override - public URI apply(String input) { - return URI.create(input); - } - }); - registerAdapter(URI.class, String.class, new Function() { - @Override - public String apply(URI input) { - return input.toString(); - } - }); - registerAdapter(Closure.class, ConfigurableEntityFactory.class, new Function() { - @SuppressWarnings("unchecked") - @Override - public ConfigurableEntityFactory apply(Closure input) { - return new ClosureEntityFactory(input); - } - }); - @SuppressWarnings({"unused", "deprecation"}) - Function ignoredVarHereToAllowSuppressDeprecationWarning1 = registerAdapter(org.apache.brooklyn.core.entity.factory.EntityFactory.class, ConfigurableEntityFactory.class, new Function() { - @SuppressWarnings("unchecked") - @Override - public ConfigurableEntityFactory apply(org.apache.brooklyn.core.entity.factory.EntityFactory input) { - if (input instanceof ConfigurableEntityFactory) return (ConfigurableEntityFactory)input; - return new ConfigurableEntityFactoryFromEntityFactory(input); - } - }); - @SuppressWarnings({"unused", "deprecation"}) - Function ignoredVarHereToAllowSuppressDeprecationWarning2 = registerAdapter(Closure.class, org.apache.brooklyn.core.entity.factory.EntityFactory.class, new Function() { - @SuppressWarnings("unchecked") - @Override - public org.apache.brooklyn.core.entity.factory.EntityFactory apply(Closure input) { - return new ClosureEntityFactory(input); - } - }); - registerAdapter(Closure.class, Predicate.class, new Function() { - @Override - public Predicate apply(final Closure closure) { - return new Predicate() { - @Override public boolean apply(Object input) { - return (Boolean) closure.call(input); - } - }; - } - }); - registerAdapter(Closure.class, Function.class, new Function() { - @Override - public Function apply(final Closure closure) { - return new Function() { - @Override public Object apply(Object input) { - return closure.call(input); - } - }; - } - }); - registerAdapter(Object.class, Duration.class, new Function() { - @Override - public Duration apply(final Object input) { - return org.apache.brooklyn.util.time.Duration.of(input); - } - }); - registerAdapter(Object.class, TimeDuration.class, new Function() { - @SuppressWarnings("deprecation") - @Override - public TimeDuration apply(final Object input) { - log.warn("deprecated automatic coercion of Object to TimeDuration (set breakpoint in TypeCoercions to inspect, convert to Duration)"); - return JavaGroovyEquivalents.toTimeDuration(input); - } - }); - registerAdapter(TimeDuration.class, Long.class, new Function() { - @Override - public Long apply(final TimeDuration input) { - log.warn("deprecated automatic coercion of TimeDuration to Long (set breakpoint in TypeCoercions to inspect, use Duration instead of Long!)"); - return input.toMilliseconds(); - } - }); - registerAdapter(Integer.class, AtomicLong.class, new Function() { - @Override public AtomicLong apply(final Integer input) { - return new AtomicLong(input); - } - }); - registerAdapter(Long.class, AtomicLong.class, new Function() { - @Override public AtomicLong apply(final Long input) { - return new AtomicLong(input); - } - }); - registerAdapter(String.class, AtomicLong.class, new Function() { - @Override public AtomicLong apply(final String input) { - return new AtomicLong(Long.parseLong(input.trim())); - } - }); - registerAdapter(Integer.class, AtomicInteger.class, new Function() { - @Override public AtomicInteger apply(final Integer input) { - return new AtomicInteger(input); - } - }); - registerAdapter(String.class, AtomicInteger.class, new Function() { - @Override public AtomicInteger apply(final String input) { - return new AtomicInteger(Integer.parseInt(input.trim())); - } - }); - /** This always returns a {@link Double}, cast as a {@link Number}; - * however primitives and boxers get exact typing due to call in #stringToPrimitive */ - registerAdapter(String.class, Number.class, new Function() { - @Override - public Number apply(String input) { - return Double.valueOf(input); - } - }); - registerAdapter(BigDecimal.class, Double.class, new Function() { - @Override - public Double apply(BigDecimal input) { - return input.doubleValue(); - } - }); - registerAdapter(BigInteger.class, Long.class, new Function() { - @Override - public Long apply(BigInteger input) { - return input.longValue(); - } - }); - registerAdapter(BigInteger.class, Integer.class, new Function() { - @Override - public Integer apply(BigInteger input) { - return input.intValue(); - } - }); - registerAdapter(String.class, BigDecimal.class, new Function() { - @Override - public BigDecimal apply(String input) { - return new BigDecimal(input); - } - }); - registerAdapter(Double.class, BigDecimal.class, new Function() { - @Override - public BigDecimal apply(Double input) { - return BigDecimal.valueOf(input); - } - }); - registerAdapter(String.class, BigInteger.class, new Function() { - @Override - public BigInteger apply(String input) { - return new BigInteger(input); - } - }); - registerAdapter(Long.class, BigInteger.class, new Function() { - @Override - public BigInteger apply(Long input) { - return BigInteger.valueOf(input); - } - }); - registerAdapter(Integer.class, BigInteger.class, new Function() { - @Override - public BigInteger apply(Integer input) { - return BigInteger.valueOf(input); - } - }); - registerAdapter(String.class, Date.class, new Function() { - @Override - public Date apply(final String input) { - return Time.parseDate(input); - } - }); - registerAdapter(String.class, Class.class, new Function() { - @Override - public Class apply(final String input) { - try { - return Class.forName(input); - } catch (ClassNotFoundException e) { - throw Exceptions.propagate(e); - } - } - }); - registerAdapter(String.class, AttributeSensor.class, new Function() { - @Override - public AttributeSensor apply(final String input) { - Entity entity = BrooklynTaskTags.getContextEntity(Tasks.current()); - if (entity!=null) { - Sensor result = entity.getEntityType().getSensor(input); - if (result instanceof AttributeSensor) - return (AttributeSensor) result; - } - return Sensors.newSensor(Object.class, input); - } - }); - registerAdapter(String.class, Sensor.class, new Function() { - @Override - public AttributeSensor apply(final String input) { - Entity entity = BrooklynTaskTags.getContextEntity(Tasks.current()); - if (entity!=null) { - Sensor result = entity.getEntityType().getSensor(input); - if (result != null) - return (AttributeSensor) result; - } - return Sensors.newSensor(Object.class, input); - } - }); - registerAdapter(String.class, List.class, new Function() { - @Override - public List apply(final String input) { - return JavaStringEscapes.unwrapJsonishListIfPossible(input); - } - }); - registerAdapter(String.class, Set.class, new Function() { - @Override - public Set apply(final String input) { - return MutableSet.copyOf(JavaStringEscapes.unwrapJsonishListIfPossible(input)).asUnmodifiable(); - } - }); - registerAdapter(String.class, QuorumCheck.class, new Function() { - @Override - public QuorumCheck apply(final String input) { - return QuorumChecks.of(input); - } - }); - registerAdapter(Iterable.class, String[].class, new Function() { - @Nullable - @Override - public String[] apply(@Nullable Iterable list) { - if (list == null) return null; - String[] result = new String[Iterables.size(list)]; - int count = 0; - for (Object element : list) { - result[count++] = coerce(element, String.class); - } - return result; - } - }); - registerAdapter(Iterable.class, Integer[].class, new Function() { - @Nullable - @Override - public Integer[] apply(@Nullable Iterable list) { - if (list == null) return null; - Integer[] result = new Integer[Iterables.size(list)]; - int count = 0; - for (Object element : list) { - result[count++] = coerce(element, Integer.class); - } - return result; - } - }); - registerAdapter(Iterable.class, int[].class, new Function() { - @Nullable - @Override - public int[] apply(@Nullable Iterable list) { - if (list == null) return null; - int[] result = new int[Iterables.size(list)]; - int count = 0; - for (Object element : list) { - result[count++] = coerce(element, int.class); - } - return result; - } - }); - registerAdapter(String.class, Map.class, new Function() { - @Override - public Map apply(final String input) { - Exception error = null; - - // first try wrapping in braces if needed - if (!input.trim().startsWith("{")) { - try { - return apply("{ "+input+" }"); - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - // prefer this error - error = e; - // fall back to parsing without braces, e.g. if it's multiline - } - } - - try { - return Yamls.getAs( Yamls.parseAll(input), Map.class ); - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - if (error!=null && input.indexOf('\n')==-1) { - // prefer the original error if it wasn't braced and wasn't multiline - e = error; - } - throw new IllegalArgumentException("Cannot parse string as map with flexible YAML parsing; "+ - (e instanceof ClassCastException ? "yaml treats it as a string" : - (e instanceof IllegalArgumentException && Strings.isNonEmpty(e.getMessage())) ? e.getMessage() : - ""+e) ); - } - - // NB: previously we supported this also, when we did json above; - // yaml support is better as it supports quotes (and better than json because it allows dropping quotes) - // snake-yaml, our parser, also accepts key=value -- although i'm not sure this is strictly yaml compliant; - // our tests will catch it if snake behaviour changes, and we can reinstate this - // (but note it doesn't do quotes; see http://code.google.com/p/guava-libraries/issues/detail?id=412 for that): -// return ImmutableMap.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().withKeyValueSeparator("=").split(input)); - } - }); - } } diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java index 47d4c4938c..36a9ea7a54 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java @@ -33,9 +33,10 @@ import java.util.Set; import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; +import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableSet; -import org.apache.brooklyn.util.core.flags.ClassCoercionException; import org.apache.brooklyn.util.core.flags.TypeCoercions; +import org.apache.brooklyn.util.javalang.coerce.ClassCoercionException; import org.apache.brooklyn.util.text.StringPredicates; import org.codehaus.groovy.runtime.GStringImpl; import org.slf4j.Logger; @@ -296,11 +297,11 @@ public void testKeyEqualsValueStringToMapCoercion() { Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", "2")); } - @Test(expectedExceptions=IllegalArgumentException.class) + @Test(expectedExceptions=ClassCoercionException.class) public void testJsonStringWithoutBracesOrSpaceDisallowedAsMapCoercion() { // yaml requires spaces after the colon - Map s = TypeCoercions.coerce("a:1,b:2", Map.class); - Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", 2)); + TypeCoercions.coerce("a:1,b:2", Map.class); + Asserts.shouldHaveFailedPreviously(); } @Test @@ -351,7 +352,7 @@ public void testCoerceStringToNumber() { assertEquals(TypeCoercions.coerce("1.0", Number.class), (Number) Double.valueOf(1.0)); } - @Test(expectedExceptions = ClassCoercionException.class) + @Test(expectedExceptions = org.apache.brooklyn.util.javalang.coerce.ClassCoercionException.class) public void testInvalidCoercionThrowsClassCoercionException() { TypeCoercions.coerce(new Object(), TypeToken.of(Integer.class)); } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java index dffc143f1e..010bcca12a 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java @@ -31,9 +31,9 @@ import org.apache.brooklyn.rest.domain.ApiError; import org.apache.brooklyn.rest.domain.ApiError.Builder; import org.apache.brooklyn.util.collections.MutableSet; -import org.apache.brooklyn.util.core.flags.ClassCoercionException; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.exceptions.UserFacingException; +import org.apache.brooklyn.util.javalang.coerce.ClassCoercionException; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/ClassCoercionException.java similarity index 78% rename from core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java rename to utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/ClassCoercionException.java index cea7484cd9..fbc1cbe0d6 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/ClassCoercionException.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.brooklyn.util.core.flags; +package org.apache.brooklyn.util.javalang.coerce; /** * Thrown to indicate that {@link TypeCoercions} could not cast an object from one @@ -25,17 +25,24 @@ public class ClassCoercionException extends ClassCastException { private static final long serialVersionUID = -4616045237993172497L; + private final Throwable cause; + public ClassCoercionException() { super(); + cause = null; } - - /** - * Constructs a ClassCoercionException with the specified - * detail message. - * - * @param s the detail message. - */ public ClassCoercionException(String s) { super(s); + cause = null; + } + public ClassCoercionException(String s, Throwable cause) { + super(s); + this.cause = cause; + } + + @Override + public Throwable getCause() { + return cause; } + } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CoerceFunctionals.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CoerceFunctionals.java new file mode 100644 index 0000000000..b03a8b3553 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CoerceFunctionals.java @@ -0,0 +1,41 @@ +/* + * 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.javalang.coerce; + +import com.google.common.base.Function; + +public class CoerceFunctionals { + + private CoerceFunctionals() {} + + public static class CoerceFunction implements Function { + private final TypeCoercer coercer; + private final Class type; + + public CoerceFunction(TypeCoercer coercer, Class type) { + this.coercer = coercer; + this.type = type; + } + @Override + public T apply(Object input) { + return coercer.coerce(input, type); + } + } + +} \ No newline at end of file diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java new file mode 100644 index 0000000000..94c93e9bfd --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java @@ -0,0 +1,380 @@ +/* + * 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.javalang.coerce; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.collections.QuorumCheck; +import org.apache.brooklyn.util.collections.QuorumCheck.QuorumChecks; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.net.Cidr; +import org.apache.brooklyn.util.net.Networking; +import org.apache.brooklyn.util.net.UserAndHostAndPort; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.time.Time; +import org.apache.brooklyn.util.yaml.Yamls; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.net.HostAndPort; + +public class CommonAdaptorTypeCoercions { + + private final TypeCoercerExtensible coercer; + + public CommonAdaptorTypeCoercions(TypeCoercerExtensible coercer) { + this.coercer = coercer; + } + + public TypeCoercerExtensible getCoercer() { + return coercer; + } + + public CommonAdaptorTypeCoercions registerAllAdapters() { + registerStandardAdapters(); + registerRecursiveIterableAdapters(); + registerClassForNameAdapters(); + registerCollectionJsonAdapters(); + + return this; + } + + /** Registers an adapter for use with type coercion. Returns any old adapter registered for this pair. */ + public synchronized Function registerAdapter(Class sourceType, Class targetType, Function fn) { + return coercer.registerAdapter(sourceType, targetType, fn); + } + + @SuppressWarnings("rawtypes") + public void registerStandardAdapters() { + registerAdapter(CharSequence.class, String.class, new Function() { + @Override + public String apply(CharSequence input) { + return input.toString(); + } + }); + registerAdapter(byte[].class, String.class, new Function() { + @Override + public String apply(byte[] input) { + return new String(input); + } + }); + registerAdapter(Collection.class, Set.class, new Function() { + @SuppressWarnings("unchecked") + @Override + public Set apply(Collection input) { + return Sets.newLinkedHashSet(input); + } + }); + registerAdapter(Collection.class, List.class, new Function() { + @SuppressWarnings("unchecked") + @Override + public List apply(Collection input) { + return Lists.newArrayList(input); + } + }); + registerAdapter(String.class, InetAddress.class, new Function() { + @Override + public InetAddress apply(String input) { + return Networking.getInetAddressWithFixedName(input); + } + }); + registerAdapter(String.class, HostAndPort.class, new Function() { + @Override + public HostAndPort apply(String input) { + return HostAndPort.fromString(input); + } + }); + registerAdapter(String.class, UserAndHostAndPort.class, new Function() { + @Override + public UserAndHostAndPort apply(String input) { + return UserAndHostAndPort.fromString(input); + } + }); + registerAdapter(String.class, Cidr.class, new Function() { + @Override + public Cidr apply(String input) { + return new Cidr(input); + } + }); + registerAdapter(String.class, URL.class, new Function() { + @Override + public URL apply(String input) { + try { + return new URL(input); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + }); + registerAdapter(URL.class, String.class, new Function() { + @Override + public String apply(URL input) { + return input.toString(); + } + }); + registerAdapter(String.class, URI.class, new Function() { + @Override + public URI apply(String input) { + return URI.create(input); + } + }); + registerAdapter(URI.class, String.class, new Function() { + @Override + public String apply(URI input) { + return input.toString(); + } + }); + registerAdapter(Object.class, Duration.class, new Function() { + @Override + public Duration apply(final Object input) { + return org.apache.brooklyn.util.time.Duration.of(input); + } + }); + + registerAdapter(Integer.class, AtomicLong.class, new Function() { + @Override public AtomicLong apply(final Integer input) { + return new AtomicLong(input); + } + }); + registerAdapter(Long.class, AtomicLong.class, new Function() { + @Override public AtomicLong apply(final Long input) { + return new AtomicLong(input); + } + }); + registerAdapter(String.class, AtomicLong.class, new Function() { + @Override public AtomicLong apply(final String input) { + return new AtomicLong(Long.parseLong(input.trim())); + } + }); + registerAdapter(Integer.class, AtomicInteger.class, new Function() { + @Override public AtomicInteger apply(final Integer input) { + return new AtomicInteger(input); + } + }); + registerAdapter(String.class, AtomicInteger.class, new Function() { + @Override public AtomicInteger apply(final String input) { + return new AtomicInteger(Integer.parseInt(input.trim())); + } + }); + /** This always returns a {@link Double}, cast as a {@link Number}; + * however primitives and boxers get exact typing due to call in #stringToPrimitive */ + registerAdapter(String.class, Number.class, new Function() { + @Override + public Number apply(String input) { + return Double.valueOf(input); + } + }); + registerAdapter(BigDecimal.class, Double.class, new Function() { + @Override + public Double apply(BigDecimal input) { + return input.doubleValue(); + } + }); + registerAdapter(BigInteger.class, Long.class, new Function() { + @Override + public Long apply(BigInteger input) { + return input.longValue(); + } + }); + registerAdapter(BigInteger.class, Integer.class, new Function() { + @Override + public Integer apply(BigInteger input) { + return input.intValue(); + } + }); + registerAdapter(String.class, BigDecimal.class, new Function() { + @Override + public BigDecimal apply(String input) { + return new BigDecimal(input); + } + }); + registerAdapter(Double.class, BigDecimal.class, new Function() { + @Override + public BigDecimal apply(Double input) { + return BigDecimal.valueOf(input); + } + }); + registerAdapter(String.class, BigInteger.class, new Function() { + @Override + public BigInteger apply(String input) { + return new BigInteger(input); + } + }); + registerAdapter(Long.class, BigInteger.class, new Function() { + @Override + public BigInteger apply(Long input) { + return BigInteger.valueOf(input); + } + }); + registerAdapter(Integer.class, BigInteger.class, new Function() { + @Override + public BigInteger apply(Integer input) { + return BigInteger.valueOf(input); + } + }); + registerAdapter(String.class, Date.class, new Function() { + @Override + public Date apply(final String input) { + return Time.parseDate(input); + } + }); + registerAdapter(String.class, QuorumCheck.class, new Function() { + @Override + public QuorumCheck apply(final String input) { + return QuorumChecks.of(input); + } + }); + } + + @SuppressWarnings("rawtypes") + public void registerRecursiveIterableAdapters() { + + // these refer to the coercer to recursively coerce; + // they throw if there are errors (but the registry apply loop will catch and handle), + // as currently the registry does not support Maybe or opting-out + + registerAdapter(Iterable.class, String[].class, new Function() { + @Nullable + @Override + public String[] apply(@Nullable Iterable list) { + if (list == null) return null; + String[] result = new String[Iterables.size(list)]; + int count = 0; + for (Object element : list) { + result[count++] = coercer.coerce(element, String.class); + } + return result; + } + }); + registerAdapter(Iterable.class, Integer[].class, new Function() { + @Nullable + @Override + public Integer[] apply(@Nullable Iterable list) { + if (list == null) return null; + Integer[] result = new Integer[Iterables.size(list)]; + int count = 0; + for (Object element : list) { + result[count++] = coercer.coerce(element, Integer.class); + } + return result; + } + }); + registerAdapter(Iterable.class, int[].class, new Function() { + @Nullable + @Override + public int[] apply(@Nullable Iterable list) { + if (list == null) return null; + int[] result = new int[Iterables.size(list)]; + int count = 0; + for (Object element : list) { + result[count++] = coercer.coerce(element, int.class); + } + return result; + } + }); + } + + @SuppressWarnings("rawtypes") + public void registerClassForNameAdapters() { + registerAdapter(String.class, Class.class, new Function() { + @Override + public Class apply(final String input) { + try { + return Class.forName(input); + } catch (ClassNotFoundException e) { + throw Exceptions.propagate(e); + } + } + }); + } + + @SuppressWarnings("rawtypes") + public void registerCollectionJsonAdapters() { + registerAdapter(String.class, List.class, new Function() { + @Override + public List apply(final String input) { + return JavaStringEscapes.unwrapJsonishListIfPossible(input); + } + }); + registerAdapter(String.class, Set.class, new Function() { + @Override + public Set apply(final String input) { + return MutableSet.copyOf(JavaStringEscapes.unwrapJsonishListIfPossible(input)).asUnmodifiable(); + } + }); + registerAdapter(String.class, Map.class, new Function() { + @Override + public Map apply(final String input) { + Exception error = null; + + // first try wrapping in braces if needed + if (!input.trim().startsWith("{")) { + try { + return apply("{ "+input+" }"); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + // prefer this error + error = e; + // fall back to parsing without braces, e.g. if it's multiline + } + } + + try { + return Yamls.getAs( Yamls.parseAll(input), Map.class ); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (error!=null && input.indexOf('\n')==-1) { + // prefer the original error if it wasn't braced and wasn't multiline + e = error; + } + throw new IllegalArgumentException("Cannot parse string as map with flexible YAML parsing; "+ + (e instanceof ClassCastException ? "yaml treats it as a string" : + (e instanceof IllegalArgumentException && Strings.isNonEmpty(e.getMessage())) ? e.getMessage() : + ""+e) ); + } + + // NB: previously we supported this also, when we did json above; + // yaml support is better as it supports quotes (and better than json because it allows dropping quotes) + // snake-yaml, our parser, also accepts key=value -- although i'm not sure this is strictly yaml compliant; + // our tests will catch it if snake behaviour changes, and we can reinstate this + // (but note it doesn't do quotes; see http://code.google.com/p/guava-libraries/issues/detail?id=412 for that): +// return ImmutableMap.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().withKeyValueSeparator("=").split(input)); + } + }); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/EnumTypeCoercions.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/EnumTypeCoercions.java new file mode 100644 index 0000000000..b57625a145 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/EnumTypeCoercions.java @@ -0,0 +1,104 @@ +/* + * 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.javalang.coerce; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.guava.Functionals; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Enums; +import org.apache.brooklyn.util.javalang.JavaClassNames; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; + +public class EnumTypeCoercions { + + /** + * Type coercion {@link Function function} for {@link Enum enums}. + *

+ * Tries to convert the string to {@link CaseFormat#UPPER_UNDERSCORE} first, + * handling all of the different {@link CaseFormat format} possibilites. Failing + * that, it tries a case-insensitive comparison with the valid enum values. + *

+ * Returns {@code defaultValue} if the string cannot be converted. + * + * @see TypeCoercions#coerce(Object, Class) + * @see Enum#valueOf(Class, String) + */ + public static > Function stringToEnum(final Class type, @Nullable final E defaultValue) { + return new StringToEnumFunction(type, defaultValue); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Function stringToEnumUntyped(final Class type, @Nullable final T defaultValue) { + if (!type.isEnum()) return new Functionals.ConstantFunction(null); + return (Function) new StringToEnumFunction((Class)type, (Enum)defaultValue); + } + + private static class StringToEnumFunction> implements Function { + private final Class type; + private final E defaultValue; + + public StringToEnumFunction(Class type, @Nullable E defaultValue) { + this.type = type; + this.defaultValue = defaultValue; + } + @Override + public E apply(String input) { + return tryCoerce(input, type).or(defaultValue); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Maybe tryCoerceUntyped(String input, Class targetType) { + if (input==null) return null; + if (targetType==null) return Maybe.absent("Null enum type"); + if (!targetType.isEnum()) return Maybe.absent("Type '"+targetType+"' is not an enum"); + return tryCoerce(input, (Class)targetType); + } + + public static > Maybe tryCoerce(String input, Class targetType) { + if (input==null) return null; + if (targetType==null) return Maybe.absent("Null enum type"); + if (!targetType.isEnum()) return Maybe.absent("Type '"+targetType+"' is not an enum"); + + List options = ImmutableList.of( + input, + CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, input), + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, input), + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, input), + CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, input)); + for (String value : options) { + try { + return Maybe.of(Enum.valueOf(targetType, value)); + } catch (IllegalArgumentException iae) { + continue; + } + } + Maybe result = Enums.valueOfIgnoreCase(targetType, input); + if (result.isPresent()) return result; + return Maybe.absent(new ClassCoercionException("Invalid value '"+input+"' for "+JavaClassNames.simpleClassName(targetType)+"; expected one of "+ + Arrays.asList(Enums.values(targetType)))); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java new file mode 100644 index 0000000000..cad798d7ad --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java @@ -0,0 +1,208 @@ +/* + * 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.javalang.coerce; + +import java.lang.reflect.Method; + +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; + +import com.google.common.primitives.Primitives; + +public class PrimitiveStringTypeCoercions { + + public PrimitiveStringTypeCoercions() {} + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Maybe tryCoerce(Object value, Class targetType) { + //deal with primitive->primitive casting + if (isPrimitiveOrBoxer(targetType) && isPrimitiveOrBoxer(value.getClass())) { + // Don't just rely on Java to do its normal casting later; if caller writes + // long `l = coerce(new Integer(1), Long.class)` then letting java do its casting will fail, + // because an Integer will not automatically be unboxed and cast to a long + return Maybe.of(castPrimitive(value, (Class)targetType)); + } + + //deal with string->primitive + if (value instanceof String && isPrimitiveOrBoxer(targetType)) { + return Maybe.of(stringToPrimitive((String)value, (Class)targetType)); + } + + //deal with primitive->string + if (isPrimitiveOrBoxer(value.getClass()) && targetType.equals(String.class)) { + return Maybe.of((T) value.toString()); + } + + //look for value.asType where Type is castable to targetType + String targetTypeSimpleName = JavaClassNames.verySimpleClassName(targetType); + if (targetTypeSimpleName!=null && targetTypeSimpleName.length()>0) { + for (Method m: value.getClass().getMethods()) { + if (m.getName().startsWith("as") && m.getParameterTypes().length==0 && + targetType.isAssignableFrom(m.getReturnType()) ) { + if (m.getName().equals("as"+JavaClassNames.verySimpleClassName(m.getReturnType()))) { + try { + return Maybe.of((T) m.invoke(value)); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed, "+e)); + } + } + } + } + } + + return null; + } + + /** + * Sometimes need to explicitly cast primitives, rather than relying on Java casting. + * For example, when using generics then type-erasure means it doesn't actually cast, + * which causes tests to fail with 0 != 0.0 + */ + @SuppressWarnings("unchecked") + public static T castPrimitive(Object value, Class targetType) { + if (value==null) return null; + assert isPrimitiveOrBoxer(targetType) : "targetType="+targetType; + assert isPrimitiveOrBoxer(value.getClass()) : "value="+targetType+"; valueType="+value.getClass(); + + Class sourceWrapType = Primitives.wrap(value.getClass()); + Class targetWrapType = Primitives.wrap(targetType); + + // optimization, for when already correct type + if (sourceWrapType == targetWrapType) { + return (T) value; + } + + if (targetWrapType == Boolean.class) { + // only char can be mapped to boolean + // (we could say 0=false, nonzero=true, but there is no compelling use case so better + // to encourage users to write as boolean) + if (sourceWrapType == Character.class) + return (T) stringToPrimitive(value.toString(), targetType); + + throw new ClassCoercionException("Cannot cast "+sourceWrapType+" ("+value+") to "+targetType); + } else if (sourceWrapType == Boolean.class) { + // boolean can't cast to anything else + + throw new ClassCoercionException("Cannot cast "+sourceWrapType+" ("+value+") to "+targetType); + } + + // for whole-numbers (where casting to long won't lose anything)... + long v = 0; + boolean islong = true; + if (sourceWrapType == Character.class) { + v = (long) ((Character)value).charValue(); + } else if (sourceWrapType == Byte.class) { + v = (long) ((Byte)value).byteValue(); + } else if (sourceWrapType == Short.class) { + v = (long) ((Short)value).shortValue(); + } else if (sourceWrapType == Integer.class) { + v = (long) ((Integer)value).intValue(); + } else if (sourceWrapType == Long.class) { + v = ((Long)value).longValue(); + } else { + islong = false; + } + if (islong) { + if (targetWrapType == Character.class) return (T) Character.valueOf((char)v); + if (targetWrapType == Byte.class) return (T) Byte.valueOf((byte)v); + if (targetWrapType == Short.class) return (T) Short.valueOf((short)v); + if (targetWrapType == Integer.class) return (T) Integer.valueOf((int)v); + if (targetWrapType == Long.class) return (T) Long.valueOf((long)v); + if (targetWrapType == Float.class) return (T) Float.valueOf((float)v); + if (targetWrapType == Double.class) return (T) Double.valueOf((double)v); + throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType); + } + + // for real-numbers (cast to double)... + double d = 0; + boolean isdouble = true; + if (sourceWrapType == Float.class) { + d = (double) ((Float)value).floatValue(); + } else if (sourceWrapType == Double.class) { + d = (double) ((Double)value).doubleValue(); + } else { + isdouble = false; + } + if (isdouble) { + if (targetWrapType == Character.class) return (T) Character.valueOf((char)d); + if (targetWrapType == Byte.class) return (T) Byte.valueOf((byte)d); + if (targetWrapType == Short.class) return (T) Short.valueOf((short)d); + if (targetWrapType == Integer.class) return (T) Integer.valueOf((int)d); + if (targetWrapType == Long.class) return (T) Long.valueOf((long)d); + if (targetWrapType == Float.class) return (T) Float.valueOf((float)d); + if (targetWrapType == Double.class) return (T) Double.valueOf((double)d); + throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType); + } else { + throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType); + } + } + + public static boolean isPrimitiveOrBoxer(Class type) { + // cf Boxing.isPrimitiveOrBoxerClass + return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type); + } + + @SuppressWarnings("unchecked") + public static T stringToPrimitive(String value, Class targetType) { + assert Primitives.allPrimitiveTypes().contains(targetType) || Primitives.allWrapperTypes().contains(targetType) : "targetType="+targetType; + // If char, then need to do explicit conversion + if (targetType == Character.class || targetType == char.class) { + if (value.length() == 1) { + return (T) (Character) value.charAt(0); + } else if (value.length() != 1) { + throw new ClassCoercionException("Cannot coerce type String to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); + } + } + value = value.trim(); + // For boolean we could use valueOf, but that returns false whereas we'd rather throw errors on bad values + if (targetType == Boolean.class || targetType == boolean.class) { + if ("true".equalsIgnoreCase(value)) return (T) Boolean.TRUE; + if ("false".equalsIgnoreCase(value)) return (T) Boolean.FALSE; + if ("yes".equalsIgnoreCase(value)) return (T) Boolean.TRUE; + if ("no".equalsIgnoreCase(value)) return (T) Boolean.FALSE; + if ("t".equalsIgnoreCase(value)) return (T) Boolean.TRUE; + if ("f".equalsIgnoreCase(value)) return (T) Boolean.FALSE; + if ("y".equalsIgnoreCase(value)) return (T) Boolean.TRUE; + if ("n".equalsIgnoreCase(value)) return (T) Boolean.FALSE; + + throw new ClassCoercionException("Cannot coerce type String to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); + } + + // Otherwise can use valueOf reflectively + Class wrappedType; + if (Primitives.allPrimitiveTypes().contains(targetType)) { + wrappedType = Primitives.wrap(targetType); + } else { + wrappedType = targetType; + } + + try { + return (T) wrappedType.getMethod("valueOf", String.class).invoke(null, value); + } catch (Exception e) { + ClassCoercionException tothrow = new ClassCoercionException("Cannot coerce "+JavaStringEscapes.wrapJavaString(value)+" to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); + tothrow.initCause(e); + throw tothrow; + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java new file mode 100644 index 0000000000..bdac81ca7e --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java @@ -0,0 +1,31 @@ +/* + * 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.javalang.coerce; + +import org.apache.brooklyn.util.guava.Maybe; + +import com.google.common.reflect.TypeToken; + +public interface TypeCoercer { + + T coerce(Object input, Class type); + Maybe tryCoerce(Object input, Class type); + Maybe tryCoerce(Object input, TypeToken type); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java new file mode 100644 index 0000000000..eb17b044df --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java @@ -0,0 +1,296 @@ +/* + * 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.javalang.coerce; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.time.Time; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.collect.Table; +import com.google.common.reflect.TypeToken; + +/** + * Attempts to coerce {@code value} to {@code targetType}. + *

+ * Maintains a registry of adapter functions for type pairs in a {@link Table} which + * is searched after checking various strategies, including the following: + *

    + *
  • {@code value.asTargetType()} + *
  • {@code TargetType.fromType(value)} (if {@code value instanceof Type}) + *
  • {@code value.targetTypeValue()} (handy for primitives) + *
  • {@code TargetType.valueOf(value)} (for enums) + *
+ *

+ * A default set of adapters will handle most common Java-type coercions + * as well as String coercion to: + *

    + *
  • {@link Set}, {@link List}, {@link Map} and similar -- parses as YAML + *
  • {@link Date} -- parses using {@link Time#parseDate(String)} + *
  • {@link Duration} -- parses using {@link Duration#parse(String)} + *
+ */ +public class TypeCoercerExtensible implements TypeCoercer { + + private static final Logger log = LoggerFactory.getLogger(TypeCoercerExtensible.class); + + protected TypeCoercerExtensible() {} + + /** has all the strategies (primitives, collections, etc) + * and all the adapters from {@link CommonAdaptorTypeCoercions} */ + public static TypeCoercerExtensible newDefault() { + return new CommonAdaptorTypeCoercions(newEmpty()).registerAllAdapters().getCoercer(); + } + + /** has all the strategies (primitives, collections, etc) but no adapters, + * so caller can pick and choose e.g. from {@link CommonAdaptorTypeCoercions} */ + public static TypeCoercerExtensible newEmpty() { + return new TypeCoercerExtensible(); + } + + /** Store the coercion {@link Function functions} in a {@link Table table}. */ + private Table, Class, Function> registry = HashBasedTable.create(); + + @Override + public T coerce(Object value, Class targetType) { + return coerce(value, TypeToken.of(targetType)); + } + + public T coerce(Object value, TypeToken targetTypeToken) { + return tryCoerce(value, targetTypeToken).get(); + } + + @Override + public Maybe tryCoerce(Object input, Class type) { + return tryCoerce(input, TypeToken.of(type)); + } + + @Override + public Maybe tryCoerce(Object value, TypeToken targetTypeToken) { + Maybe result = tryCoerceInternal(value, targetTypeToken); + return Maybe.Absent.changeExceptionSupplier(result, ClassCoercionException.class); + } + + @SuppressWarnings("unchecked") + protected Maybe tryCoerceInternal(Object value, TypeToken targetTypeToken) { + if (value==null) return Maybe.of((T)null); + Class targetType = targetTypeToken.getRawType(); + Maybe result = null; + Maybe firstError = null; + + //recursive coercion of parameterized collections and map entries + if (targetTypeToken.getType() instanceof ParameterizedType) { + if (value instanceof Collection && Collection.class.isAssignableFrom(targetType)) { + result = tryCoerceCollection(value, targetTypeToken, targetType); + } else if (value instanceof Map && Map.class.isAssignableFrom(targetType)) { + result = tryCoerceMap(value, targetTypeToken); + } + } + if (result!=null && result.isPresent()) return result; + if (result!=null && firstError==null) firstError = result; + + if (targetType.isInstance(value)) return Maybe.of( (T) value ); + + result = PrimitiveStringTypeCoercions.tryCoerce(value, targetType); + if (result!=null && result.isPresent()) return result; + if (result!=null && firstError==null) firstError = result; + + result = tryCoerceWithFromMethod(value, targetType); + if (result!=null && result.isPresent()) return result; + if (result!=null && firstError==null) firstError = result; + + //ENHANCEMENT could look in type hierarchy of both types for a conversion method... + + //at this point, if either is primitive then run instead over boxed types + Class boxedT = Boxing.PRIMITIVE_TO_BOXED.get(targetType); + Class boxedVT = Boxing.PRIMITIVE_TO_BOXED.get(value.getClass()); + if (boxedT!=null || boxedVT!=null) { + try { + if (boxedT==null) boxedT=targetType; + Object boxedV = boxedVT==null ? value : boxedVT.getConstructor(value.getClass()).newInstance(value); + return tryCoerce(boxedV, (Class)boxedT); + } catch (Exception e) { + return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): unboxing failed", e)); + } + } + + //for enums call valueOf with the string representation of the value + if (targetType.isEnum()) { + result = EnumTypeCoercions.tryCoerceUntyped(Strings.toString(value), (Class)targetType); + if (result!=null && result.isPresent()) return result; + if (result!=null && firstError==null) firstError = result; + } + + //now look in registry + synchronized (registry) { + Map, Function> adapters = registry.row(targetType); + for (Map.Entry, Function> entry : adapters.entrySet()) { + if (entry.getKey().isInstance(value)) { + try { + T resultT = ((Function)entry.getValue()).apply(value); + + // Check if need to unwrap again (e.g. if want List and are given a String "1,2,3" + // then we'll have so far converted to List.of("1", "2", "3"). Call recursively. + // First check that value has changed, to avoid stack overflow! + if (!Objects.equal(value, resultT) && targetTypeToken.getType() instanceof ParameterizedType) { + // Could duplicate check for `result instanceof Collection` etc; but recursive call + // will be fine as if that doesn't match we'll safely reach `targetType.isInstance(value)` + // and just return the result. + return tryCoerce(resultT, targetTypeToken); + } + return Maybe.of(resultT); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (log.isDebugEnabled()) { + log.debug("When coercing, registry adapter "+entry+" gave error on "+value+" -> "+targetType+" " + + (firstError==null ? "(rethrowing)" : "(suppressing as there is already an error)") + + ": "+e, e); + } + if (firstError==null) { + if (e instanceof ClassCoercionException) firstError = Maybe.absent(e); + else firstError = Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetType.getCanonicalName()+" ("+value+")", e)); + } + continue; + } + } + } + } + + //not found + if (firstError!=null) return firstError; + return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetType.getCanonicalName()+" ("+value+"): no adapter known")); + } + + @SuppressWarnings("unchecked") + protected Maybe tryCoerceWithFromMethod(Object value, Class targetType) { + //now look for static TargetType.fromType(Type t) where value instanceof Type + for (Method m: targetType.getMethods()) { + if (((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC) && + m.getName().startsWith("from") && m.getParameterTypes().length==1 && + m.getParameterTypes()[0].isInstance(value)) { + if (m.getName().equals("from"+JavaClassNames.verySimpleClassName(m.getParameterTypes()[0]))) { + try { + return Maybe.of((T) m.invoke(null, value)); + } catch (Exception e) { + Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed, "+e)); + } + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + protected Maybe tryCoerceMap(Object value, TypeToken targetTypeToken) { + if (!(value instanceof Map) || !(Map.class.isAssignableFrom(targetTypeToken.getRawType()))) return null; + Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments(); + if (arguments.length != 2) { + throw new IllegalStateException("Unexpected number of parameters in map type: " + arguments); + } + Map coerced = Maps.newLinkedHashMap(); + TypeToken mapKeyType = TypeToken.of(arguments[0]); + TypeToken mapValueType = TypeToken.of(arguments[1]); + int i=0; + for (Map.Entry entry : ((Map) value).entrySet()) { + Maybe k = tryCoerce(entry.getKey(), mapKeyType); + if (k.isAbsent()) return Maybe.absent(new ClassCoercionException( + "Could not coerce key of entry "+i+" in "+value+" to "+targetTypeToken, + ((Maybe.Absent)k).getException())); + + Maybe v = tryCoerce(entry.getValue(), mapValueType); + if (v.isAbsent()) return Maybe.absent(new ClassCoercionException( + "Could not coerce value of entry "+i+" in "+value+" to "+targetTypeToken, + ((Maybe.Absent)v).getException())); + + coerced.put(k.get(), v.get()); + + i++; + } + return Maybe.of((T) Maps.newLinkedHashMap(coerced)); + } + + /** tries to coerce a list; + * returns null if it just doesn't apply, a {@link Maybe.Present} if it succeeded, + * or {@link Maybe.Absent} with a good exception if it should have applied but couldn't */ + @SuppressWarnings("unchecked") + protected Maybe tryCoerceCollection(Object value, TypeToken targetTypeToken, Class targetType) { + if (!(value instanceof Iterable) || !(Iterable.class.isAssignableFrom(targetTypeToken.getRawType()))) return null; + Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments(); + if (arguments.length != 1) { + return Maybe.absent(new IllegalStateException("Unexpected number of parameters in collection type: " + arguments)); + } + Collection coerced = Lists.newLinkedList(); + TypeToken listEntryType = TypeToken.of(arguments[0]); + int i = 0; + for (Object entry : (Iterable) value) { + Maybe entryCoerced = tryCoerce(entry, listEntryType); + if (entryCoerced.isPresent()) { + coerced.add(entryCoerced.get()); + } else { + return Maybe.absent(new ClassCoercionException( + "Could not coerce entry "+i+" in "+value+" to "+targetTypeToken, + ((Maybe.Absent)entryCoerced).getException())); + } + i++; + } + if (Set.class.isAssignableFrom(targetType)) { + return Maybe.of((T) Sets.newLinkedHashSet(coerced)); + } else { + return Maybe.of((T) Lists.newArrayList(coerced)); + } + } + + /** + * Returns a function that does a type coercion to the given type. For example, + * {@code TypeCoercions.function(Double.class)} will return a function that will + * coerce its input value to a {@link Double} (or throw a {@link ClassCoercionException} + * if that is not possible). + */ + public Function function(final Class type) { + return new CoerceFunctionals.CoerceFunction(this, type); + } + + /** Registers an adapter for use with type coercion. Returns any old adapter registered for this pair. */ + @SuppressWarnings("unchecked") + public synchronized Function registerAdapter(Class sourceType, Class targetType, Function fn) { + return (Function) registry.put(targetType, sourceType, fn); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java new file mode 100644 index 0000000000..786d9e8f43 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java @@ -0,0 +1,379 @@ +/* + * 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.javalang.coerce; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.text.StringPredicates; +import org.codehaus.groovy.runtime.GStringImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; + +public class TypeCoercionsTest { + + private static final Logger log = LoggerFactory.getLogger(TypeCoercionsTest.class); + + TypeCoercerExtensible coercer = TypeCoercerExtensible.newDefault(); + + protected T coerce(Object x, Class type) { + return coercer.coerce(x, type); + } + protected T coerce(Object x, TypeToken type) { + return coercer.coerce(x, type); + } + + @Test + public void testCoerceCharSequenceToString() { + assertEquals(coerce(new StringBuilder("abc"), String.class), "abc"); + assertEquals(coerce(new GStringImpl(new Object[0], new String[0]), String.class), ""); + } + + @Test + public void testCoerceStringToPrimitive() { + assertEquals(coerce("1", Character.class), (Character)'1'); + assertEquals(coerce(" ", Character.class), (Character)' '); + assertEquals(coerce("1", Short.class), (Short)((short)1)); + assertEquals(coerce("1", Integer.class), (Integer)1); + assertEquals(coerce("1", Long.class), (Long)1l); + assertEquals(coerce("1", Float.class), (Float)1f); + assertEquals(coerce("1", Double.class), (Double)1d); + assertEquals(coerce("true", Boolean.class), (Boolean)true); + assertEquals(coerce("False", Boolean.class), (Boolean)false); + assertEquals(coerce("true ", Boolean.class), (Boolean)true); + assertNull(coerce(null, Boolean.class), null); + + assertEquals(coerce("1", char.class), (Character)'1'); + assertEquals(coerce("1", short.class), (Short)((short)1)); + assertEquals(coerce("1", int.class), (Integer)1); + assertEquals(coerce("1", long.class), (Long)1l); + assertEquals(coerce("1", float.class), (Float)1f); + assertEquals(coerce("1", double.class), (Double)1d); + assertEquals(coerce("TRUE", boolean.class), (Boolean)true); + assertEquals(coerce("false", boolean.class), (Boolean)false); + } + + @Test + public void testCoercePrimitivesToSameType() { + assertEquals(coerce('1', Character.class), (Character)'1'); + assertEquals(coerce((short)1, Short.class), (Short)((short)1)); + assertEquals(coerce(1, Integer.class), (Integer)1); + assertEquals(coerce(1l, Long.class), (Long)1l); + assertEquals(coerce(1f, Float.class), (Float)1f); + assertEquals(coerce(1d, Double.class), (Double)1d); + assertEquals(coerce(true, Boolean.class), (Boolean)true); + } + + @Test + public void testCastPrimitives() { + assertEquals(coerce(1L, Character.class), (Character)(char)1); + assertEquals(coerce(1L, Byte.class), (Byte)(byte)1); + assertEquals(coerce(1L, Short.class), (Short)(short)1); + assertEquals(coerce(1L, Integer.class), (Integer)1); + assertEquals(coerce(1L, Long.class), (Long)(long)1); + assertEquals(coerce(1L, Float.class), (Float)(float)1); + assertEquals(coerce(1L, Double.class), (Double)(double)1); + + assertEquals(coerce(1L, char.class), (Character)(char)1); + assertEquals(coerce(1L, byte.class), (Byte)(byte)1); + assertEquals(coerce(1L, short.class), (Short)(short)1); + assertEquals(coerce(1L, int.class), (Integer)1); + assertEquals(coerce(1L, long.class), (Long)(long)1); + assertEquals(coerce(1L, float.class), (Float)(float)1); + assertEquals(coerce(1L, double.class), (Double)(double)1); + + assertEquals(coerce((char)1, Integer.class), (Integer)1); + assertEquals(coerce((byte)1, Integer.class), (Integer)1); + assertEquals(coerce((short)1, Integer.class), (Integer)1); + assertEquals(coerce((int)1, Integer.class), (Integer)1); + assertEquals(coerce((long)1, Integer.class), (Integer)1); + assertEquals(coerce((float)1, Integer.class), (Integer)1); + assertEquals(coerce((double)1, Integer.class), (Integer)1); + } + + @Test + public void testCoercePrimitiveFailures() { + // error messages don't have to be this exactly, but they should include sufficient information... + assertCoercionFailsWithErrorMatching("maybe", boolean.class, StringPredicates.containsAllLiterals("String", "boolean", "maybe")); + assertCoercionFailsWithErrorMatching("NaN", int.class, StringPredicates.containsAllLiterals("int", "NaN")); + assertCoercionFailsWithErrorMatching('c', boolean.class, StringPredicates.containsAllLiterals("boolean", "(c)")); // will say 'string' rather than 'char' + assertCoercionFailsWithErrorMatching(0, boolean.class, StringPredicates.containsAllLiterals("Integer", "boolean", "0")); + } + + protected void assertCoercionFailsWithErrorMatching(Object input, Class type, Predicate errorMessageRequirement) { + try { + Object result = coerce(input, type); + Assert.fail("Should have failed type coercion of "+input+" to "+type+", instead got: "+result); + } catch (Exception e) { + if (errorMessageRequirement==null || errorMessageRequirement.apply(e.toString())) + log.info("Primitive coercion failed as expected, with: "+e); + else + Assert.fail("Error from type coercion of "+input+" to "+type+" failed with wrong exception; expected match of "+errorMessageRequirement+" but got: "+e); + } + + } + + @Test + public void testCastToNumericPrimitives() { + assertEquals(coerce(BigInteger.ONE, Integer.class), (Integer)1); + assertEquals(coerce(BigInteger.ONE, int.class), (Integer)1); + assertEquals(coerce(BigInteger.valueOf(Long.MAX_VALUE), Long.class), (Long)Long.MAX_VALUE); + assertEquals(coerce(BigInteger.valueOf(Long.MAX_VALUE), long.class), (Long)Long.MAX_VALUE); + + assertEquals(coerce(BigDecimal.valueOf(0.5), Double.class), 0.5d, 0.00001d); + assertEquals(coerce(BigDecimal.valueOf(0.5), double.class), 0.5d, 0.00001d); + } + + @Test + public void testCoerceStringToBigNumber() { + assertEquals(coerce("0.5", BigDecimal.class), BigDecimal.valueOf(0.5)); + assertEquals(coerce("1", BigInteger.class), BigInteger.valueOf(1)); + } + + @Test + public void testCoerceStringToEnum() { + assertEquals(coerce("LOWERCASE", PerverseEnum.class), PerverseEnum.lowercase); + assertEquals(coerce("CAMELCASE", PerverseEnum.class), PerverseEnum.camelCase); + assertEquals(coerce("upper", PerverseEnum.class), PerverseEnum.UPPER); + assertEquals(coerce("upper_with_underscore", PerverseEnum.class), PerverseEnum.UPPER_WITH_UNDERSCORE); + assertEquals(coerce("LOWER_WITH_UNDERSCORE", PerverseEnum.class), PerverseEnum.lower_with_underscore); + } + public static enum PerverseEnum { + lowercase, + camelCase, + UPPER, + UPPER_WITH_UNDERSCORE, + lower_with_underscore; + } + + @Test + public void testListToSetCoercion() { + Set s = coerce(ImmutableList.of(1), Set.class); + Assert.assertEquals(s, ImmutableSet.of(1)); + } + + @Test + public void testSetToListCoercion() { + List s = coerce(ImmutableSet.of(1), List.class); + Assert.assertEquals(s, ImmutableList.of(1)); + } + + @Test + public void testIterableToArrayCoercion() { + String[] s = coerce(ImmutableList.of("a", "b"), String[].class); + Assert.assertTrue(Arrays.equals(s, new String[] {"a", "b"}), "result="+Arrays.toString(s)); + + Integer[] i = coerce(ImmutableList.of(1, 2), Integer[].class); + Assert.assertTrue(Arrays.equals(i, new Integer[] {1, 2}), "result="+Arrays.toString(i)); + + int[] i2 = coerce(ImmutableList.of(1, 2), int[].class); + Assert.assertTrue(Arrays.equals(i2, new int[] {1, 2}), "result="+Arrays.toString(i2)); + + int[] i3 = coerce(MutableSet.of("1", 2), int[].class); + Assert.assertTrue(Arrays.equals(i3, new int[] {1, 2}), "result="+Arrays.toString(i3)); + } + + @Test + public void testListEntryCoercion() { + @SuppressWarnings("serial") + List s = coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken>>() { }); + Assert.assertEquals(s, ImmutableList.of(Integer.class, Double.class)); + } + + @Test + public void testListEntryToSetCoercion() { + @SuppressWarnings("serial") + Set s = coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken>>() { }); + Assert.assertEquals(s, ImmutableSet.of(Integer.class, Double.class)); + } + + @Test + public void testListEntryToCollectionCoercion() { + @SuppressWarnings("serial") + Collection s = coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken>>() { }); + Assert.assertEquals(s, ImmutableList.of(Integer.class, Double.class)); + } + + @Test + public void testMapValueCoercion() { + @SuppressWarnings("serial") + Map s = coerce(ImmutableMap.of("int", "java.lang.Integer", "double", "java.lang.Double"), new TypeToken>>() { }); + Assert.assertEquals(s, ImmutableMap.of("int", Integer.class, "double", Double.class)); + } + + @Test + public void testMapKeyCoercion() { + @SuppressWarnings("serial") + Map s = coerce(ImmutableMap.of("java.lang.Integer", "int", "java.lang.Double", "double"), new TypeToken, String>>() { }); + Assert.assertEquals(s, ImmutableMap.of(Integer.class, "int", Double.class, "double")); + } + + @Test + public void testStringToListCoercion() { + List s = coerce("a,b,c", List.class); + Assert.assertEquals(s, ImmutableList.of("a", "b", "c")); + } + + @Test + @SuppressWarnings("serial") + public void testCoerceRecursivelyStringToGenericsCollection() { + assertEquals(coerce("1,2", new TypeToken>() {}), ImmutableList.of(1, 2)); + } + + @Test + public void testJsonStringToMapCoercion() { + Map s = coerce("{ \"a\" : \"1\", b : 2 }", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2)); + } + + @Test + public void testJsonStringWithoutQuotesToMapCoercion() { + Map s = coerce("{ a : 1 }", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", 1)); + } + + @Test + public void testJsonComplexTypesToMapCoercion() { + Map s = coerce("{ a : [1, \"2\", '\"3\"'], b: { c: d, 'e': \"f\" } }", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", ImmutableList.of(1, "2", "\"3\""), + "b", ImmutableMap.of("c", "d", "e", "f"))); + } + + @Test + public void testJsonStringWithoutBracesToMapCoercion() { + Map s = coerce("a : 1", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", 1)); + } + + @Test + public void testJsonStringWithoutBracesWithMultipleToMapCoercion() { + Map s = coerce("a : 1, b : 2", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", 2)); + } + + @Test + public void testKeyEqualsValueStringToMapCoercion() { + Map s = coerce("a=1,b=2", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", "2")); + } + + @Test(expectedExceptions=ClassCoercionException.class) + public void testJsonStringWithoutBracesOrSpaceDisallowedAsMapCoercion() { + // yaml requires spaces after the colon + coerce("a:1,b:2", Map.class); + Asserts.shouldHaveFailedPreviously(); + } + + @Test + public void testEqualsInBracesMapCoercion() { + Map s = coerce("{ a = 1, b = '2' }", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", "2")); + } + + @Test + public void testKeyEqualsOrColonValueWithBracesStringToMapCoercion() { + Map s = coerce("{ a=1, b: 2 }", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2)); + } + + @Test + public void testKeyEqualsOrColonValueWithoutBracesStringToMapCoercion() { + Map s = coerce("a=1, b: 2", Map.class); + Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2)); + } + + @Test + public void testURItoStringCoercion() { + String s = coerce(URI.create("http://localhost:1234/"), String.class); + Assert.assertEquals(s, "http://localhost:1234/"); + } + + @Test + public void testURLtoStringCoercion() throws MalformedURLException { + String s = coerce(new URL("http://localhost:1234/"), String.class); + Assert.assertEquals(s, "http://localhost:1234/"); + } + + @Test + public void testAs() { + Integer x = coerce(new WithAs("3"), Integer.class); + Assert.assertEquals(x, (Integer)3); + } + + @Test + public void testFrom() { + WithFrom x = coerce("3", WithFrom.class); + Assert.assertEquals(x.value, 3); + } + + @Test + public void testCoerceStringToNumber() { + assertEquals(coerce("1", Number.class), (Number) Double.valueOf(1)); + assertEquals(coerce("1.0", Number.class), (Number) Double.valueOf(1.0)); + } + + @Test(expectedExceptions = org.apache.brooklyn.util.javalang.coerce.ClassCoercionException.class) + public void testInvalidCoercionThrowsClassCoercionException() { + coerce(new Object(), TypeToken.of(Integer.class)); + } + + @Test + public void testCoercionFunction() { + assertEquals(coercer.function(Double.class).apply("1"), Double.valueOf(1)); + } + + public static class WithAs { + String value; + public WithAs(Object x) { value = ""+x; } + public Integer asInteger() { + return Integer.parseInt(value); + } + } + + public static class WithFrom { + int value; + public static WithFrom fromString(String s) { + WithFrom result = new WithFrom(); + result.value = Integer.parseInt(s); + return result; + } + } + +} From b1999e3e26519965c99a4e3bc8c1b23b011e8195 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Wed, 29 Jun 2016 03:45:30 +0100 Subject: [PATCH 6/7] misc utils minor improvements mainly around findMethod --- .../org/apache/brooklyn/test/Asserts.java | 9 ++++ .../brooklyn/util/collections/MutableSet.java | 4 +- .../brooklyn/util/javalang/Reflections.java | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java index a544f0189b..43dcaa18a2 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java +++ b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java @@ -712,6 +712,9 @@ public static void assertTrue(boolean condition, String message) { public static void assertFalse(boolean condition, String message) { if (condition) fail(message); } + public static void assertFalse(boolean condition) { + if (condition) fail(); + } /** * Fails a test with the given message. @@ -720,6 +723,7 @@ public static void assertFalse(boolean condition, String message) { public static AssertionError fail(String message) { throw new AssertionError(message); } + public static AssertionError fail() { throw new AssertionError(); } public static void assertEqualsIgnoringOrder(Iterable actual, Iterable expected) { assertEqualsIgnoringOrder(actual, expected, false, null); @@ -1347,4 +1351,9 @@ public static void eventuallyOnNotify(T object, Predicate predicate) { eventuallyOnNotify(object, Suppliers.ofInstance(object), predicate, null); } + public static void assertSize(Iterable list, int expectedSize) { + if (list==null) fail("List is null"); + if (Iterables.size(list)!=expectedSize) fail("List has wrong size "+Iterables.size(list)+" (expected "+expectedSize+"): "+list); + } + } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableSet.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableSet.java index 70d8a9d8fe..bcadf33372 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableSet.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableSet.java @@ -55,7 +55,7 @@ public static MutableSet of(V v1, V v2) { return result; } - public static MutableSet of(V v1, V v2, V v3, V ...vMore) { + public static MutableSet of(V v1, V v2, V v3, @SuppressWarnings("unchecked") V ...vMore) { MutableSet result = new MutableSet(); result.add(v1); result.add(v2); @@ -128,7 +128,7 @@ public Builder add(V value) { return this; } - public Builder add(V v1, V v2, V ...values) { + public Builder add(V v1, V v2, @SuppressWarnings("unchecked") V ...values) { result.add(v1); result.add(v2); for (V value: values) result.add(value); diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java index 7b2cba4d42..37f3fe6f6c 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java @@ -583,6 +583,41 @@ public static Set> getInterfacesIncludingClassAncestors(Class clazz) return result; } + /** Returns any method exactly matching the given signature, including privates and on parent classes. */ + public static Maybe findMethodMaybe(Class clazz, String name, Class... parameterTypes) { + if (clazz == null || name == null) return Maybe.absentNoTrace("class or name is null"); + Iterable result = findMethods(false, clazz, name, parameterTypes); + if (!result.iterator().hasNext()) return Maybe.absentNoTrace("no methods matching "+clazz.getName()+"."+name+"("+Arrays.asList(parameterTypes)+")"); + return Maybe.of(result.iterator().next()); + } + /** Returns all methods compatible with the given argument types, including privates and on parent classes and where the method takes a supertype. */ + public static Iterable findMethodsCompatible(Class clazz, String name, Class... parameterTypes) { + return findMethods(true, clazz, name, parameterTypes); + } + private static Iterable findMethods(boolean allowCovariantParameterClasses, Class clazz, String name, Class... parameterTypes) { + if (clazz == null || name == null) { + return Collections.emptySet(); + } + List result = MutableList.of(); + Class clazzToInspect = clazz; + + while (clazzToInspect != null) { + methods: for (Method m: clazzToInspect.getDeclaredMethods()) { + if (!name.equals(m.getName())) continue methods; + if (m.getParameterTypes().length!=parameterTypes.length) continue methods; + parameters: for (int i=0; i clazz, String name, Class... parameterTypes) throws NoSuchMethodException { if (clazz == null || name == null) { throw new NullPointerException("Must not be null: clazz="+clazz+"; name="+name); @@ -951,4 +986,10 @@ public static String findMappedNameAndLog(Map renames, String na return name; } + public static boolean hasSpecialSerializationMethods(Class type) { + if (type==null) return false; + if (findMethodMaybe(type, "writeObject", java.io.ObjectOutputStream.class).isPresent()) return true; + return hasSpecialSerializationMethods(type.getSuperclass()); + } + } From 89b89c52e2157b85ccc535a291cb4682b0048f88 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Thu, 7 Jul 2016 10:17:25 +0100 Subject: [PATCH 7/7] make our `coercer` a `private static final` (code review) --- .../java/org/apache/brooklyn/util/core/flags/TypeCoercions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java index 3f64a4dc94..5eb9141a04 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java @@ -63,7 +63,7 @@ public class TypeCoercions { private TypeCoercions() {} - static TypeCoercerExtensible coercer; + private static final TypeCoercerExtensible coercer; static { coercer = TypeCoercerExtensible.newEmpty(); BrooklynInitialization.initTypeCoercionStandardAdapters();