TypeCoercions: support more generic coercers
Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/b945fdc0 Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/b945fdc0 Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/b945fdc0 Branch: refs/heads/master Commit: b945fdc08c41d1c3c944e9a41663d52902426baa Parents: 377454e Author: Aled Sage <[email protected]> Authored: Mon May 22 14:09:54 2017 +0100 Committer: Aled Sage <[email protected]> Committed: Tue May 23 14:45:33 2017 +0100 ---------------------------------------------------------------------- .../brooklyn/util/core/flags/TypeCoercions.java | 15 ++- .../coerce/CommonAdaptorTryCoercions.java | 110 +++++++++++++++++++ .../coerce/CommonAdaptorTypeCoercions.java | 6 +- .../util/javalang/coerce/TryCoercer.java | 45 ++++++++ .../javalang/coerce/TypeCoercerExtensible.java | 76 ++++--------- .../coerce/TypeCoercerExtensibleTest.java | 82 ++++++++++++++ 6 files changed, 269 insertions(+), 65 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java ---------------------------------------------------------------------- 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 9b39a56..9bc25e2 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 @@ -18,9 +18,6 @@ */ package org.apache.brooklyn.util.core.flags; -import groovy.lang.Closure; -import groovy.time.TimeDuration; - import java.lang.reflect.Constructor; import java.util.Map; @@ -40,19 +37,25 @@ 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.javalang.Reflections; +import org.apache.brooklyn.util.javalang.coerce.CommonAdaptorTryCoercions; 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.TryCoercer; 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.annotations.Beta; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.reflect.TypeToken; +import groovy.lang.Closure; +import groovy.time.TimeDuration; + /** Static class providing a shared {@link TypeCoercer} for all of Brooklyn */ public class TypeCoercions { @@ -68,6 +71,7 @@ public class TypeCoercions { public static void initStandardAdapters() { new BrooklynCommonAdaptorTypeCoercions(coercer).registerAllAdapters(); + new CommonAdaptorTryCoercions(coercer).registerAllAdapters(); registerDeprecatedBrooklynAdapters(); registerBrooklynAdapters(); registerGroovyAdapters(); @@ -82,6 +86,11 @@ public class TypeCoercions { return coercer.registerAdapter(sourceType, targetType, fn); } + @Beta + public static void registerAdapter(TryCoercer fn) { + coercer.registerAdapter(fn); + } + public static <T> Function<Object, T> function(final Class<T> type) { return coercer.function(type); } http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java new file mode 100644 index 0000000..b0a494a --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java @@ -0,0 +1,110 @@ +/* + * 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.util.List; + +import org.apache.brooklyn.util.exceptions.CompoundRuntimeException; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.text.Strings; + +import com.google.common.collect.Lists; +import com.google.common.reflect.TypeToken; + +/** + * Defines and registers common generic coercers (e.g. to call a "fromXyz" method, to + * convert from one type to another). + */ +public class CommonAdaptorTryCoercions { + + private final TypeCoercerExtensible coercer; + + public CommonAdaptorTryCoercions(TypeCoercerExtensible coercer) { + this.coercer = coercer; + } + + public CommonAdaptorTryCoercions registerAllAdapters() { + registerAdapter(new TryCoercerWithFromMethod()); + registerAdapter(new TryCoercerToEnum()); + registerAdapter(new TryCoercerForPrimitivesAndStrings()); + return this; + } + + /** Registers an adapter for use with type coercion. */ + public synchronized void registerAdapter(TryCoercer fn) { + coercer.registerAdapter(fn); + } + + protected static class TryCoercerWithFromMethod implements TryCoercer { + @Override + @SuppressWarnings("unchecked") + public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> targetType) { + Class<? super T> rawTargetType = targetType.getRawType(); + + List<ClassCoercionException> exceptions = Lists.newArrayList(); + //now look for static TargetType.fromType(Type t) where value instanceof Type + for (Method m: rawTargetType.getMethods()) { + if (((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC) && + m.getName().startsWith("from") && m.getParameterTypes().length==1 && + m.getParameterTypes()[0].isInstance(input)) { + if (m.getName().equals("from"+JavaClassNames.verySimpleClassName(m.getParameterTypes()[0]))) { + try { + return Maybe.of((T) m.invoke(null, input)); + } catch (Exception e) { + exceptions.add(new ClassCoercionException("Cannot coerce type "+input.getClass()+" to "+rawTargetType.getCanonicalName()+" ("+input+"): "+m.getName()+" adapting failed", e)); + } + } + } + } + if (exceptions.isEmpty()) { + return null; + } else if (exceptions.size() == 1) { + return Maybe.absent(exceptions.get(0)); + } else { + String errMsg = "Failed coercing type "+input.getClass()+" to "+rawTargetType.getCanonicalName(); + return Maybe.absent(new CompoundRuntimeException(errMsg, exceptions)); + } + } + } + + protected static class TryCoercerToEnum implements TryCoercer { + @Override + @SuppressWarnings("unchecked") + public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> targetType) { + Class<? super T> rawTargetType = targetType.getRawType(); + + //for enums call valueOf with the string representation of the value + if (rawTargetType.isEnum()) { + return EnumTypeCoercions.tryCoerceUntyped(Strings.toString(input), (Class<T>)rawTargetType); + } else { + return null; + } + } + } + + protected static class TryCoercerForPrimitivesAndStrings implements TryCoercer { + @Override + public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> targetType) { + return PrimitiveStringTypeCoercions.tryCoerce(input, targetType.getRawType()); + } + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java ---------------------------------------------------------------------- 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 index 03745e6..b74548e 100644 --- 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 @@ -41,8 +41,8 @@ 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.text.Strings; import org.apache.brooklyn.util.time.Duration; import org.apache.brooklyn.util.time.Time; import org.apache.brooklyn.util.yaml.Yamls; @@ -61,10 +61,6 @@ public class CommonAdaptorTypeCoercions { this.coercer = coercer; } - public TypeCoercerExtensible getCoercer() { - return coercer; - } - public CommonAdaptorTypeCoercions registerAllAdapters() { registerStandardAdapters(); registerRecursiveIterableAdapters(); http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java new file mode 100644 index 0000000..f410fbc --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java @@ -0,0 +1,45 @@ +/* + * 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.annotations.Beta; +import com.google.common.reflect.TypeToken; + +/** + * A coercer that can be registered, which will try to coerce the given input to the given type. + * + * This can be used for "generic" coercers, such as those that look for a {@code fromValue()} + * method on the target type. + */ +@Beta +public interface TryCoercer { + + /** + * The meaning of the return value is: + * <ul> + * <li>null - no errors, recommend continue with fallbacks (i.e. not found). + * <li>absent - had some kind of exception, recommend continue with fallbacks (but can report this error if + * other fallbacks fail). + * <li>present - coercion successful. + * </ul> + */ + <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type); +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java ---------------------------------------------------------------------- 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 index 366ae26..a673131 100644 --- 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 @@ -18,8 +18,6 @@ */ 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; @@ -28,17 +26,15 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.apache.brooklyn.util.exceptions.CompoundRuntimeException; 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.annotations.Beta; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.collect.HashBasedTable; @@ -77,7 +73,10 @@ public class TypeCoercerExtensible implements TypeCoercer { /** has all the strategies (primitives, collections, etc) * and all the adapters from {@link CommonAdaptorTypeCoercions} */ public static TypeCoercerExtensible newDefault() { - return new CommonAdaptorTypeCoercions(newEmpty()).registerAllAdapters().getCoercer(); + TypeCoercerExtensible result = newEmpty(); + new CommonAdaptorTypeCoercions(result).registerAllAdapters(); + new CommonAdaptorTryCoercions(result).registerAllAdapters(); + return result; } /** has all the strategies (primitives, collections, etc) but no adapters, @@ -87,7 +86,10 @@ public class TypeCoercerExtensible implements TypeCoercer { } /** Store the coercion {@link Function functions} in a {@link Table table}. */ - private Table<Class<?>, Class<?>, Function<?,?>> registry = HashBasedTable.create(); + private final Table<Class<?>, Class<?>, Function<?,?>> registry = HashBasedTable.create(); + + /** Store the generic coercers. */ + private final List<TryCoercer> genericCoercers = Lists.newCopyOnWriteArrayList(); @Override public <T> T coerce(Object value, Class<T> targetType) { @@ -142,13 +144,11 @@ public class TypeCoercerExtensible implements TypeCoercer { 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; + for (TryCoercer coercer : genericCoercers) { + result = coercer.tryCoerce(value, targetTypeToken); + 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... @@ -165,13 +165,6 @@ public class TypeCoercerExtensible implements TypeCoercer { } } - //for enums call valueOf with the string representation of the value - if (targetType.isEnum()) { - result = EnumTypeCoercions.tryCoerceUntyped(Strings.toString(value), (Class<T>)targetType); - if (result!=null && result.isPresent()) return result; - if (result!=null && firstError==null) firstError = result; - } - //now look in registry synchronized (registry) { Map<Class<?>, Function<?,?>> adapters = registry.row(targetType); @@ -212,42 +205,6 @@ public class TypeCoercerExtensible implements TypeCoercer { return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetType.getCanonicalName()+" ("+value+"): no adapter known")); } - /** - * The meaning of the return value is: - * <ul> - * <li>null - no errors, continue with fallbacks (i.e. not found). - * <li>absent - had some kind of exception, continue with fallbacks (but can report this error if - * other fallbacks fail). - * <li>present - coercion successful, return value. - * </ul> - */ - @SuppressWarnings("unchecked") - protected <T> Maybe<T> tryCoerceWithFromMethod(Object value, Class<? super T> targetType) { - List<ClassCoercionException> exceptions = Lists.newArrayList(); - //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) { - exceptions.add(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed", e)); - } - } - } - } - if (exceptions.isEmpty()) { - return null; - } else if (exceptions.size() == 1) { - return Maybe.absent(exceptions.get(0)); - } else { - String errMsg = "Failed coercing type "+value.getClass()+" to "+targetType.getCanonicalName(); - return Maybe.absent(new CompoundRuntimeException(errMsg, exceptions)); - } - } - @SuppressWarnings("unchecked") protected <T> Maybe<T> tryCoerceMap(Object value, TypeToken<T> targetTypeToken) { if (!(value instanceof Map) || !(Map.class.isAssignableFrom(targetTypeToken.getRawType()))) return null; @@ -324,4 +281,9 @@ public class TypeCoercerExtensible implements TypeCoercer { return (Function<? super A,B>) registry.put(targetType, sourceType, fn); } + /** Registers a generic adapter for use with type coercion. */ + @Beta + public synchronized void registerAdapter(TryCoercer fn) { + genericCoercers.add(fn); + } } http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java ---------------------------------------------------------------------- diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java new file mode 100644 index 0000000..afb10f9 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.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.coerce; + +import static org.testng.Assert.assertEquals; + +import java.util.Objects; + +import org.apache.brooklyn.util.guava.Maybe; +import org.testng.annotations.Test; + +import com.google.common.base.MoreObjects; +import com.google.common.reflect.TypeToken; + +public class TypeCoercerExtensibleTest { + + TypeCoercerExtensible coercer = TypeCoercerExtensible.newDefault(); + + protected <T> T coerce(Object x, Class<T> type) { + return coercer.coerce(x, type); + } + protected <T> T coerce(Object x, TypeToken<T> type) { + return coercer.coerce(x, type); + } + + @Test + public void testRegisterNewGenericCoercer() { + coercer.registerAdapter(new TryCoercer() { + @Override + @SuppressWarnings("unchecked") + public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type) { + if (input instanceof String && type.getRawType() == MyClazz.class) { + return (Maybe<T>) Maybe.of(new MyClazz("myprefix"+input)); + } else { + return null; + } + } + }); + + assertEquals(coerce("abc", MyClazz.class), new MyClazz("myprefixabc")); + } + + public static class MyClazz { + private final String val; + + public MyClazz(String val) { + this.val = val; + } + + @Override + public int hashCode() { + return Objects.hashCode(val); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MyClazz)) return false; + return Objects.equals(val, ((MyClazz)obj).val); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("val", val).toString(); + } + } +}
