http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java 
b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
new file mode 100644
index 0000000..f2f31fe
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
@@ -0,0 +1,94 @@
+/*
+ * 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.config;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+import com.google.common.reflect.TypeToken;
+
+/**
+ * Represents the name of a piece of typed configuration data for an entity.
+ * <p>
+ * Two ConfigKeys should be considered equal if they have the same FQN.
+ */
+public interface ConfigKey<T> {
+    /**
+     * Returns the description of the configuration parameter, for display.
+     */
+    String getDescription();
+
+    /**
+     * Returns the name of the configuration parameter, in a dot-separated 
namespace (FQN).
+     */
+    String getName();
+
+    /**
+     * Returns the constituent parts of the configuration parameter name as a 
{@link Collection}.
+     */
+    Collection<String> getNameParts();
+
+    /**
+     * Returns the Guava TypeToken, including info on generics.
+     */
+    TypeToken<T> getTypeToken();
+    
+    /**
+     * Returns the type of the configuration parameter data.
+     * <p> 
+     * This returns a "super" of T only in the case where T is generified, 
+     * and in such cases it returns the Class instance for the unadorned T ---
+     * i.e. for List<String> this returns Class<List> ---
+     * this is of course because there is no actual Class<List<String>> 
instance.
+     */
+    Class<? super T> getType();
+
+    /**
+     * Returns the name of of the configuration parameter data type, as a 
{@link String}.
+     */
+    String getTypeName();
+
+    /**
+     * Returns the default value of the configuration parameter.
+     */
+    T getDefaultValue();
+
+    /**
+     * Returns true if a default configuration value has been set.
+     */
+    boolean hasDefaultValue();
+    
+    /**
+     * @return True if the configuration can be changed at runtime.
+     */
+    boolean isReconfigurable();
+    
+    /**
+     * @return The inheritance model, or <code>null</code> for the default in 
any context.
+     */
+    @Nullable ConfigInheritance getInheritance();
+
+    /** Interface for elements which want to be treated as a config key 
without actually being one
+     * (e.g. config attribute sensors).
+     */
+    public interface HasConfigKey<T> {
+        public ConfigKey<T> getConfigKey();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java 
b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java
new file mode 100644
index 0000000..665bbf6
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java
@@ -0,0 +1,86 @@
+/*
+ * 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.config;
+
+import java.util.Map;
+
+import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+import org.apache.brooklyn.util.guava.Maybe;
+
+import com.google.common.base.Predicate;
+
+public interface ConfigMap {
+    
+    /** @see #getConfig(ConfigKey, Object), with default value as per the key, 
or null */
+    public <T> T getConfig(ConfigKey<T> key);
+    
+    /** @see #getConfig(ConfigKey, Object), with default value as per the key, 
or null */
+    public <T> T getConfig(HasConfigKey<T> key);
+    
+    /**
+     * @see #getConfig(ConfigKey, Object), with provided default value if not 
set
+     * @deprecated since 0.7.0; use {@link #getConfig(HasConfigKey)}
+     */
+    @Deprecated
+    public <T> T getConfig(HasConfigKey<T> key, T defaultValue);
+    
+    /**
+     * Returns value stored against the given key,
+     * resolved (if it is a Task, possibly blocking), and coerced to the 
appropriate type, 
+     * or given default value if not set, 
+     * unless the default value is null in which case it returns the default.
+     * 
+     * @deprecated since 0.7.0; use {@link #getConfig(ConfigKey)}
+     */
+    @Deprecated
+    public <T> T getConfig(ConfigKey<T> key, T defaultValue);
+
+    /** as {@link #getConfigRaw(ConfigKey)} but returning null if not present 
+     * @deprecated since 0.7.0 use {@link #getConfigRaw(ConfigKey)} */
+    @Deprecated
+    public Object getRawConfig(ConfigKey<?> key);
+    
+    /** returns the value stored against the given key, 
+     * <b>not</b> any default,
+     * <b>not</b> resolved (and guaranteed non-blocking),
+     * and <b>not</b> type-coerced.
+     * @param key  key to look up
+     * @param includeInherited  for {@link ConfigMap} instances which have an 
inheritance hierarchy, 
+     *        whether to traverse it or not; has no effects where there is no 
inheritance 
+     * @return raw, unresolved, uncoerced value of key in map,  
+     *         but <b>not</b> any default on the key
+     */
+    public Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean 
includeInherited);
+
+    /** returns a map of all config keys to their raw (unresolved+uncoerced) 
contents */
+    public Map<ConfigKey<?>,Object> getAllConfig();
+
+    /** returns submap matching the given filter predicate; see 
ConfigPredicates for common predicates */
+    public ConfigMap submap(Predicate<ConfigKey<?>> filter);
+
+    /** returns a read-only map view which has string keys (corresponding to 
the config key names);
+     * callers encouraged to use the typed keys (and so not use this method),
+     * but in some compatibility areas having a Properties-like view is useful 
*/
+    public Map<String,Object> asMapWithStringKeys();
+    
+    public int size();
+    
+    public boolean isEmpty();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java 
b/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java
new file mode 100644
index 0000000..e0e8e8f
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java
@@ -0,0 +1,35 @@
+/*
+ * 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.config;
+
+import java.util.Map;
+
+/** convenience extension where map is principally strings or converted to 
strings
+ * (supporting BrooklynProperties) */
+public interface StringConfigMap extends ConfigMap {
+    /** @see #getFirst(java.util.Map, String...) */
+    public String getFirst(String... keys);
+    /** returns the value of the first key which is defined
+     * <p>
+     * takes the following flags:
+     * 'warnIfNone' or 'failIfNone' (both taking a boolean (to use default 
message) or a string (which is the message));
+     * and 'defaultIfNone' (a default value to return if there is no such 
property);
+     * defaults to no warning and null default value */
+    public String getFirst(@SuppressWarnings("rawtypes") Map flags, String... 
keys);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java 
b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java
new file mode 100644
index 0000000..20fc98d
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java
@@ -0,0 +1,499 @@
+/*
+ * 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.test;
+
+import groovy.lang.Closure;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.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.Sets;
+
+/**
+ * TODO should move this to new package brooklyn.util.assertions
+ * and TODO should add a repeating() method which returns an AssertingRepeater 
extending Repeater
+ * and:
+ * <li> adds support for requireAllIterationsTrue
+ * <li> convenience run methods equivalent to succeedsEventually and 
succeedsContinually
+ */
+@Beta
+public class Asserts {
+
+    /**
+     * The default timeout for assertions. Alter in individual tests by giving 
a
+     * "timeout" entry in method flags.
+     */
+    public static final Duration DEFAULT_TIMEOUT = Duration.THIRTY_SECONDS;
+
+    private static final Logger log = LoggerFactory.getLogger(Asserts.class);
+
+    private Asserts() {}
+    
+    // --- selected routines from testng.Assert for visibility without needing 
that package
+    
+    /**
+     * Asserts that a condition is true. If it isn't,
+     * an AssertionError, with the given message, is thrown.
+     * @param condition the condition to evaluate
+     * @param message the assertion error message
+     */
+    public static void assertTrue(boolean condition, String message) {
+        if (!condition) fail(message);
+    }
+
+    /**
+     * Asserts that a condition is false. If it isn't,
+     * an AssertionError, with the given message, is thrown.
+     * @param condition the condition to evaluate
+     * @param message the assertion error message
+     */
+    public static void assertFalse(boolean condition, String message) {
+        if (condition) fail(message);
+    }
+
+    /**
+     * Fails a test with the given message.
+     * @param message the assertion error message
+     */
+    public static AssertionError fail(String message) {
+        throw new AssertionError(message);
+    }
+
+    public static void assertEqualsIgnoringOrder(Iterable<?> actual, 
Iterable<?> expected) {
+        assertEqualsIgnoringOrder(actual, expected, false, null);
+    }
+
+    public static void assertEqualsIgnoringOrder(Iterable<?> actual, 
Iterable<?> expected, boolean logDuplicates, String errmsg) {
+        Set<?> actualSet = Sets.newLinkedHashSet(actual);
+        Set<?> expectedSet = Sets.newLinkedHashSet(expected);
+        Set<?> extras = Sets.difference(actualSet, expectedSet);
+        Set<?> missing = Sets.difference(expectedSet, actualSet);
+        List<Object> duplicates = Lists.newArrayList(actual);
+        for (Object a : actualSet) {
+            duplicates.remove(a);
+        }
+        String fullErrmsg = "extras="+extras+"; missing="+missing
+                + (logDuplicates ? "; 
duplicates="+MutableSet.copyOf(duplicates) : "")
+                +"; actualSize="+Iterables.size(actual)+"; 
expectedSize="+Iterables.size(expected)
+                +"; actual="+actual+"; expected="+expected+"; "+errmsg;
+        assertTrue(extras.isEmpty(), fullErrmsg);
+        assertTrue(missing.isEmpty(), fullErrmsg);
+        assertTrue(Iterables.size(actual) == Iterables.size(expected), 
fullErrmsg);
+        assertTrue(actualSet.equals(expectedSet), fullErrmsg); // should be 
covered by extras/missing/size test
+    }
+
+    // --- new routines
+    
+    public static <T> void eventually(Supplier<? extends T> supplier, 
Predicate<T> predicate) {
+        eventually(ImmutableMap.<String,Object>of(), supplier, predicate);
+    }
+    
+    public static <T> void eventually(Map<String,?> flags, Supplier<? extends 
T> supplier, Predicate<T> predicate) {
+        eventually(flags, supplier, predicate, (String)null);
+    }
+    
+    public static <T> void eventually(Map<String,?> flags, Supplier<? extends 
T> supplier, Predicate<T> predicate, String errMsg) {
+        Duration timeout = toDuration(flags.get("timeout"), 
Duration.ONE_SECOND);
+        Duration period = toDuration(flags.get("period"), Duration.millis(10));
+        long periodMs = period.toMilliseconds();
+        long startTime = System.currentTimeMillis();
+        long expireTime = startTime+timeout.toMilliseconds();
+        
+        boolean first = true;
+        T supplied = supplier.get();
+        while (first || System.currentTimeMillis() <= expireTime) {
+            supplied = supplier.get();
+            if (predicate.apply(supplied)) {
+                return;
+            }
+            first = false;
+            if (periodMs > 0) sleep(periodMs);
+        }
+        fail("supplied="+supplied+"; predicate="+predicate+(errMsg!=null?"; 
"+errMsg:""));
+    }
+    
+    // TODO improve here -- these methods aren't very useful without timeouts
+    public static <T> void continually(Supplier<? extends T> supplier, 
Predicate<T> predicate) {
+        continually(ImmutableMap.<String,Object>of(), supplier, predicate);
+    }
+
+    public static <T> void continually(Map<String,?> flags, Supplier<? extends 
T> supplier, Predicate<? super T> predicate) {
+        continually(flags, supplier, predicate, (String)null);
+    }
+
+    public static <T> void continually(Map<String,?> flags, Supplier<? extends 
T> supplier, Predicate<T> predicate, String errMsg) {
+        Duration duration = toDuration(flags.get("timeout"), 
Duration.ONE_SECOND);
+        Duration period = toDuration(flags.get("period"), Duration.millis(10));
+        long periodMs = period.toMilliseconds();
+        long startTime = System.currentTimeMillis();
+        long expireTime = startTime+duration.toMilliseconds();
+        
+        boolean first = true;
+        while (first || System.currentTimeMillis() <= expireTime) {
+            assertTrue(predicate.apply(supplier.get()), 
"supplied="+supplier.get()+"; predicate="+predicate+(errMsg!=null?"; 
"+errMsg:""));
+            if (periodMs > 0) sleep(periodMs);
+            first = false;
+        }
+    }
+
+    
+    /**
+     * Asserts given runnable succeeds in default duration.
+     * @see #DEFAULT_TIMEOUT
+     */
+    public static void succeedsEventually(Runnable r) {
+        succeedsEventually(ImmutableMap.<String,Object>of(), r);
+    }
+
+    public static void succeedsEventually(Map<String,?> flags, Runnable r) {
+        succeedsEventually(flags, toCallable(r));
+    }
+    
+    /**
+     * Asserts given callable succeeds (runs without failure) in default 
duration.
+     * @see #DEFAULT_TIMEOUT
+     */
+    public static <T> T succeedsEventually(Callable<T> c) {
+        return succeedsEventually(ImmutableMap.<String,Object>of(), c);
+    }
+    
+    // FIXME duplication with TestUtils.BooleanWithMessage
+    public static class BooleanWithMessage {
+        boolean value; String message;
+        public BooleanWithMessage(boolean value, String message) {
+            this.value = value; this.message = message;
+        }
+        public boolean asBoolean() {
+            return value;
+        }
+        public String toString() {
+            return message;
+        }
+    }
+
+    /**
+     * Convenience method for cases where we need to test until something is 
true.
+     *
+     * The runnable will be invoked periodically until it succesfully 
concludes.
+     * <p>
+     * The following flags are supported:
+     * <ul>
+     * <li>abortOnError (boolean, default true)
+     * <li>abortOnException - (boolean, default false)
+     * <li>timeout - (a Duration or an integer in millis, defaults to 
30*SECONDS)
+     * <li>period - (a Duration or an integer in millis, for fixed retry time; 
if not set, defaults to exponentially increasing from 1 to 500ms)
+     * <li>minPeriod - (a Duration or an integer in millis; only used if 
period not explicitly set; the minimum period when exponentially increasing; 
defaults to 1ms)
+     * <li>maxPeriod - (a Duration or an integer in millis; only used if 
period not explicitly set; the maximum period when exponentially increasing; 
defaults to 500ms)
+     * <li>maxAttempts - (integer, Integer.MAX_VALUE)
+     * </ul>
+     * 
+     * The following flags are deprecated:
+     * <ul>
+     * <li>useGroovyTruth - (defaults to false; any result code apart from 
'false' will be treated as success including null; ignored for Runnables which 
aren't Callables)
+     * </ul>
+     * 
+     * @param flags, accepts the flags listed above
+     * @param r
+     * @param finallyBlock
+     */
+    public static <T> T succeedsEventually(Map<String,?> flags, Callable<T> c) 
{
+        boolean abortOnException = get(flags, "abortOnException", false);
+        boolean abortOnError = get(flags, "abortOnError", false);
+        boolean useGroovyTruth = get(flags, "useGroovyTruth", false);
+        boolean logException = get(flags, "logException", true);
+
+        // To speed up tests, default is for the period to start small and 
increase...
+        Duration duration = toDuration(flags.get("timeout"), DEFAULT_TIMEOUT);
+        Duration fixedPeriod = toDuration(flags.get("period"), null);
+        Duration minPeriod = (fixedPeriod != null) ? fixedPeriod : 
toDuration(flags.get("minPeriod"), Duration.millis(1));
+        Duration maxPeriod = (fixedPeriod != null) ? fixedPeriod : 
toDuration(flags.get("maxPeriod"), Duration.millis(500));
+        int maxAttempts = get(flags, "maxAttempts", Integer.MAX_VALUE);
+        int attempt = 0;
+        long startTime = System.currentTimeMillis();
+        try {
+            Throwable lastException = null;
+            T result = null;
+            long lastAttemptTime = 0;
+            long expireTime = startTime+duration.toMilliseconds();
+            long sleepTimeBetweenAttempts = minPeriod.toMilliseconds();
+            
+            while (attempt < maxAttempts && lastAttemptTime < expireTime) {
+                try {
+                    attempt++;
+                    lastAttemptTime = System.currentTimeMillis();
+                    result = c.call();
+                    if (log.isTraceEnabled()) log.trace("Attempt {} after {} 
ms: {}", new Object[] {attempt, System.currentTimeMillis() - startTime, 
result});
+                    if (useGroovyTruth) {
+                        if (groovyTruth(result)) return result;
+                    } else if (Boolean.FALSE.equals(result)) {
+                        if (result instanceof BooleanWithMessage) 
+                            log.warn("Test returned an instance of 
BooleanWithMessage but useGroovyTruth is not set! " +
+                                     "The result of this probably isn't what 
you intended.");
+                        // FIXME surprising behaviour, "false" result here is 
acceptable
+                        return result;
+                    } else {
+                        return result;
+                    }
+                    lastException = null;
+                } catch(Throwable e) {
+                    lastException = e;
+                    if (log.isTraceEnabled()) log.trace("Attempt {} after {} 
ms: {}", new Object[] {attempt, System.currentTimeMillis() - startTime, 
e.getMessage()});
+                    if (abortOnException) throw e;
+                    if (abortOnError && e instanceof Error) throw e;
+                }
+                long sleepTime = Math.min(sleepTimeBetweenAttempts, 
expireTime-System.currentTimeMillis());
+                if (sleepTime > 0) Thread.sleep(sleepTime);
+                sleepTimeBetweenAttempts = 
Math.min(sleepTimeBetweenAttempts*2, maxPeriod.toMilliseconds());
+            }
+            
+            log.info("succeedsEventually exceeded max attempts or timeout - {} 
attempts lasting {} ms, for {}", new Object[] {attempt, 
System.currentTimeMillis()-startTime, c});
+            if (lastException != null)
+                throw lastException;
+            throw fail("invalid result: "+result);
+        } catch (Throwable t) {
+            if (logException) log.info("failed succeeds-eventually, 
"+attempt+" attempts, "+
+                    (System.currentTimeMillis()-startTime)+"ms elapsed "+
+                    "(rethrowing): "+t);
+            throw propagate(t);
+        }
+    }
+
+    public static <T> void succeedsContinually(Runnable r) {
+        succeedsContinually(ImmutableMap.<String,Object>of(), r);
+    }
+    
+    public static <T> void succeedsContinually(Map<?,?> flags, Runnable r) {
+        succeedsContinually(flags, toCallable(r));
+    }
+
+    public static <T> T succeedsContinually(Callable<T> c) {
+        return succeedsContinually(ImmutableMap.<String,Object>of(), c);
+    }
+    
+    public static <T> T succeedsContinually(Map<?,?> flags, Callable<T> job) {
+        Duration duration = toDuration(flags.get("timeout"), 
Duration.ONE_SECOND);
+        Duration period = toDuration(flags.get("period"), Duration.millis(10));
+        long periodMs = period.toMilliseconds();
+        long startTime = System.currentTimeMillis();
+        long expireTime = startTime+duration.toMilliseconds();
+        int attempt = 0;
+        
+        boolean first = true;
+        T result = null;
+        while (first || System.currentTimeMillis() <= expireTime) {
+            attempt++;
+            try {
+                result = job.call();
+            } catch (Exception e) {
+                log.info("succeedsContinually failed - {} attempts lasting {} 
ms, for {} (rethrowing)", new Object[] {attempt, 
System.currentTimeMillis()-startTime, job});
+                throw propagate(e);
+            }
+            if (periodMs > 0) sleep(periodMs);
+            first = false;
+        }
+        return result;
+    }
+    
+    private static Duration toDuration(Object duration, Duration defaultVal) {
+        if (duration == null)
+            return defaultVal;
+        else 
+            return Duration.of(duration);
+    }
+    
+    public static void assertFails(Runnable r) {
+        assertFailsWith(toCallable(r), Predicates.alwaysTrue());
+    }
+    
+    public static void assertFails(Callable<?> c) {
+        assertFailsWith(c, Predicates.alwaysTrue());
+    }
+    
+    public static void assertFailsWith(Callable<?> c, final Closure<Boolean> 
exceptionChecker) {
+        assertFailsWith(c, new Predicate<Throwable>() {
+            public boolean apply(Throwable input) {
+                return exceptionChecker.call(input);
+            }
+        });
+    }
+    
+    public static void assertFailsWith(Runnable c, final Class<? extends 
Throwable> validException, final Class<? extends Throwable> 
...otherValidExceptions) {
+        final List<Class<?>> validExceptions = 
ImmutableList.<Class<?>>builder()
+                .add(validException)
+                .addAll(ImmutableList.copyOf(otherValidExceptions))
+                .build();
+        
+        assertFailsWith(c, new Predicate<Throwable>() {
+            public boolean apply(Throwable e) {
+                for (Class<?> validException: validExceptions) {
+                    if (validException.isInstance(e)) return true;
+                }
+                fail("Test threw exception of unexpected type 
"+e.getClass()+"; expecting "+validExceptions);
+                return false;
+            }
+        });
+    }
+
+    public static void assertFailsWith(Runnable r, Predicate<? super 
Throwable> exceptionChecker) {
+        assertFailsWith(toCallable(r), exceptionChecker);
+    }
+    
+    public static void assertFailsWith(Callable<?> c, Predicate<? super 
Throwable> exceptionChecker) {
+        boolean failed = false;
+        try {
+            c.call();
+        } catch (Throwable e) {
+            failed = true;
+            if (!exceptionChecker.apply(e)) {
+                log.debug("Test threw invalid exception (failing)", e);
+                fail("Test threw invalid exception: "+e);
+            }
+            log.debug("Test for exception successful ("+e+")");
+        }
+        if (!failed) fail("Test code should have thrown exception but did 
not");
+    }
+
+    public static void assertReturnsEventually(final Runnable r, Duration 
timeout) throws InterruptedException, ExecutionException, TimeoutException {
+        final AtomicReference<Throwable> throwable = new 
AtomicReference<Throwable>();
+        Runnable wrappedR = new Runnable() {
+            @Override public void run() {
+                try {
+                    r.run();
+                } catch (Throwable t) {
+                    throwable.set(t);
+                    throw Exceptions.propagate(t);
+                }
+            }
+        };
+        Thread thread = new Thread(wrappedR, "assertReturnsEventually("+r+")");
+        try {
+            thread.start();
+            thread.join(timeout.toMilliseconds());
+            if (thread.isAlive()) {
+                throw new TimeoutException("Still running: r="+r+"; 
thread="+Arrays.toString(thread.getStackTrace()));
+            }
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        } finally {
+            thread.interrupt();
+        }
+        
+        if (throwable.get() !=  null) {
+            throw new ExecutionException(throwable.get());
+        }
+    }
+
+    public static <T> void assertThat(T object, Predicate<T> condition) {
+        if (condition.apply(object)) return;
+        fail("Failed "+condition+": "+object);
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static boolean groovyTruth(Object o) {
+        // TODO Doesn't handle matchers (see 
http://docs.codehaus.org/display/GROOVY/Groovy+Truth)
+        if (o == null) {
+            return false;
+        } else if (o instanceof Boolean) {
+            return (Boolean)o;
+        } else if (o instanceof String) {
+            return !((String)o).isEmpty();
+        } else if (o instanceof Collection) {
+            return !((Collection)o).isEmpty();
+        } else if (o instanceof Map) {
+            return !((Map)o).isEmpty();
+        } else if (o instanceof Iterator) {
+            return ((Iterator)o).hasNext();
+        } else if (o instanceof Enumeration) {
+            return ((Enumeration)o).hasMoreElements();
+        } else {
+            return true;
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    private static <T> T get(Map<String,?> map, String key, T defaultVal) {
+        Object val = map.get(key);
+        return (T) ((val == null) ? defaultVal : val);
+    }
+    
+    private static Callable<?> toCallable(Runnable r) {
+        return (r instanceof Callable) ? (Callable<?>)r : new 
RunnableAdapter<Void>(r, null);
+    }
+    
+    /** Same as {@link java.util.concurrent.Executors#callable(Runnable)}, 
except includes toString() */
+    static final class RunnableAdapter<T> implements Callable<T> {
+        final Runnable task;
+        final T result;
+        RunnableAdapter(Runnable task, T result) {
+            this.task = task;
+            this.result = result;
+        }
+        public T call() {
+            task.run();
+            return result;
+        }
+        @Override
+        public String toString() {
+            return "RunnableAdapter("+task+")";
+        }
+    }
+    
+    private static void sleep(long periodMs) {
+        if (periodMs > 0) {
+            try {
+                Thread.sleep(periodMs);
+            } catch (InterruptedException e) {
+                throw propagate(e);
+            }
+        }
+    }
+    
+    private static RuntimeException propagate(Throwable t) {
+        if (t instanceof InterruptedException) {
+            Thread.currentThread().interrupt();
+        }
+        if (t instanceof RuntimeException) throw (RuntimeException)t;
+        if (t instanceof Error) throw (Error)t;
+        throw new RuntimeException(t);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java 
b/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java
new file mode 100644
index 0000000..b072630
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import java.util.List;
+
+// FIXME move to brooklyn.util.cli.CommandLineArgs, and change get to "remove"
+public class CommandLineUtil {
+
+    public static String getCommandLineOption (List<String> args, String 
param){
+        return getCommandLineOption(args, param, null);
+    }
+
+    /** given a list of args, e.g. --name Foo --parent Bob
+     * will return "Foo" as param name, and remove those entries from the args 
list
+     */
+    public static String getCommandLineOption(List<String> args, String param, 
String defaultValue) {
+        int i = args.indexOf(param);
+        if (i >= 0) {
+            String result = args.get(i + 1);
+            args.remove(i + 1);
+            args.remove(i);
+            return result;
+        } else {
+            return defaultValue;
+        }
+    }
+
+    public static int getCommandLineOptionInt(List<String> args, String param, 
int defaultValue) {
+        String s = getCommandLineOption(args, param,null);
+        if (s == null) return defaultValue;
+        return Integer.parseInt(s);
+    }
+
+    //we don't want instances.
+    private CommandLineUtil(){}
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java
 
b/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java
new file mode 100644
index 0000000..94f9a04
--- /dev/null
+++ 
b/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java
@@ -0,0 +1,180 @@
+/*
+ * 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;
+
+import groovy.lang.Closure;
+import groovy.lang.GString;
+import groovy.time.TimeDuration;
+
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+
+// FIXME move to brooklyn.util.groovy
+public class JavaGroovyEquivalents {
+
+    private static final Logger log = 
LoggerFactory.getLogger(JavaGroovyEquivalents.class);
+
+    public static String join(Collection<?> collection, String separator) {
+        StringBuffer result = new StringBuffer();
+        Iterator<?> ci = collection.iterator();
+        if (ci.hasNext()) result.append(asNonnullString(ci.next()));
+        while (ci.hasNext()) {
+            result.append(separator);
+            result.append(asNonnullString(ci.next()));
+        }
+        return result.toString();
+    }
+
+    /** simple elvislike operators; uses groovy truth */
+    @SuppressWarnings("unchecked")
+    public static <T> Collection<T> elvis(Collection<T> preferred, 
Collection<?> fallback) {
+        // TODO Would be nice to not cast, but this is groovy equivalent! 
Let's fix generics in stage 2
+        return groovyTruth(preferred) ? preferred : (Collection<T>) fallback;
+    }
+    public static String elvis(String preferred, String fallback) {
+        return groovyTruth(preferred) ? preferred : fallback;
+    }
+    public static String elvisString(Object preferred, Object fallback) {
+        return elvis(asString(preferred), asString(fallback));
+    }
+    public static <T> T elvis(T preferred, T fallback) {
+        return groovyTruth(preferred) ? preferred : fallback;
+    }
+    public static <T> T elvis(Iterable<?> preferences) {
+        return elvis(Iterables.toArray(preferences, Object.class));
+    }
+    public static <T> T elvis(Object... preferences) {
+        if (preferences.length == 0) throw new 
IllegalArgumentException("preferences must not be empty for elvis");
+        for (Object contender : preferences) {
+            if (groovyTruth(contender)) return (T) fix(contender);
+        }
+        return (T) fix(preferences[preferences.length-1]);
+    }
+    
+    public static Object fix(Object o) {
+        if (o instanceof GString) return (o.toString());
+        return o;
+    }
+
+    public static String asString(Object o) {
+        if (o==null) return null;
+        return o.toString();
+    }
+    public static String asNonnullString(Object o) {
+        if (o==null) return "null";
+        return o.toString();
+    }
+    
+    public static boolean groovyTruth(Collection<?> c) {
+        return c != null && !c.isEmpty();
+    }
+    public static boolean groovyTruth(String s) {
+        return s != null && !s.isEmpty();
+    }
+    public static boolean groovyTruth(Object o) {
+        // TODO Doesn't handle matchers (see 
http://docs.codehaus.org/display/GROOVY/Groovy+Truth)
+        if (o == null) {
+            return false;
+        } else if (o instanceof Boolean) {
+            return (Boolean)o;
+        } else if (o instanceof String) {
+            return !((String)o).isEmpty();
+        } else if (o instanceof Collection) {
+            return !((Collection)o).isEmpty();
+        } else if (o instanceof Map) {
+            return !((Map)o).isEmpty();
+        } else if (o instanceof Iterator) {
+            return ((Iterator)o).hasNext();
+        } else if (o instanceof Enumeration) {
+            return ((Enumeration)o).hasMoreElements();
+        } else {
+            return true;
+        }
+    }
+    
+    public static <T> Predicate<T> groovyTruthPredicate() {
+        return new Predicate<T>() {
+            @Override public boolean apply(T val) {
+                return groovyTruth(val);
+            }
+        };
+    }
+    
+    public static Function<Object,Boolean> groovyTruthFunction() {
+        return new Function<Object, Boolean>() {
+           @Override public Boolean apply(Object input) {
+               return groovyTruth(input);
+           }
+        };
+    }
+
+    public static <K,V> Map<K,V> mapOf(K key1, V val1) {
+        Map<K,V> result = Maps.newLinkedHashMap();
+        result.put(key1, val1);
+        return result;
+    }
+
+    /** @deprecated since 0.6.0 use {@link Duration#of(Object)} */
+    @Deprecated
+    public static TimeDuration toTimeDuration(Object duration) {
+        // TODO Lazy coding here for large number values; but refactoring away 
from groovy anyway...
+        
+        if (duration == null) {
+            return null;
+        } else if (duration instanceof TimeDuration) {
+            return (TimeDuration) duration;
+        } else if (duration instanceof Number) {
+            long d = ((Number)duration).longValue();
+            if (d <= Integer.MAX_VALUE && d >= Integer.MIN_VALUE) {
+                return new TimeDuration(0,0,0,(int)d);
+            } else {
+                log.warn("Number "+d+" too large to convert to TimeDuration; 
using Integer.MAX_VALUE instead");
+                return new TimeDuration(0,0,0,Integer.MAX_VALUE);
+            }
+        } else {
+            throw new IllegalArgumentException("Cannot convert "+duration+" of 
type "+duration.getClass().getName()+" to a TimeDuration");
+        }
+    }
+
+    public static <T> Predicate<T> toPredicate(final Closure<Boolean> c) {
+        return new Predicate<T>() {
+            @Override public boolean apply(T input) {
+                return c.call(input);
+            }
+        };
+    }
+    
+    @SuppressWarnings("unchecked")
+    public static <T> Callable<T> toCallable(final Runnable job) {
+        return (Callable<T>) ((job instanceof Callable) ? (Callable<T>)job : 
Executors.callable(job));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java 
b/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java
new file mode 100644
index 0000000..7a9b1af
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java
@@ -0,0 +1,180 @@
+/*
+ * 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;
+
+import groovy.io.GroovyPrintStream;
+import groovy.time.TimeDuration;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.stream.StreamGobbler;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+
+import com.google.common.collect.Maps;
+import com.google.common.io.Closer;
+
+/**
+ * @deprecated since 0.7; does not return exit status, stderr, etc, so utility 
is of very limited use; and is not used in core brooklyn at all!;
+ * use ProcessTool or SystemProcessTaskFactory.
+ */
+@Deprecated
+public class ShellUtils {
+
+    public static long TIMEOUT = 60*1000;
+
+    /**
+     * Executes the given command.
+     * <p>
+     * Uses {@code bash -l -c cmd} (to have a good PATH set), and defaults for 
other fields.
+     * <p>
+     * requires a logger and a context object (whose toString is used in the 
logger and in error messages)
+     * optionally takes a string to use as input to the command
+     *
+     * @see {@link #exec(String, String, Logger, Object)}
+     */
+    public static String[] exec(String cmd, Logger log, Object context) {
+        return exec(cmd, null, log, context);
+    }
+    /** @see {@link #exec(String[], String[], File, String, Logger, Object)} */
+    public static String[] exec(String cmd, String input, Logger log, Object 
context) {
+        return exec(new String[] { "bash", "-l", "-c", cmd }, null, null, 
input, log, context);
+    }
+    /** @see {@link #exec(Map, String[], String[], File, String, Logger, 
Object)} */
+    public static String[] exec(Map flags, String cmd, Logger log, Object 
context) {
+        return exec(flags, new String[] { "bash", "-l", "-c", cmd }, null, 
null, null, log, context);
+    }
+    /** @see {@link #exec(Map, String[], String[], File, String, Logger, 
Object)} */
+    public static String[] exec(Map flags, String cmd, String input, Logger 
log, Object context) {
+        return exec(flags, new String[] { "bash", "-l", "-c", cmd }, null, 
null, input, log, context);
+    }
+    /** @see {@link #exec(Map, String[], String[], File, String, Logger, 
Object)} */
+    public static String[] exec(String[] cmd, String[] envp, File dir, String 
input, Logger log, Object context) {
+        return exec(Maps.newLinkedHashMap(), cmd, envp, dir, input, log, 
context);
+    }
+
+    private static long getTimeoutMs(Map flags) {
+        long timeout = TIMEOUT;
+
+        Object tf = flags.get("timeout");
+
+        if (tf instanceof Number) {
+            timeout = ((Number) tf).longValue();
+        } else if (tf instanceof TimeDuration) {
+            timeout = ((TimeDuration) tf).toMilliseconds();
+        }
+
+        //if (tf != null) timeout = tf;
+
+        return timeout;
+    }
+
+    /**
+     * Executes the given command.
+     * <p>
+     * Uses the given environmnet (inherited if null) and cwd ({@literal .} if 
null),
+     * feeding it the given input stream (if not null) and logging I/O at 
debug (if not null).
+     * <p>
+     * flags:  timeout (Duration), 0 for forever; default 60 seconds
+     *
+     * @throws IllegalStateException if return code non-zero
+     * @return lines from stdout.
+     */
+    public static String[] exec(Map flags, final String[] cmd, String[] envp, 
File dir, String input, final Logger log, final Object context) {
+        if (log.isDebugEnabled()) {
+            log.debug("Running local command: {}% {}", context, 
Strings.join(cmd, " "));
+        }
+        Closer closer = Closer.create();
+        try {
+            final Process proc = Runtime.getRuntime().exec(cmd, envp, dir); // 
Call *execute* on the string
+            ByteArrayOutputStream stdoutB = new ByteArrayOutputStream();
+            ByteArrayOutputStream stderrB = new ByteArrayOutputStream();
+            PrintStream stdoutP = new GroovyPrintStream(stdoutB);
+            PrintStream stderrP = new GroovyPrintStream(stderrB);
+            @SuppressWarnings("resource")
+            StreamGobbler stdoutG = new StreamGobbler(proc.getInputStream(), 
stdoutP, log).setLogPrefix("["+context+":stdout] ");
+            stdoutG.start();
+            closer.register(stdoutG);
+            @SuppressWarnings("resource")
+            StreamGobbler stderrG = new StreamGobbler(proc.getErrorStream(), 
stderrP, log).setLogPrefix("["+context+":stderr] ");
+            stderrG.start();
+            closer.register(stderrG);
+            if (input!=null && input.length()>0) {
+                proc.getOutputStream().write(input.getBytes());
+                proc.getOutputStream().flush();
+            }
+
+            final long timeout = getTimeoutMs(flags);
+            final AtomicBoolean ended = new AtomicBoolean(false);
+            final AtomicBoolean killed = new AtomicBoolean(false);
+
+            //if a timeout was specified, this thread will kill the process. 
This is a work around because the process.waitFor'
+            //doesn't accept a timeout.
+            Thread timeoutThread = new Thread(new Runnable() {
+                public void run() {
+                    if (timeout <= 0) return;
+                    try { 
+                        Thread.sleep(timeout);
+                        if (!ended.get()) {
+                            if (log.isDebugEnabled()) {
+                                log.debug("Timeout exceeded for "+context+"% 
"+Strings.join(cmd, " "));
+                            }
+                            proc.destroy();
+                            killed.set(true);
+                        }
+                    } catch (Exception e) { }
+                }
+            });
+            if (timeout > 0) timeoutThread.start();
+            int exitCode = proc.waitFor();
+            ended.set(true);
+            if (timeout > 0) timeoutThread.interrupt();
+
+            stdoutG.blockUntilFinished();
+            stderrG.blockUntilFinished();
+            if (exitCode!=0 || killed.get()) {
+                String message = killed.get() ? "terminated after timeout" : 
"exit code "+exitCode;
+                if (log.isDebugEnabled()) {
+                    log.debug("Completed local command (problem, throwing): 
"+context+"% "+Strings.join(cmd, " ")+" - "+message);
+                }
+                String e = "Command failed ("+message+"): "+Strings.join(cmd, 
" ");
+                log.warn(e+"\n"+stdoutB+(stderrB.size()>0 ? "\n--\n"+stderrB : 
""));
+                throw new IllegalStateException(e+" (details logged)");
+            }
+            if (log.isDebugEnabled()) {
+                log.debug("Completed local command: "+context+"% 
"+Strings.join(cmd, " ")+" - exit code 0");
+            }
+            return stdoutB.toString().split("\n");
+        } catch (IOException e) {
+            throw Exceptions.propagate(e);
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        } finally {
+            Streams.closeQuietly(closer);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java
 
b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java
new file mode 100644
index 0000000..8446e55
--- /dev/null
+++ 
b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java
@@ -0,0 +1,242 @@
+/*
+ * 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.collections;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.util.collections.QuorumCheck.QuorumChecks;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+/** things which it seems should be in guava, but i can't find 
+ * @author alex */
+public class CollectionFunctionals {
+
+    private static final class EqualsSetPredicate implements 
Predicate<Iterable<?>> {
+        private final Iterable<?> target;
+
+        private EqualsSetPredicate(Iterable<?> target) {
+            this.target = target;
+        }
+
+        @Override
+        public boolean apply(@Nullable Iterable<?> input) {
+            if (input==null) return false;
+            return Sets.newHashSet(target).equals(Sets.newHashSet(input));
+        }
+    }
+
+    private static final class KeysOfMapFunction<K> implements Function<Map<K, 
?>, Set<K>> {
+        @Override
+        public Set<K> apply(Map<K, ?> input) {
+            if (input==null) return null;
+            return input.keySet();
+        }
+
+        @Override public String toString() { return "keys"; }
+    }
+
+    private static final class SizeSupplier implements Supplier<Integer> {
+        private final Iterable<?> collection;
+
+        private SizeSupplier(Iterable<?> collection) {
+            this.collection = collection;
+        }
+
+        @Override
+        public Integer get() {
+            return Iterables.size(collection);
+        }
+
+        @Override public String toString() { return 
"sizeSupplier("+collection+")"; }
+    }
+
+    public static final class SizeFunction implements Function<Iterable<?>, 
Integer> {
+        private final Integer valueIfInputNull;
+
+        private SizeFunction(Integer valueIfInputNull) {
+            this.valueIfInputNull = valueIfInputNull;
+        }
+
+        @Override
+        public Integer apply(Iterable<?> input) {
+            if (input==null) return valueIfInputNull;
+            return Iterables.size(input);
+        }
+
+        @Override public String toString() { return "sizeFunction"; }
+    }
+
+    public static Supplier<Integer> sizeSupplier(final Iterable<?> collection) 
{
+        return new SizeSupplier(collection);
+    }
+    
+    public static Function<Iterable<?>, Integer> sizeFunction() { return 
sizeFunction(null); }
+    
+    public static Function<Iterable<?>, Integer> sizeFunction(final Integer 
valueIfInputNull) {
+        return new SizeFunction(valueIfInputNull);
+    }
+
+    public static final class FirstElementFunction<T> implements 
Function<Iterable<? extends T>, T> {
+        private FirstElementFunction() {
+        }
+
+        @Override
+        public T apply(Iterable<? extends T> input) {
+            if (input==null) return null;
+            return Iterables.get(input, 0);
+        }
+
+        @Override public String toString() { return "firstElementFunction"; }
+    }
+
+    public static <T> Function<Iterable<? extends T>, T> firstElement() {
+        return new FirstElementFunction<T>();
+    }
+    
+    public static <K> Function<Map<K,?>,Set<K>> keys() {
+        return new KeysOfMapFunction<K>();
+    }
+
+    public static <K> Function<Map<K, ?>, Integer> mapSize() {
+        return mapSize(null);
+    }
+    
+    public static <K> Function<Map<K, ?>, Integer> mapSize(Integer 
valueIfNull) {
+        return 
Functions.compose(CollectionFunctionals.sizeFunction(valueIfNull), 
CollectionFunctionals.<K>keys());
+    }
+
+    /** default guava Equals predicate will reflect order of target, and will 
fail when matching against a list;
+     * this treats them both as sets */
+    public static Predicate<Iterable<?>> equalsSetOf(Object... target) {
+        return equalsSet(Arrays.asList(target));
+    }
+    public static Predicate<Iterable<?>> equalsSet(final Iterable<?> target) {
+        return new EqualsSetPredicate(target);
+    }
+
+    public static Predicate<Iterable<?>> sizeEquals(int targetSize) {
+        return Predicates.compose(Predicates.equalTo(targetSize), 
CollectionFunctionals.sizeFunction());
+    }
+
+    public static Predicate<Iterable<?>> empty() {
+        return sizeEquals(0);
+    }
+
+    public static Predicate<Iterable<?>> notEmpty() {
+        return Predicates.not(empty());
+    }
+
+    public static <K> Predicate<Map<K,?>> mapSizeEquals(int targetSize) {
+        return Predicates.compose(Predicates.equalTo(targetSize), 
CollectionFunctionals.<K>mapSize());
+    }
+
+    public static <T,I extends Iterable<T>> Function<I, List<T>> limit(final 
int max) {
+        return new LimitFunction<T,I>(max);
+    }
+
+    private static final class LimitFunction<T, I extends Iterable<T>> 
implements Function<I, List<T>> {
+        private final int max;
+        private LimitFunction(int max) {
+            this.max = max;
+        }
+        @Override
+        public List<T> apply(I input) {
+            if (input==null) return null;
+            MutableList<T> result = MutableList.of();
+            for (T i: input) {
+                result.add(i);
+                if (result.size()>=max)
+                    return result;
+            }
+            return result;
+        }
+    }
+
+    // ---------
+    public static <I,T extends Collection<I>> Predicate<T> contains(I item) {
+        return new CollectionContains<I,T>(item);
+    }
+    
+    private static final class CollectionContains<I,T extends Collection<I>> 
implements Predicate<T> {
+        private final I item;
+        private CollectionContains(I item) {
+            this.item = item;
+        }
+        @Override
+        public boolean apply(T input) {
+            if (input==null) return false;
+            return input.contains(item);
+        }
+        @Override
+        public String toString() {
+            return "contains("+item+")";
+        }
+    }
+
+    // ---------
+    
+    public static <T,TT extends Iterable<T>> Predicate<TT> all(Predicate<T> 
attributeSatisfies) {
+        return quorum(QuorumChecks.all(), attributeSatisfies);
+    }
+
+    public static <T,TT extends Iterable<T>> Predicate<TT> quorum(QuorumCheck 
quorumCheck, Predicate<T> attributeSatisfies) {
+        return new QuorumSatisfies<T, TT>(quorumCheck, attributeSatisfies);
+    }
+
+
+    private static final class QuorumSatisfies<I,T extends Iterable<I>> 
implements Predicate<T> {
+        private final Predicate<I> itemCheck;
+        private final QuorumCheck quorumCheck;
+        private QuorumSatisfies(QuorumCheck quorumCheck, Predicate<I> 
itemCheck) {
+            this.itemCheck = Preconditions.checkNotNull(itemCheck, 
"itemCheck");
+            this.quorumCheck = Preconditions.checkNotNull(quorumCheck, 
"quorumCheck");
+        }
+        @Override
+        public boolean apply(T input) {
+            if (input==null) return false;
+            int sizeHealthy = 0, totalSize = 0;
+            for (I item: input) {
+                totalSize++;
+                if (itemCheck.apply(item)) sizeHealthy++;
+            }
+            return quorumCheck.isQuorate(sizeHealthy, totalSize);
+        }
+        @Override
+        public String toString() {
+            return quorumCheck.toString()+"("+itemCheck+")";
+        }
+    }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java
----------------------------------------------------------------------
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java 
b/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java
new file mode 100644
index 0000000..ef7f451
--- /dev/null
+++ 
b/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java
@@ -0,0 +1,581 @@
+/*
+ * 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.collections;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+import javax.annotation.Nonnull;
+
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.primitives.Primitives;
+
+/** Jsonya = JSON-yet-another (tool) 
+ * <p>
+ * provides conveniences for working with maps and lists containing maps and 
lists,
+ * and other datatypes too, easily convertible to json.
+ * <p> 
+ * see {@link JsonyaTest} for examples
+ * 
+ * @since 0.6.0
+ **/
+@Beta
+public class Jsonya {
+
+    private Jsonya() {}
+
+    /** creates a {@link Navigator} backed by the given map (focussed at the 
root) */
+    public static <T extends Map<?,?>> Navigator<T> of(T map) {
+        return new Navigator<T>(map, MutableMap.class);
+    }
+    
+    /** creates a {@link Navigator} backed by the map at the focus of the 
given navigator */
+    public static <T extends Map<?,?>> Navigator<T> of(Navigator<T> navigator) 
{
+        return new Navigator<T>(navigator.getFocusMap(), MutableMap.class);
+    }
+    
+    /** creates a {@link Navigator} backed by a newly created map;
+     * the map can be accessed by {@link Navigator#getMap()} */
+    public static Navigator<MutableMap<Object,Object>> newInstance() {
+        return new Navigator<MutableMap<Object,Object>>(new 
MutableMap<Object,Object>(), MutableMap.class);
+    }
+    /** convenience for {@link Navigator#at(Object, Object...)} on a {@link 
#newInstance()} */
+    public static Navigator<MutableMap<Object,Object>> at(Object 
...pathSegments) {
+        return newInstance().atArray(pathSegments);
+    }
+
+    /** as {@link #newInstance()} but using the given translator to massage 
objects inserted into the Jsonya structure */
+    public static Navigator<MutableMap<Object,Object>> 
newInstanceTranslating(Function<Object,Object> translator) {
+        return newInstance().useTranslator(translator);
+    }
+
+    /** as {@link #newInstanceTranslating(Function)} using an identity function
+     * (functionally equivalent to {@link #newInstance()} but explicit about 
it */
+    public static Navigator<MutableMap<Object,Object>> newInstanceLiteral() {
+        return newInstanceTranslating(Functions.identity());
+    }
+
+    /** as {@link #newInstanceTranslating(Function)} using a function which 
only supports JSON primitives:
+     * maps and collections are traversed, strings and primitives are 
inserted, and everything else has toString applied.
+     * see {@link JsonPrimitiveDeepTranslator} */
+    public static Navigator<MutableMap<Object,Object>> newInstancePrimitive() {
+        return newInstanceTranslating(new JsonPrimitiveDeepTranslator());
+    }
+    
+    /** convenience for converting an object x to something which consists 
only of json primitives, doing
+     * {@link #toString()} on anything which is not recognised. see {@link 
JsonPrimitiveDeepTranslator} */
+    public static Object convertToJsonPrimitive(Object x) {
+        if (x==null) return null;
+        if (x instanceof Map) return 
newInstancePrimitive().put((Map<?,?>)x).getRootMap();
+        return newInstancePrimitive().put("data", x).getRootMap().get("data");
+    }
+
+    /** tells whether {@link #convertToJsonPrimitive(Object)} returns an 
object which is identical to
+     * the equivalent literal json structure. this is typically equivalent to 
saying serializing to json then
+     * deserializing will produce something where the result is equal to the 
input,
+     * modulo a few edge cases such as longs becoming ints.
+     * note that the converse (input equal to output) may not be the case,
+     * e.g. if the input contains special subclasses of collections of maps 
who care about type preservation. */
+    public static boolean isJsonPrimitiveCompatible(Object x) {
+        if (x==null) return true;
+        return convertToJsonPrimitive(x).equals(x);
+    }
+
+    @SuppressWarnings({"rawtypes","unchecked"})
+    public static class Navigator<T extends Map<?,?>> {
+
+        protected final Object root;
+        protected final Class<? extends Map> mapType;
+        protected Object focus;
+        protected Stack<Object> focusStack = new Stack<Object>();
+        protected Function<Object,Void> creationInPreviousFocus;
+        protected Function<Object,Object> translator;
+
+        public Navigator(Object backingStore, Class<? extends Map> mapType) {
+            this.root = Preconditions.checkNotNull(backingStore);
+            this.focus = backingStore;
+            this.mapType = mapType;
+        }
+        
+        // -------------- access and configuration
+        
+        /** returns the object at the focus, or null if none */
+        public Object get() {
+            return focus;
+        }
+
+        /** as {@link #get()} but always wrapped in a {@link Maybe}, absent if 
null */
+        public @Nonnull Maybe<Object> getMaybe() {
+            return Maybe.fromNullable(focus);
+        }
+        
+        /** returns the object at the focus, casted to the given type, null if 
none
+         * @throws ClassCastException if object exists here but of the wrong 
type  */
+        public <V> V get(Class<V> type) {
+            return (V)focus;
+        }
+
+        /** as {@link #get(Class)} but always wrapped in a {@link Maybe}, 
absent if null
+         * @throws ClassCastException if object exists here but of the wrong 
type  */
+        public @Nonnull <V> Maybe<V> getMaybe(Class<V> type) {
+            return Maybe.fromNullable(get(type));
+        }
+
+        /** gets the object at the indicated path from the current focus
+         * (without changing the path to that focus; use {@link #at(Object, 
Object...)} to change focus) */
+        // Jun 2014, semantics changed so that focus does not change, which is 
more natural
+        public Object get(Object pathSegment, Object ...furtherPathSegments) {
+            push();
+            at(pathSegment, furtherPathSegments);
+            Object result = get();
+            pop();
+            return result;
+        }
+        
+        public Navigator<T> root() {
+            focus = root;
+            return this;
+        }
+
+        /** returns the object at the root */
+        public Object getRoot() {
+            return root;
+        }
+        
+        /** returns the {@link Map} at the root, throwing if root is not a map 
*/
+        public T getRootMap() {
+            return (T) root;
+        }
+
+        /** returns a {@link Map} at the given focus, creating if needed (so 
never null),
+         * throwing if it exists already and is not a map */
+        public T getFocusMap() {
+            map();
+            return (T)focus;
+        }
+        
+        /** as {@link #getFocusMap()} but always wrapped in a {@link Maybe}, 
absent if null
+         * @throws ClassCastException if object exists here but of the wrong 
type  */
+        public @Nonnull Maybe<T> getFocusMapMaybe() {
+            return Maybe.fromNullable(getFocusMap());
+        }
+
+        /** specifies a translator function to use when new data is added;
+         * by default everything is added as a literal (ie {@link 
Functions#identity()}), 
+         * but if you want to do translation on the way in,
+         * set a translation function
+         * <p>
+         * note that translation should be idempotent as implementation may 
apply it multiple times in certain cases
+         */
+        public Navigator<T> useTranslator(Function<Object,Object> translator) {
+            this.translator = translator;
+            return this;
+        }
+        
+        protected Object translate(Object x) {
+            if (translator==null) return x;
+            return translator.apply(x);
+        }
+
+        protected Object translateKey(Object x) {
+            if (translator==null) return x;
+            // this could return the toString to make it strict json
+            // but json libraries seem to do that so not strictly necessary
+            return translator.apply(x);
+        }
+
+        // ------------- navigation (map mainly)
+
+        /** pushes the current focus to a stack, so that this location will be 
restored on the corresponding {@link #pop()} */
+        public Navigator<T> push() {
+            focusStack.push(focus);
+            return this;
+        }
+        
+        /** pops the most recently pushed focus, so that it returns to the 
last location {@link #push()}ed */
+        public Navigator<T> pop() {
+            focus = focusStack.pop();
+            return this;
+        }
+        
+        /** returns the navigator moved to focus at the indicated key sequence 
in the given map */
+        public Navigator<T> at(Object pathSegment, Object 
...furtherPathSegments) {
+            down(pathSegment);
+            return atArray(furtherPathSegments);
+        }
+        public Navigator<T> atArray(Object[] furtherPathSegments) {
+            for (Object p: furtherPathSegments)
+                down(p);
+            return this;
+        }
+        
+        /** ensures the given focus is a map, creating if needed (and creating 
inside the list if it is in a list) */
+        public Navigator<T> map() {
+            if (focus==null) {
+                focus = newMap();
+                creationInPreviousFocus.apply(focus);
+            }
+            if (focus instanceof List) {
+                Map m = newMap();
+                ((List)focus).add(translate(m));
+                focus = m;
+                return this;
+            }
+            if (!(focus instanceof Map))
+                throw new IllegalStateException("focus here is "+focus+"; 
expected a map");
+            return this;
+        }
+
+        /** puts the given key-value pair at the current focus (or multiple 
such), 
+         *  creating a map if needed, replacing any values stored against keys 
supplied here;
+         *  if you wish to merge deep maps, see {@link #add(Object, 
Object...)} */
+        public Navigator<T> put(Object k1, Object v1, Object ...kvOthers) {
+            map();
+            putInternal((Map)focus, k1, v1, kvOthers);
+            return this;
+        }
+        
+        public Navigator<T> putIfNotNull(Object k1, Object v1) {
+            if (v1!=null) {
+                map();
+                putInternal((Map)focus, k1, v1);
+            }
+            return this;
+        }
+        
+        protected void putInternal(Map target, Object k1, Object v1, Object 
...kvOthers) {
+            assert (kvOthers.length % 2) == 0 : "even number of arguments 
required for put";
+            target.put(translateKey(k1), translate(v1));
+            for (int i=0; i<kvOthers.length; ) {
+                target.put(translateKey(kvOthers[i++]), 
translate(kvOthers[i++]));    
+            }
+        }
+
+        /** as {@link #put(Object, Object, Object...)} for the kv-pairs in the 
given map; ignores null for convenience */
+        public Navigator<T> put(Map map) {
+            map();
+            if (map==null) return this;
+            ((Map)focus).putAll((Map)translate(map));
+            return this;
+        }
+        
+        protected Map newMap() {
+            try {
+                return mapType.newInstance();
+            } catch (Exception e) {
+                throw Throwables.propagate(e);
+            }
+        }
+
+        /** utility for {@link #at(Object, Object...)}, taking one argument at 
a time */
+        protected Navigator<T> down(final Object pathSegment) {
+            if (focus instanceof List) {
+                return downList(pathSegment);
+            }
+            if ((focus instanceof Map) || focus==null) {
+                return downMap(pathSegment);
+            }
+            throw new IllegalStateException("focus here is "+focus+"; cannot 
descend to '"+pathSegment+"'");
+        }
+
+        protected Navigator<T> downMap(Object pathSegmentO) {
+            final Object pathSegment = translateKey(pathSegmentO);
+            final Map givenParentMap = (Map)focus;
+            if (givenParentMap!=null) {
+                creationInPreviousFocus = null;
+                focus = givenParentMap.get(pathSegment);
+            }
+            if (focus==null) {
+                final Function<Object, Void> previousCreation = 
creationInPreviousFocus;
+                creationInPreviousFocus = new Function<Object, Void>() {
+                    public Void apply(Object input) {
+                        creationInPreviousFocus = null;
+                        Map parentMap = givenParentMap;
+                        if (parentMap==null) {
+                            parentMap = newMap();
+                            previousCreation.apply(parentMap);
+                        }
+                        parentMap.put(pathSegment, translate(input));
+                        return null;
+                    }
+                };
+            }
+            return this;
+        }
+
+        protected Navigator<T> downList(final Object pathSegment) {
+            if (!(pathSegment instanceof Integer))
+                throw new IllegalStateException("focus here is a list 
("+focus+"); cannot descend to '"+pathSegment+"'");
+            final List givenParentList = (List)focus;
+            // previous focus always non-null
+            creationInPreviousFocus = null;
+            focus = givenParentList.get((Integer)pathSegment);
+            if (focus==null) {
+                // don't need to worry about creation here; we don't create 
list entries simply by navigating
+                // TODO a nicer architecture would create a new object with 
focus for each traversal
+                // in that case we could create, filling other positions with 
null; but is there a need?
+                creationInPreviousFocus = new Function<Object, Void>() {
+                    public Void apply(Object input) {
+                        throw new IllegalStateException("cannot create 
"+input+" here because we are at a non-existent position in a list");
+                    }
+                };
+            }
+            return this;
+        }
+
+        // ------------- navigation (list mainly)
+
+        /** ensures the given focus is a list */
+        public Navigator<T> list() {
+            if (focus==null) {
+                focus = newList();
+                creationInPreviousFocus.apply(focus);
+            }
+            if (!(focus instanceof List))
+                throw new IllegalStateException("focus here is "+focus+"; 
expected a list");
+            return this;
+        }
+
+        protected List newList() {
+            return new ArrayList();
+        }
+        
+        /** adds the given items to the focus, whether a list or a map,
+         * creating the focus as a map if it doesn't already exist.
+         * to add items to a list which might not exist, precede by a call to 
{@link #list()}.
+         * <p>
+         * when adding items to a list, iterable and array arguments are 
flattened because 
+         * that makes the most sense when working with deep maps (adding one 
map to another where both contain lists, for example); 
+         * to prevent flattening use {@link #addUnflattened(Object, 
Object...)} 
+         * <p>
+         * when adding to a map, arguments will be treated as things to put 
into the map,
+         * accepting either multiple arguments, as key1, value1, key2, value2, 
...
+         * (and must be an event number); or a single argument which must be a 
map,
+         * in which case the value for each key in the supplied map is added 
to any existing value against that key in the target map
+         * (in other words, it will do a "deep put", where nested maps are 
effectively merged)
+         * <p>
+         * this implementation will currently throw if you attempt to add a 
non-map to anything present which is not a list;
+         * auto-conversion to a list may be added in a future version
+         * */
+        public Navigator<T> add(Object o1, Object ...others) {
+            if (focus==null) map();
+            addInternal(focus, focus, o1, others);
+            return this;
+        }
+
+        /** adds the given arguments to a list at this point (will not descend 
into maps, and will not flatten lists) */
+        public Navigator<T> addUnflattened(Object o1, Object ...others) {
+            ((Collection)focus).add(translate(o1));
+            for (Object oi: others) ((Collection)focus).add(translate(oi));
+            return this;
+        }
+        
+        protected void addInternal(Object initialFocus, Object currentFocus, 
Object o1, Object ...others) {
+            if (currentFocus instanceof Map) {
+                Map target = (Map)currentFocus;
+                Map source;
+                if (others.length==0) {
+                    // add as a map
+                    if (o1==null)
+                        // ignore if null
+                        return ;
+                    if (!(o1 instanceof Map))
+                        throw new IllegalStateException("cannot add: focus 
here is "+currentFocus+" (in "+initialFocus+"); expected a collection, or a map 
(with a map being added, not "+o1+")");
+                    source = (Map)translate(o1);
+                } else {
+                    // build a source map from the arguments as key-value pairs
+                    if ((others.length % 2)==0)
+                        throw new IllegalArgumentException("cannot add an odd 
number of arguments to a map" +
+                                " ("+o1+" then "+Arrays.toString(others)+" in 
"+currentFocus+" in "+initialFocus+")");
+                    source = MutableMap.of(translateKey(o1), 
translate(others[0]));
+                    for (int i=1; i<others.length; )
+                        source.put(translateKey(others[i++]), 
translate(others[i++]));
+                }
+                // and add the source map to the target
+                for (Object entry : source.entrySet()) {
+                    Object key = ((Map.Entry)entry).getKey();
+                    Object sv = ((Map.Entry)entry).getValue();
+                    Object tv = target.get(key);
+                    if (!target.containsKey(key)) {
+                        target.put(key, sv);
+                    } else {
+                        addInternal(initialFocus, tv, sv);
+                    }
+                }
+                return;
+            }
+            // lists are easy to add to, but remember we have to flatten
+            if (!(currentFocus instanceof Collection))
+                // TODO a nicer architecture might replace the current target 
with a list (also above where single non-map argument is supplied)
+                throw new IllegalStateException("cannot add: focus here is 
"+currentFocus+"; expected a collection");
+            addFlattened((Collection)currentFocus, o1);
+            for (Object oi: others) addFlattened((Collection)currentFocus, 
oi); 
+        }
+
+        protected void addFlattened(Collection target, Object item) {
+            if (item instanceof Iterable) {
+                for (Object i: (Iterable)item)
+                    addFlattened(target, i);
+                return;
+            }
+            if (item.getClass().isArray()) {
+                for (Object i: ((Object[])item))
+                    addFlattened(target, i);
+                return;
+            }
+            // nothing to flatten
+            target.add(translate(item));
+        }
+        
+        /** Returns JSON serialized output for given focus in the given jsonya;
+         * applies a naive toString for specialized types */
+        @Override
+        public String toString() {
+            return render(get());
+        }
+    }
+
+    public static String render(Object focus) {
+        if (focus instanceof Map) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            boolean first = true;
+            for (Object entry: ((Map<?,?>)focus).entrySet()) {
+                if (!first) sb.append(",");
+                else first = false;
+                sb.append(" ");
+                sb.append( render(((Map.Entry<?,?>)entry).getKey()) );
+                sb.append(": ");
+                sb.append( render(((Map.Entry<?,?>)entry).getValue()) );
+            }
+            sb.append(" }");
+            return sb.toString();
+        }
+        if (focus instanceof Collection) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("[");
+            boolean first = true;
+            for (Object entry: (Collection<?>)focus) {
+                if (!first) sb.append(",");
+                else first = false;
+                sb.append( render(entry) );
+            }
+            sb.append(" ]");
+            return sb.toString();
+        }
+        if (focus instanceof String) {
+            return JavaStringEscapes.wrapJavaString((String)focus);
+        }
+        if (focus == null || focus instanceof Number || focus instanceof 
Boolean)
+            return ""+focus;
+        
+        return render(""+focus);
+    }
+
+    /** Converts an object to one which uses standard JSON objects where 
possible
+     * (strings, numbers, booleans, maps, lists), and uses toString elsewhere 
*/
+    public static class JsonPrimitiveDeepTranslator implements 
Function<Object,Object> {
+        public static JsonPrimitiveDeepTranslator INSTANCE = new 
JsonPrimitiveDeepTranslator();
+        
+        /** No need to instantiate except when subclassing. Use static {@link 
#INSTANCE}. */
+        protected JsonPrimitiveDeepTranslator() {}
+        
+        @Override
+        public Object apply(Object input) {
+            return apply(input, new HashSet<Object>());
+        }
+        
+        protected Object apply(Object input, Set<Object> stack) {
+            if (input==null) return applyNull(stack);
+            
+            if (isPrimitiveOrBoxer(input.getClass()))
+                return applyPrimitiveOrBoxer(input, stack);
+            
+            if (input instanceof String)
+                return applyString((String)input, stack);
+            
+            stack = new HashSet<Object>(stack);
+            if (!stack.add(input))
+                // fail if object is self-recursive; don't even try toString 
as that is dangerous
+                // (extra measure of safety, since maps and lists generally 
fail elsewhere with recursive entries, 
+                // eg in hashcode or toString)
+                return "[REF_ANCESTOR:"+stack.getClass()+"]";
+
+            if (input instanceof Collection<?>)
+                return applyCollection( (Collection<?>)input, stack );
+            
+            if (input instanceof Map<?,?>)
+                return applyMap( (Map<?,?>)input, stack );
+
+            return applyOther(input, stack);
+        }
+
+        protected Object applyNull(Set<Object> stack) {
+            return null;
+        }
+
+        protected Object applyPrimitiveOrBoxer(Object input, Set<Object> 
stack) {
+            return input;
+        }
+
+        protected Object applyString(String input, Set<Object> stack) {
+            return input.toString();
+        }
+
+        protected Object applyCollection(Collection<?> input, Set<Object> 
stack) {
+            MutableList<Object> result = MutableList.of();
+            
+            for (Object xi: input)
+                result.add(apply(xi, stack));
+
+            return result;
+        }
+
+        protected Object applyMap(Map<?, ?> input, Set<Object> stack) {
+            MutableMap<Object, Object> result = MutableMap.of();
+            
+            for (Map.Entry<?,?> xi: input.entrySet())
+                result.put(apply(xi.getKey(), stack), apply(xi.getValue(), 
stack));
+
+            return result;
+        }
+
+        protected Object applyOther(Object input, Set<Object> stack) {
+            return input.toString();
+        }        
+
+        public static boolean isPrimitiveOrBoxer(Class<?> type) {
+            return Primitives.allPrimitiveTypes().contains(type) || 
Primitives.allWrapperTypes().contains(type);
+        }
+    }
+
+}

Reply via email to