You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ha...@apache.org on 2015/08/18 17:03:27 UTC

[18/64] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - apply org.apache package prefix to utils-common

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);
+        }
+    }
+
+}