Return-Path: X-Original-To: apmail-brooklyn-commits-archive@minotaur.apache.org Delivered-To: apmail-brooklyn-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id A708F18723 for ; Tue, 18 Aug 2015 11:00:59 +0000 (UTC) Received: (qmail 72536 invoked by uid 500); 18 Aug 2015 11:00:56 -0000 Delivered-To: apmail-brooklyn-commits-archive@brooklyn.apache.org Received: (qmail 72483 invoked by uid 500); 18 Aug 2015 11:00:56 -0000 Mailing-List: contact commits-help@brooklyn.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@brooklyn.incubator.apache.org Delivered-To: mailing list commits@brooklyn.incubator.apache.org Received: (qmail 72473 invoked by uid 99); 18 Aug 2015 11:00:56 -0000 Received: from Unknown (HELO spamd1-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 18 Aug 2015 11:00:56 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd1-us-west.apache.org (ASF Mail Server at spamd1-us-west.apache.org) with ESMTP id D92A6DEB98 for ; Tue, 18 Aug 2015 11:00:55 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd1-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: 1.399 X-Spam-Level: * X-Spam-Status: No, score=1.399 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, KAM_LAZY_DOMAIN_SECURITY=1, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RP_MATCHES_RCVD=-0.381] autolearn=disabled Received: from mx1-eu-west.apache.org ([10.40.0.8]) by localhost (spamd1-us-west.apache.org [10.40.0.7]) (amavisd-new, port 10024) with ESMTP id awacHoyoEvQn for ; Tue, 18 Aug 2015 11:00:47 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-eu-west.apache.org (ASF Mail Server at mx1-eu-west.apache.org) with SMTP id B792C25615 for ; Tue, 18 Aug 2015 11:00:20 +0000 (UTC) Received: (qmail 68816 invoked by uid 99); 18 Aug 2015 11:00:16 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 18 Aug 2015 11:00:16 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id D29A1DFFED; Tue, 18 Aug 2015 11:00:16 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: heneveld@apache.org To: commits@brooklyn.incubator.apache.org Date: Tue, 18 Aug 2015 11:00:40 -0000 Message-Id: <954955fc348448f7a45a8508dc93da7d@git.apache.org> In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [25/64] incubator-brooklyn git commit: [BROOKLYN-162] Refactor package in ./core/util http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/main/java/org/apache/brooklyn/core/util/flags/FlagUtils.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/flags/FlagUtils.java b/core/src/main/java/org/apache/brooklyn/core/util/flags/FlagUtils.java new file mode 100644 index 0000000..6f28f9b --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/flags/FlagUtils.java @@ -0,0 +1,587 @@ +/* + * 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.core.util.flags; + +import static brooklyn.util.GroovyJavaMethods.elvis; +import static brooklyn.util.GroovyJavaMethods.truth; +import static com.google.common.base.Preconditions.checkNotNull; +import groovy.lang.Closure; +import groovy.lang.GroovyObject; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.apache.brooklyn.api.entity.trait.Configurable; +import org.apache.brooklyn.core.util.config.ConfigBag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.config.ConfigKey; +import brooklyn.config.ConfigKey.HasConfigKey; +import brooklyn.util.GroovyJavaMethods; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.guava.Maybe; + +import com.google.common.base.Objects; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + + +/** class to help transfer values passed as named arguments to other well-known variables/fields/objects; + * see the test case for example usage */ +public class FlagUtils { + + public static final Logger log = LoggerFactory.getLogger(FlagUtils.class); + + private FlagUtils() {} + + /** see {@link #setFieldsFromFlags(Object o, ConfigBag)} */ + public static Map setPublicFieldsFromFlags(Map flags, Object o) { + return setFieldsFromFlagsInternal(o, Arrays.asList(o.getClass().getFields()), flags, null, true); + } + + /** see {@link #setFieldsFromFlags(Object, ConfigBag)} */ + public static Map setFieldsFromFlags(Map flags, Object o) { + return setFieldsFromFlagsInternal(o, getAllFields(o.getClass()), flags, null, true); + } + + /** sets all fields (including private and static, local and inherited) annotated {@link SetFromFlag} on the given object, + * from the given flags map, returning just those flag-value pairs passed in which do not correspond to SetFromFlags fields + * annotated ConfigKey and HasConfigKey fields are _configured_ (and we assume the object in that case is {@link Configurable}); + * keys should be ConfigKey, HasConfigKey, or String; + * default values are also applied unless that is specified false on one of the variants of this method which takes such an argument + */ + public static void setFieldsFromFlags(Object o, ConfigBag configBag) { + setFieldsFromFlagsInternal(o, getAllFields(o.getClass()), configBag.getAllConfig(), configBag, true); + } + + /** as {@link #setFieldsFromFlags(Object, ConfigBag)}, but allowing control over whether default values should be set */ + public static void setFieldsFromFlags(Object o, ConfigBag configBag, boolean setDefaultVals) { + setFieldsFromFlagsInternal(o, getAllFields(o.getClass()), configBag.getAllConfig(), configBag, setDefaultVals); + } + + /** as {@link #setFieldsFromFlags(Object, ConfigBag)}, but specifying a subset of flags to use */ + public static void setFieldsFromFlagsWithBag(Object o, Map flags, ConfigBag configBag, boolean setDefaultVals) { + setFieldsFromFlagsInternal(o, getAllFields(o.getClass()), flags, configBag, setDefaultVals); + } + + /** + * Sets the field with the given flag (if it exists) to the given value. + * Will attempt to coerce the value to the required type. + * Will respect "nullable" on the SetFromFlag annotation. + * + * @throws IllegalArgumentException If fieldVal is null and the SetFromFlag annotation set nullable=false + */ + public static boolean setFieldFromFlag(Object o, String flagName, Object fieldVal) { + return setFieldFromFlagInternal(checkNotNull(flagName, "flagName"), fieldVal, o, getAllFields(o.getClass())); + } + + /** get all fields (including private and static) on the given object and all supertypes, + * that are annotated with SetFromFlags. + */ + public static Map getFieldsWithFlags(Object o) { + return getFieldsWithFlagsInternal(o, getAllFields(o.getClass())); + } + + /** + * Finds the {@link Field} on the given object annotated with the given name flag. + */ + public static Field findFieldForFlag(String flagName, Object o) { + return findFieldForFlagInternal(flagName, o, getAllFields(o.getClass())); + } + + /** get all fields (including private and static) and their values on the given object and all supertypes, + * where the field is annotated with SetFromFlags. + */ + public static Map getFieldsWithFlagsExcludingModifiers(Object o, int excludingModifiers) { + List filteredFields = Lists.newArrayList(); + for (Field contender : getAllFields(o.getClass())) { + if ((contender.getModifiers() & excludingModifiers) == 0) { + filteredFields.add(contender); + } + } + return getFieldsWithFlagsInternal(o, filteredFields); + } + + /** get all fields with the given modifiers, and their values on the given object and all supertypes, + * where the field is annotated with SetFromFlags. + */ + public static Map getFieldsWithFlagsWithModifiers(Object o, int requiredModifiers) { + List filteredFields = Lists.newArrayList(); + for (Field contender : getAllFields(o.getClass())) { + if ((contender.getModifiers() & requiredModifiers) == requiredModifiers) { + filteredFields.add(contender); + } + } + return getFieldsWithFlagsInternal(o, filteredFields); + } + + /** sets _all_ accessible _{@link ConfigKey}_ and {@link HasConfigKey} fields on the given object, + * using the indicated flags/config-bag + * @deprecated since 0.7.0 use {@link #setAllConfigKeys(Map, Configurable, boolean)} */ + public static Map setAllConfigKeys(Map flagsOrConfig, Configurable instance) { + return setAllConfigKeys(flagsOrConfig, instance, false); + } + /** sets _all_ accessible _{@link ConfigKey}_ and {@link HasConfigKey} fields on the given object, + * using the indicated flags/config-bag */ + public static Map setAllConfigKeys(Map flagsOrConfig, Configurable instance, boolean includeFlags) { + ConfigBag bag = new ConfigBag().putAll(flagsOrConfig); + setAllConfigKeys(instance, bag, includeFlags); + return bag.getUnusedConfigMutable(); + } + + /** sets _all_ accessible _{@link ConfigKey}_ and {@link HasConfigKey} fields on the given object, + * using the indicated flags/config-bag + * @deprecated since 0.7.0 use {@link #setAllConfigKeys(Configurable, ConfigBag, boolean)} */ + public static void setAllConfigKeys(Configurable o, ConfigBag bag) { + setAllConfigKeys(o, bag, false); + } + /** sets _all_ accessible _{@link ConfigKey}_ and {@link HasConfigKey} fields on the given object, + * using the indicated flags/config-bag */ + public static void setAllConfigKeys(Configurable o, ConfigBag bag, boolean includeFlags) { + for (Field f: getAllFields(o.getClass())) { + ConfigKey key = getFieldAsConfigKey(o, f); + if (key!=null) { + FlagConfigKeyAndValueRecord record = getFlagConfigKeyRecord(f, key, bag); + if ((includeFlags && record.isValuePresent()) || record.getConfigKeyMaybeValue().isPresent()) { + setField(o, f, record.getValueOrNullPreferringConfigKey(), null); + } + } + } + } + + public static class FlagConfigKeyAndValueRecord { + private String flagName = null; + private ConfigKey configKey = null; + private Maybe flagValue = Maybe.absent(); + private Maybe configKeyValue = Maybe.absent(); + + public String getFlagName() { + return flagName; + } + public ConfigKey getConfigKey() { + return configKey; + } + public Maybe getFlagMaybeValue() { + return flagValue; + } + public Maybe getConfigKeyMaybeValue() { + return configKeyValue; + } + public Object getValueOrNullPreferringConfigKey() { + return getConfigKeyMaybeValue().or(getFlagMaybeValue()).orNull(); + } + public Object getValueOrNullPreferringFlag() { + return getFlagMaybeValue().or(getConfigKeyMaybeValue()).orNull(); + } + /** true if value is present for either flag or config key */ + public boolean isValuePresent() { + return flagValue.isPresent() || configKeyValue.isPresent(); + } + + @Override + public String toString() { + return Objects.toStringHelper(this).omitNullValues() + .add("flag", flagName) + .add("configKey", configKey) + .add("flagValue", flagValue.orNull()) + .add("configKeyValue", configKeyValue.orNull()) + .toString(); + } + } + + /** gets all the flags/keys in the given config bag which are applicable to the given type's config keys and flags */ + public static List findAllFlagsAndConfigKeys(T optionalInstance, Class type, ConfigBag input) { + List output = new ArrayList(); + for (Field f: getAllFields(type)) { + ConfigKey key = getFieldAsConfigKey(optionalInstance, f); + FlagConfigKeyAndValueRecord record = getFlagConfigKeyRecord(f, key, input); + if (record.isValuePresent()) + output.add(record); + } + return output; + } + + /** returns the flag/config-key record for the given input */ + private static FlagConfigKeyAndValueRecord getFlagConfigKeyRecord(Field f, ConfigKey key, ConfigBag input) { + FlagConfigKeyAndValueRecord result = new FlagConfigKeyAndValueRecord(); + result.configKey = key; + if (key!=null && input.containsKey(key)) + result.configKeyValue = Maybe.of(input.getStringKey(key.getName())); + SetFromFlag flag = f.getAnnotation(SetFromFlag.class); + if (flag!=null) { + result.flagName = flag.value(); + if (input.containsKey(flag.value())) + result.flagValue = Maybe.of(input.getStringKey(flag.value())); + } + return result; + } + + /** returns all fields on the given class, superclasses, and interfaces thereof, in that order of preference, + * (excluding fields on Object) */ + public static List getAllFields(Class base, Closure filter) { + return getAllFields(base, GroovyJavaMethods.predicateFromClosure(filter)); + } + public static List getAllFields(Class base) { + return getAllFields(base, Predicates.alwaysTrue()); + } + public static List getAllFields(Class base, Predicate filter) { + return getLocalFields(getAllAssignableTypes(base), filter); + } + /** returns all fields explicitly declared on the given classes */ + public static List getLocalFields(List> classes) { + return getLocalFields(classes, Predicates.alwaysTrue()); + } + public static List getLocalFields(List> classes, Closure filter) { + return getLocalFields(classes, GroovyJavaMethods.predicateFromClosure(filter)); + } + public static List getLocalFields(List> classes, Predicate filter) { + List fields = Lists.newArrayList(); + for (Class c : classes) { + for (Field f : c.getDeclaredFields()) { + if (filter.apply(f)) fields.add(f); + } + } + return fields; + } + + /** returns base, superclasses, then interfaces */ + public static List> getAllAssignableTypes(Class base) { + return getAllAssignableTypes(base, new Predicate>() { + @Override public boolean apply(Class it) { + return (it != Object.class) && (it != GroovyObject.class); + } + }); + } + public static List> getAllAssignableTypes(Class base, Closure filter) { + return getAllAssignableTypes(base, GroovyJavaMethods.>predicateFromClosure(filter)); + } + public static List> getAllAssignableTypes(Class base, Predicate> filter) { + List> classes = Lists.newArrayList(); + for (Class c = base; c != null; c = c.getSuperclass()) { + if (filter.apply(c)) classes.add(c); + } + for (int i=0; i interf : classes.get(i).getInterfaces()) { + if (filter.apply(interf) && !(classes.contains(interf))) classes.add(interf); + } + } + return classes; + } + + private static Map getFieldsWithFlagsInternal(Object o, Collection fields) { + Map result = Maps.newLinkedHashMap(); + for (Field f: fields) { + SetFromFlag cf = f.getAnnotation(SetFromFlag.class); + if (cf != null) { + String flagName = elvis(cf.value(), f.getName()); + if (truth(flagName)) { + result.put(flagName, getField(o, f)); + } else { + log.warn("Ignoring field {} of object {} as no flag name available", f, o); + } + } + } + return result; + } + + private static Field findFieldForFlagInternal(String flagName, Object o, Collection fields) { + for (Field f: fields) { + SetFromFlag cf = f.getAnnotation(SetFromFlag.class); + if (cf != null) { + String contenderName = elvis(cf.value(), f.getName()); + if (flagName.equals(contenderName)) { + return f; + } + } + } + throw new NoSuchElementException("Field with flag "+flagName+" not found on "+o+" of type "+(o != null ? o.getClass() : null)); + } + + private static boolean setFieldFromFlagInternal(String flagName, Object fieldVal, Object o, Collection fields) { + for (Field f: fields) { + SetFromFlag cf = f.getAnnotation(SetFromFlag.class); + if (cf != null && flagName.equals(elvis(cf.value(), f.getName()))) { + setField(o, f, fieldVal, cf); + return true; + } + } + return false; + } + + private static Map setFieldsFromFlagsInternal(Object o, Collection fields, Map flagsOrConfig, ConfigBag bag, boolean setDefaultVals) { + if (bag==null) bag = new ConfigBag().putAll(flagsOrConfig); + for (Field f: fields) { + SetFromFlag cf = f.getAnnotation(SetFromFlag.class); + if (cf!=null) setFieldFromConfig(o, f, bag, cf, setDefaultVals); + } + return bag.getUnusedConfigMutable(); + } + + private static void setFieldFromConfig(Object o, Field f, ConfigBag bag, SetFromFlag optionalAnnotation, boolean setDefaultVals) { + String flagName = optionalAnnotation==null ? null : (String)elvis(optionalAnnotation.value(), f.getName()); + // prefer flag name, if present + if (truth(flagName) && bag.containsKey(flagName)) { + setField(o, f, bag.getStringKey(flagName), optionalAnnotation); + return; + } + // first check whether it is a key + ConfigKey key = getFieldAsConfigKey(o, f); + if (key!=null && bag.containsKey(key)) { + Object uncoercedValue = bag.getStringKey(key.getName()); + setField(o, f, uncoercedValue, optionalAnnotation); + return; + } + if (setDefaultVals && optionalAnnotation!=null && truth(optionalAnnotation.defaultVal())) { + Object oldValue; + try { + f.setAccessible(true); + oldValue = f.get(o); + if (oldValue==null || oldValue.equals(getDefaultValueForType(f.getType()))) { + setField(o, f, optionalAnnotation.defaultVal(), optionalAnnotation); + } + } catch (Exception e) { + Exceptions.propagate(e); + } + return; + } + } + + /** returns the given field as a config key, if it is an accessible config key, otherwise null */ + private static ConfigKey getFieldAsConfigKey(Object optionalInstance, Field f) { + if (optionalInstance==null) { + if ((f.getModifiers() & Modifier.STATIC)==0) + // non-static field on null instance, can't be set + return null; + } + if (ConfigKey.class.isAssignableFrom(f.getType())) { + return (ConfigKey) getField(optionalInstance, f); + } else if (HasConfigKey.class.isAssignableFrom(f.getType())) { + return ((HasConfigKey)getField(optionalInstance, f)).getConfigKey(); + } + return null; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static void setConfig(Object objectOfField, ConfigKey key, Object value, SetFromFlag optionalAnnotation) { + if (objectOfField instanceof Configurable) { + ((Configurable)objectOfField).setConfig((ConfigKey)key, value); + return; + } else { + if (optionalAnnotation==null) { + log.warn("Cannot set key "+key.getName()+" on "+objectOfField+": containing class is not Configurable"); + } else if (!key.getName().equals(optionalAnnotation.value())) { + log.warn("Cannot set key "+key.getName()+" on "+objectOfField+" from flag "+optionalAnnotation.value()+": containing class is not Configurable"); + } else { + // if key and flag are the same, then it will probably happen automatically + if (log.isDebugEnabled()) + log.debug("Cannot set key "+key.getName()+" on "+objectOfField+" from flag "+optionalAnnotation.value()+": containing class is not Configurable"); + } + return; + } + } + + /** sets the field to the value, after checking whether the given value can be set + * respecting the constraints of the annotation + */ + public static void setField(Object objectOfField, Field f, Object value, SetFromFlag optionalAnnotation) { + try { + ConfigKey key = getFieldAsConfigKey(objectOfField, f); + if (key!=null) { + setConfig(objectOfField, key, value, optionalAnnotation); + return; + } + + if (!f.isAccessible()) f.setAccessible(true); + if (optionalAnnotation!=null && optionalAnnotation.immutable()) { + Object oldValue = f.get(objectOfField); + if (!Objects.equal(oldValue, getDefaultValueForType(f.getType())) && oldValue != value) { + throw new IllegalStateException("Forbidden modification to immutable field "+ + f+" in "+objectOfField+": attempting to change to "+value+" when was already "+oldValue); + } + } + if (optionalAnnotation!=null && !optionalAnnotation.nullable() && value==null) { + throw new IllegalArgumentException("Forbidden null assignment to non-nullable field "+ + f+" in "+objectOfField); + } + if (optionalAnnotation!=null && (f.getModifiers() & Modifier.STATIC)==Modifier.STATIC) + log.warn("Setting static field "+f+" in "+objectOfField+" from flag "+optionalAnnotation.value()+": discouraged"); + + Object newValue; + try { + newValue = TypeCoercions.coerce(value, f.getType()); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot set "+f+" in "+objectOfField+" from type "+value.getClass()+" ("+value+"): "+e, e); + } + f.set(objectOfField, newValue); + if (log.isTraceEnabled()) log.trace("FlagUtils for "+objectOfField+", setting field="+f.getName()+"; val="+value+"; newVal="+newValue+"; key="+key); + + } catch (IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + /** gets the value of the field. + */ + public static Object getField(Object objectOfField, Field f) { + try { + if (!f.isAccessible()) f.setAccessible(true); + return f.get(objectOfField); + } catch (IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + /** returns the default/inital value that is assigned to fields of the givien type; + * if the type is not primitive this value is null; + * for primitive types it is obvious but not AFAIK programmatically visible + * (e.g. 0 for int, false for boolean) + */ + public static Object getDefaultValueForType(Class t) { + if (!t.isPrimitive()) return null; + if (t==Integer.TYPE) return (int)0; + if (t==Long.TYPE) return (long)0; + if (t==Double.TYPE) return (double)0; + if (t==Float.TYPE) return (float)0; + if (t==Byte.TYPE) return (byte)0; + if (t==Short.TYPE) return (short)0; + if (t==Character.TYPE) return (char)0; + if (t==Boolean.TYPE) return false; + //should never happen + throw new IllegalStateException("Class "+t+" is an unknown primitive."); + } + + /** returns a map of all fields which are annotated 'SetFromFlag', along with the annotation */ + public static Map getAnnotatedFields(Class type) { + Map result = Maps.newLinkedHashMap(); + for (Field f: getAllFields(type)) { + SetFromFlag cf = f.getAnnotation(SetFromFlag.class); + if (truth(cf)) result.put(f, cf); + } + return result; + } + + /** returns a map of all {@link ConfigKey} fields which are annotated 'SetFromFlag', along with the annotation */ + public static Map,SetFromFlag> getAnnotatedConfigKeys(Class type) { + Map, SetFromFlag> result = Maps.newLinkedHashMap(); + List fields = getAllFields(type, new Predicate() { + @Override public boolean apply(Field f) { + return (f != null) && ConfigKey.class.isAssignableFrom(f.getType()) && ((f.getModifiers() & Modifier.STATIC)!=0); + }}); + for (Field f: fields) { + SetFromFlag cf = f.getAnnotation(SetFromFlag.class); + if (cf != null) { + ConfigKey key = getFieldAsConfigKey(null, f); + if (key != null) { + result.put(key, cf); + } + } + } + return result; + } + + /** returns a map of all fields which are annotated 'SetFromFlag' with their current values; + * useful if you want to clone settings from one object + */ + public static Map getFieldsWithValues(Object o) { + try { + Map result = Maps.newLinkedHashMap(); + for (Map.Entry entry : getAnnotatedFields(o.getClass()).entrySet()) { + Field f = entry.getKey(); + SetFromFlag cf = entry.getValue(); + String flagName = elvis(cf.value(), f.getName()); + if (truth(flagName)) { + if (!f.isAccessible()) f.setAccessible(true); + result.put(flagName, f.get(o)); + } + } + return result; + } catch (IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + /** + * @throws an IllegalStateException if there are fields required (nullable=false) which are unset + * @throws wrapped IllegalAccessException + */ + public static void checkRequiredFields(Object o) { + try { + Set unsetFields = Sets.newLinkedHashSet(); + for (Map.Entry entry : getAnnotatedFields(o.getClass()).entrySet()) { + Field f = entry.getKey(); + SetFromFlag cf = entry.getValue(); + if (!cf.nullable()) { + String flagName = elvis(cf.value(), f.getName()); + if (!f.isAccessible()) f.setAccessible(true); + Object v = f.get(o); + if (v==null) unsetFields.add(flagName); + } + } + if (truth(unsetFields)) { + throw new IllegalStateException("Missing required "+(unsetFields.size()>1 ? "fields" : "field")+": "+unsetFields); + } + } catch (IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + +// /** sets all fields in target annotated with @SetFromFlag using the configuration in the given config bag */ +// public static void setFieldsFromConfigFlags(Object target, ConfigBag configBag) { +// setFieldsFromConfigFlags(target, configBag.getAllConfig(), configBag); +// } +// +// +// /** sets all fields in target annotated with @SetFromFlag using the configuration in the given configToUse, +// * marking used in the given configBag */ +// public static void setFieldsFromConfigFlags(Object target, Map configToUse, ConfigBag configBag) { +// for (Map.Entry entry: configToUse.entrySet()) { +// setFieldFromConfigFlag(target, entry.getKey(), entry.getValue(), configBag); +// } +// } +// +// public static void setFieldFromConfigFlag(Object target, Object key, Object value, ConfigBag optionalConfigBag) { +// String name = null; +// if (key instanceof String) name = (String)key; +// else if (key instanceof ConfigKey) name = ((ConfigKey)key).getName(); +// else if (key instanceof HasConfigKey) name = ((HasConfigKey)key).getConfigKey().getName(); +// else { +// if (key!=null) { +// log.warn("Invalid config type "+key.getClass().getCanonicalName()+" ("+key+") when configuring "+target+"; ignoring"); +// } +// return; +// } +// if (setFieldFromFlag(name, value, target)) { +// if (optionalConfigBag!=null) +// optionalConfigBag.markUsed(name); +// } +// } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/main/java/org/apache/brooklyn/core/util/flags/MethodCoercions.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/flags/MethodCoercions.java b/core/src/main/java/org/apache/brooklyn/core/util/flags/MethodCoercions.java new file mode 100644 index 0000000..20116cf --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/flags/MethodCoercions.java @@ -0,0 +1,183 @@ +/* + * 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.core.util.flags; + +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.guava.Maybe; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.reflect.TypeToken; + +import javax.annotation.Nullable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A way of binding a loosely-specified method call into a strongly-typed Java method call. + */ +public class MethodCoercions { + + /** + * Returns a predicate that matches a method with the given name, and a single parameter that + * {@link org.apache.brooklyn.core.util.flags.TypeCoercions#tryCoerce(Object, com.google.common.reflect.TypeToken)} can process + * from the given argument. + * + * @param methodName name of the method + * @param argument argument that is intended to be given + * @return a predicate that will match a compatible method + */ + public static Predicate matchSingleParameterMethod(final String methodName, final Object argument) { + checkNotNull(methodName, "methodName"); + checkNotNull(argument, "argument"); + + return new Predicate() { + @Override + public boolean apply(@Nullable Method input) { + if (input == null) return false; + if (!input.getName().equals(methodName)) return false; + Type[] parameterTypes = input.getGenericParameterTypes(); + return parameterTypes.length == 1 + && TypeCoercions.tryCoerce(argument, TypeToken.of(parameterTypes[0])).isPresentAndNonNull(); + + } + }; + } + + /** + * Tries to find a single-parameter method with a parameter compatible with (can be coerced to) the argument, and + * invokes it. + * + * @param instance the object to invoke the method on + * @param methodName the name of the method to invoke + * @param argument the argument to the method's parameter. + * @return the result of the method call, or {@link brooklyn.util.guava.Maybe#absent()} if method could not be matched. + */ + public static Maybe tryFindAndInvokeSingleParameterMethod(final Object instance, final String methodName, final Object argument) { + Class clazz = instance.getClass(); + Iterable methods = Arrays.asList(clazz.getMethods()); + Optional matchingMethod = Iterables.tryFind(methods, matchSingleParameterMethod(methodName, argument)); + if (matchingMethod.isPresent()) { + Method method = matchingMethod.get(); + try { + Type paramType = method.getGenericParameterTypes()[0]; + Object coercedArgument = TypeCoercions.coerce(argument, TypeToken.of(paramType)); + return Maybe.of(method.invoke(instance, coercedArgument)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw Exceptions.propagate(e); + } + } else { + return Maybe.absent(); + } + } + + /** + * Returns a predicate that matches a method with the given name, and parameters that + * {@link org.apache.brooklyn.core.util.flags.TypeCoercions#tryCoerce(Object, com.google.common.reflect.TypeToken)} can process + * from the given list of arguments. + * + * @param methodName name of the method + * @param arguments arguments that is intended to be given + * @return a predicate that will match a compatible method + */ + public static Predicate matchMultiParameterMethod(final String methodName, final List arguments) { + checkNotNull(methodName, "methodName"); + checkNotNull(arguments, "arguments"); + + return new Predicate() { + @Override + public boolean apply(@Nullable Method input) { + if (input == null) return false; + if (!input.getName().equals(methodName)) return false; + int numOptionParams = arguments.size(); + Type[] parameterTypes = input.getGenericParameterTypes(); + if (parameterTypes.length != numOptionParams) return false; + + for (int paramCount = 0; paramCount < numOptionParams; paramCount++) { + if (!TypeCoercions.tryCoerce(((List) arguments).get(paramCount), + TypeToken.of(parameterTypes[paramCount])).isPresentAndNonNull()) return false; + } + return true; + } + }; + } + + /** + * Tries to find a multiple-parameter method with each parameter compatible with (can be coerced to) the + * corresponding argument, and invokes it. + * + * @param instance the object to invoke the method on + * @param methodName the name of the method to invoke + * @param argument a list of the arguments to the method's parameters. + * @return the result of the method call, or {@link brooklyn.util.guava.Maybe#absent()} if method could not be matched. + */ + public static Maybe tryFindAndInvokeMultiParameterMethod(final Object instance, final String methodName, final List arguments) { + Class clazz = instance.getClass(); + Iterable methods = Arrays.asList(clazz.getMethods()); + Optional matchingMethod = Iterables.tryFind(methods, matchMultiParameterMethod(methodName, arguments)); + if (matchingMethod.isPresent()) { + Method method = matchingMethod.get(); + try { + int numOptionParams = ((List)arguments).size(); + Object[] coercedArguments = new Object[numOptionParams]; + for (int paramCount = 0; paramCount < numOptionParams; paramCount++) { + Object argument = arguments.get(paramCount); + Type paramType = method.getGenericParameterTypes()[paramCount]; + coercedArguments[paramCount] = TypeCoercions.coerce(argument, TypeToken.of(paramType)); + } + return Maybe.of(method.invoke(instance, coercedArguments)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw Exceptions.propagate(e); + } + } else { + return Maybe.absent(); + } + } + + /** + * Tries to find a method with each parameter compatible with (can be coerced to) the corresponding argument, and invokes it. + * + * @param instance the object to invoke the method on + * @param methodName the name of the method to invoke + * @param argument a list of the arguments to the method's parameters, or a single argument for a single-parameter method. + * @return the result of the method call, or {@link brooklyn.util.guava.Maybe#absent()} if method could not be matched. + */ + public static Maybe tryFindAndInvokeBestMatchingMethod(final Object instance, final String methodName, final Object argument) { + if (argument instanceof List) { + List arguments = (List) argument; + + // ambiguous case: we can't tell if the user is using the multi-parameter syntax, or the single-parameter + // syntax for a method which takes a List parameter. So we try one, then fall back to the other. + + Maybe maybe = tryFindAndInvokeMultiParameterMethod(instance, methodName, arguments); + if (maybe.isAbsent()) + maybe = tryFindAndInvokeSingleParameterMethod(instance, methodName, argument); + + return maybe; + } else { + return tryFindAndInvokeSingleParameterMethod(instance, methodName, argument); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/main/java/org/apache/brooklyn/core/util/flags/SetFromFlag.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/flags/SetFromFlag.java b/core/src/main/java/org/apache/brooklyn/core/util/flags/SetFromFlag.java new file mode 100644 index 0000000..3b69c05 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/flags/SetFromFlag.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.core.util.flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation to indicate that a variable may be set through the use of a named argument, + * looking for the name specified here or inferred from the annotated field/argument/object. + *

+ * This is used to automate the processing where named arguments are passed in constructors + * and other methods, and the values of those named arguments should be transferred to + * other known fields/arguments/objects at runtime. + *

+ * Fields on a class are typically set from values in a map with a call to + * {@link FlagUtils#setFieldsFromFlags(java.util.Map, Object)}. + * That method (and related, in the same class) will attend to the arguments here. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface SetFromFlag { + + /** the flag (key) which should be used to find the value; if empty defaults to field/argument/object name */ + String value() default ""; + + /** whether the object should not be changed once set; defaults to false + *

+ * this is partially tested for in many routines, but not all; + * when nullable=false the testing (when done) is guaranteed. + * however if nullable is allowed we do not distinguish between null and unset + * so explicitly setting null then setting to a value is not detected as an illegal mutating. + */ + boolean immutable() default false; + + /** whether the object is required & should not be set to null; defaults to true. + * (there is no 'required' parameter, but setting nullable false then invoking + * e.g. {@link FlagUtils#checkRequiredFields(Object)} has the effect of requiring a value) + *

+ * code should call that method explicitly to enforce nullable false; + * errors are not done during a call to setFieldsFromFlags + * because fields may be initialised in multiple passes.) + *

+ * this is partially tested for in many routines, but not all + */ + boolean nullable() default true; + + /** The default value, if it is not explicitly set. + *

+ * The value will be coerced from String where required, for types supported by {@link TypeCoercions}. + *

+ * The field will be initialised with its default value on the first call to setFieldsFromFlags + * (or related). (The field will not be initialised if that method is not called.) + */ + String defaultVal() default ""; +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/main/java/org/apache/brooklyn/core/util/flags/TypeCoercions.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/core/util/flags/TypeCoercions.java new file mode 100644 index 0000000..2c03620 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/flags/TypeCoercions.java @@ -0,0 +1,879 @@ +/* + * 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.core.util.flags; + +import groovy.lang.Closure; +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.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.event.AttributeSensor; +import org.apache.brooklyn.api.event.Sensor; +import org.apache.brooklyn.core.internal.BrooklynInitialization; +import org.apache.brooklyn.core.util.task.Tasks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.entity.basic.BrooklynTaskTags; +import brooklyn.entity.basic.ClosureEntityFactory; +import brooklyn.entity.basic.ConfigurableEntityFactory; +import brooklyn.entity.basic.ConfigurableEntityFactoryFromEntityFactory; +import brooklyn.event.basic.Sensors; +import brooklyn.util.JavaGroovyEquivalents; +import brooklyn.util.collections.MutableSet; +import brooklyn.util.collections.QuorumCheck; +import brooklyn.util.collections.QuorumCheck.QuorumChecks; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.guava.Maybe; +import brooklyn.util.javalang.Enums; +import brooklyn.util.net.Cidr; +import brooklyn.util.net.Networking; +import brooklyn.util.net.UserAndHostAndPort; +import brooklyn.util.text.StringEscapes.JavaStringEscapes; +import brooklyn.util.text.Strings; +import brooklyn.util.time.Duration; +import brooklyn.util.time.Time; +import brooklyn.util.yaml.Yamls; + +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") +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)); + } + + /** @see #coerce(Object, Class) */ + public static Maybe tryCoerce(Object value, TypeToken targetTypeToken) { + try { + return Maybe.of( coerce(value, targetTypeToken) ); + } catch (Throwable t) { + Exceptions.propagateIfFatal(t); + return Maybe.absent(t); + } + } + + /** @see #coerce(Object, Class) */ + @SuppressWarnings({ "unchecked" }) + public static T coerce(Object value, TypeToken targetTypeToken) { + if (value==null) return null; + Class targetType = targetTypeToken.getRawType(); + + //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); + } + } + + 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); + } + } + } + } + } + + //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); + } + } + } + } + + //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); + } + } + + //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); + } + return result; + } + } + } + + //not found + throw new ClassCoercionException("Cannot coerce type "+value.getClass()+" 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); + } + + 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); + } + } + + /** + * 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); + } + + 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") + 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) { + 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; + } + } + + /** returns the simple class name, and for any inner class the portion after the $ */ + public static String getVerySimpleName(Class c) { + String s = c.getSimpleName(); + if (s.indexOf('$')>=0) + s = s.substring(s.lastIndexOf('$')+1); + return s; + } + public static final Map BOXED_TO_UNBOXED_TYPES = ImmutableMap.builder(). + put(Integer.class, Integer.TYPE). + put(Long.class, Long.TYPE). + put(Boolean.class, Boolean.TYPE). + put(Byte.class, Byte.TYPE). + put(Double.class, Double.TYPE). + put(Float.class, Float.TYPE). + put(Character.class, Character.TYPE). + put(Short.class, Short.TYPE). + build(); + public static final Map UNBOXED_TO_BOXED_TYPES = ImmutableMap.builder(). + put(Integer.TYPE, Integer.class). + put(Long.TYPE, Long.class). + put(Boolean.TYPE, Boolean.class). + put(Byte.TYPE, Byte.class). + put(Double.TYPE, Double.class). + put(Float.TYPE, Float.class). + put(Character.TYPE, Character.class). + put(Short.TYPE, Short.class). + build(); + + /** for automatic conversion */ + public static Object getMatchingConstructor(Class target, Object ...arguments) { + Constructor[] cc = target.getConstructors(); + for (Constructor c: cc) { + if (c.getParameterTypes().length != arguments.length) + continue; + boolean matches = true; + Class[] tt = c.getParameterTypes(); + for (int i=0; i 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(String.class, URI.class, new Function() { + @Override + public URI apply(String input) { + return URI.create(input); + } + }); + 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(brooklyn.entity.basic.EntityFactory.class, ConfigurableEntityFactory.class, new Function() { + @SuppressWarnings("unchecked") + @Override + public ConfigurableEntityFactory apply(brooklyn.entity.basic.EntityFactory input) { + if (input instanceof ConfigurableEntityFactory) return (ConfigurableEntityFactory)input; + return new ConfigurableEntityFactoryFromEntityFactory(input); + } + }); + @SuppressWarnings({"unused", "deprecation"}) + Function ignoredVarHereToAllowSuppressDeprecationWarning2 = registerAdapter(Closure.class, brooklyn.entity.basic.EntityFactory.class, new Function() { + @SuppressWarnings("unchecked") + @Override + public brooklyn.entity.basic.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 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)); + } + }); + } +}