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/17 21:18:00 UTC

[29/42] incubator-brooklyn git commit: [BROOKLYN-162] Refactor package in ./core/util

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/config/ConfigBag.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/config/ConfigBag.java b/core/src/main/java/org/apache/brooklyn/core/util/config/ConfigBag.java
new file mode 100644
index 0000000..0152bb9
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/config/ConfigBag.java
@@ -0,0 +1,589 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.config;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.ConcurrentModificationException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.flags.TypeCoercions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.config.ConfigKey.HasConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.javalang.JavaClassNames;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Objects;
+import com.google.common.collect.Sets;
+
+/**
+ * Stores config in such a way that usage can be tracked.
+ * Either {@link ConfigKey} or {@link String} keys can be inserted;
+ * they will be stored internally as strings.
+ * It is recommended to use {@link ConfigKey} instances to access,
+ * although in some cases (such as setting fields from flags, or copying a map)
+ * it may be necessary to mark things as used, or put, when only a string key is available.
+ * <p>
+ * This bag is order-preserving and thread-safe except where otherwise indicated,
+ * currently by synching on this instance (but that behaviour may change).
+ * <p>
+ * @author alex
+ */
+public class ConfigBag {
+
+    private static final Logger log = LoggerFactory.getLogger(ConfigBag.class);
+
+    /** an immutable, empty ConfigBag */
+    public static final ConfigBag EMPTY = new ConfigBag().setDescription("immutable empty config bag").seal();
+    
+    protected String description;
+    
+    private Map<String,Object> config;
+    private final Map<String,Object> unusedConfig;
+    private final boolean live;
+    private boolean sealed = false;
+
+    /** creates a new ConfigBag instance, empty and ready for population */
+    public static ConfigBag newInstance() {
+        return new ConfigBag();
+    }
+
+    /**
+     * Creates an instance that is backed by a "live map" (e.g. storage in a datagrid).
+     * The order-preserving nature of this class is only guaranteed if the
+     * provided storage has those properties. External modifications to the store can cause
+     * {@link ConcurrentModificationException} to be thrown, here or elsewhere. 
+     */
+    public static ConfigBag newLiveInstance(Map<String,Object> storage) {
+        return new ConfigBag(checkNotNull(storage, "storage map must be specified"));
+    }
+
+    public static ConfigBag newInstance(Map<?, ?> config) {
+        ConfigBag result = new ConfigBag();
+        result.putAll(config);
+        return result;
+    }
+
+    /** creates a new ConfigBag instance which includes all of the supplied ConfigBag's values,
+     * but which tracks usage separately (already used values are marked as such,
+     * but uses in the original set will not be marked here, and vice versa) */
+    public static ConfigBag newInstanceCopying(final ConfigBag configBag) {
+        return new ConfigBag().copy(configBag).setDescription(configBag.getDescription());
+    }
+    
+    /** creates a new ConfigBag instance which includes all of the supplied ConfigBag's values,
+     * plus an additional set of &lt;ConfigKey,Object&gt; or &lt;String,Object&gt; pairs
+     * <p>
+     * values from the original set which are used here will be marked as used in the original set
+     * (note: this applies even for values which are overridden and the overridden value is used);
+     * however subsequent uses in the original set will not be marked here
+     */
+    @Beta
+    public static ConfigBag newInstanceExtending(final ConfigBag parentBag) {
+        return new ConfigBagExtendingParent(parentBag);
+    }
+
+    /** @see #newInstanceExtending(ConfigBag) */
+    private static class ConfigBagExtendingParent extends ConfigBag {
+        ConfigBag parentBag;
+        private ConfigBagExtendingParent(ConfigBag parentBag) {
+            this.parentBag = parentBag;
+            copy(parentBag);
+        }
+        @Override
+        public void markUsed(String key) {
+            super.markUsed(key);
+            if (parentBag!=null)
+                parentBag.markUsed(key);
+        }
+    }
+    
+    /** As {@link #newInstanceExtending(ConfigBag)} but also putting the supplied values. */
+    @Beta
+    public static ConfigBag newInstanceExtending(final ConfigBag configBag, Map<?,?> optionalAdditionalValues) {
+        return newInstanceExtending(configBag).putAll(optionalAdditionalValues);
+    }
+
+    /** @deprecated since 0.7.0, not used; kept only for rebind compatibility where the inner class is used 
+     * (now replaced by a static class above) */
+    @Beta @Deprecated
+    public static ConfigBag newInstanceWithInnerClass(final ConfigBag configBag, Map<?,?> optionalAdditionalValues) {
+        return new ConfigBag() {
+            @Override
+            public void markUsed(String key) {
+                super.markUsed(key);
+                configBag.markUsed(key);
+            }
+        }.copy(configBag).putAll(optionalAdditionalValues);
+    }
+
+    public ConfigBag() {
+        config = new LinkedHashMap<String,Object>();
+        unusedConfig = new LinkedHashMap<String,Object>();
+        live = false;
+    }
+    
+    private ConfigBag(Map<String,Object> storage) {
+        this.config = storage;
+        unusedConfig = new LinkedHashMap<String,Object>();
+        live = true;
+    }
+    
+    public ConfigBag setDescription(String description) {
+        if (sealed) 
+            throw new IllegalStateException("Cannot set description to '"+description+"': this config bag has been sealed and is now immutable.");
+        this.description = description;
+        return this;
+    }
+    
+    /** optional description used to provide context for operations */
+    public String getDescription() {
+        return description;
+    }
+    
+    /** current values for all entries 
+     * @return non-modifiable map of strings to object */
+    public synchronized Map<String,Object> getAllConfig() {
+        return MutableMap.copyOf(config).asUnmodifiable();
+    }
+
+    /** current values for all entries in a map where the keys are converted to {@link ConfigKey} instances */
+    public synchronized Map<ConfigKey<?>, ?> getAllConfigAsConfigKeyMap() {
+        Map<ConfigKey<?>,Object> result = MutableMap.of();
+        for (Map.Entry<String,Object> entry: config.entrySet()) {
+            result.put(ConfigKeys.newConfigKey(Object.class, entry.getKey()), entry.getValue());
+        }
+        return result;
+    }
+
+    /** Returns the internal map containing the current values for all entries;
+     * for use where the caller wants to modify this directly and knows it is safe to do so 
+     * <p>
+     * Accesses to the returned map must be synchronized on this bag if the 
+     * thread-safe behaviour is required. */ 
+    public Map<String,Object> getAllConfigMutable() {
+        if (live) {
+            // TODO sealed no longer works as before, because `config` is the backing storage map.
+            // Therefore returning it is dangerous! Even if we were to replace our field with an immutable copy,
+            // the underlying datagrid's map would still be modifiable. We need a way to switch the returned
+            // value's behaviour to sealable (i.e. wrapping the returned map).
+            return (sealed) ? MutableMap.copyOf(config).asUnmodifiable() : config;
+        } else {
+            return config;
+        }
+    }
+
+    /** current values for all entries which have not yet been used 
+     * @return non-modifiable map of strings to object */
+    public synchronized Map<String,Object> getUnusedConfig() {
+        return MutableMap.copyOf(unusedConfig).asUnmodifiable();
+    }
+
+    /** Returns the internal map containing the current values for all entries which have not yet been used;
+     * for use where the caller wants to modify this directly and knows it is safe to do so 
+     * <p>
+     * Accesses to the returned map must be synchronized on this bag if the 
+     * thread-safe behaviour is required. */ 
+    public Map<String,Object> getUnusedConfigMutable() {
+        return unusedConfig;
+    }
+
+    public ConfigBag putAll(Map<?,?> addlConfig) {
+        if (addlConfig==null) return this;
+        for (Map.Entry<?,?> e: addlConfig.entrySet()) {
+            putAsStringKey(e.getKey(), e.getValue());
+        }
+        return this;
+    }
+    
+    public ConfigBag putAll(ConfigBag addlConfig) {
+        return putAll(addlConfig.getAllConfig());
+    }
+    
+    public <T> ConfigBag putIfAbsent(ConfigKey<T> key, T value) {
+        return putIfAbsent(MutableMap.of(key, value));
+    }
+
+    public ConfigBag putAsStringKeyIfAbsent(Object key, Object value) {
+        return putIfAbsent(MutableMap.of(key, value));
+    }
+
+    public synchronized ConfigBag putIfAbsent(Map<?, ?> propertiesToSet) {
+        if (propertiesToSet==null)
+            return this;
+        for (Map.Entry<?, ?> entry: propertiesToSet.entrySet()) {
+            Object key = entry.getKey();
+            if (key instanceof HasConfigKey<?>)
+                key = ((HasConfigKey<?>)key).getConfigKey();
+            if (key instanceof ConfigKey<?>) {
+                if (!containsKey((ConfigKey<?>)key))
+                    putAsStringKey(key, entry.getValue());
+            } else if (key instanceof String) {
+                if (!containsKey((String)key))
+                    putAsStringKey(key, entry.getValue());
+            } else {
+                logInvalidKey(key);
+            }
+        }
+        return this;
+    }
+
+    public ConfigBag putIfAbsent(ConfigBag addlConfig) {
+        return putIfAbsent(addlConfig.getAllConfig());
+    }
+
+
+    @SuppressWarnings("unchecked")
+    public <T> T put(ConfigKey<T> key, T value) {
+        return (T) putStringKey(key.getName(), value);
+    }
+    
+    public <T> ConfigBag putIfNotNull(ConfigKey<T> key, T value) {
+        if (value!=null) put(key, value);
+        return this;
+    }
+
+    public <T> ConfigBag putIfAbsentAndNotNull(ConfigKey<T> key, T value) {
+        if (value!=null) putIfAbsent(key, value);
+        return this;
+    }
+
+    /** as {@link #put(ConfigKey, Object)} but returning this ConfigBag for fluent-style coding */
+    public <T> ConfigBag configure(ConfigKey<T> key, T value) {
+        putStringKey(key.getName(), value);
+        return this;
+    }
+    
+    public <T> ConfigBag configureStringKey(String key, T value) {
+        putStringKey(key, value);
+        return this;
+    }
+    
+    protected synchronized void putAsStringKey(Object key, Object value) {
+        if (key instanceof HasConfigKey<?>) key = ((HasConfigKey<?>)key).getConfigKey();
+        if (key instanceof ConfigKey<?>) key = ((ConfigKey<?>)key).getName();
+        if (key instanceof String) {
+            putStringKey((String)key, value);
+        } else {
+            logInvalidKey(key);
+        }
+    }
+
+    protected void logInvalidKey(Object key) {
+        String message = (key == null ? "Invalid key 'null'" : "Invalid key type "+key.getClass().getCanonicalName()+" ("+key+")") +
+                " being used for configuration, ignoring";
+        log.debug(message, new Throwable("Source of "+message));
+        log.warn(message);
+    }
+    
+    /** recommended to use {@link #put(ConfigKey, Object)} but there are times
+     * (e.g. when copying a map) where we want to put a string key directly 
+     */
+    public synchronized Object putStringKey(String key, Object value) {
+        if (sealed) 
+            throw new IllegalStateException("Cannot insert "+key+"="+value+": this config bag has been sealed and is now immutable.");
+        boolean isNew = !config.containsKey(key);
+        boolean isUsed = !isNew && !unusedConfig.containsKey(key);
+        Object old = config.put(key, value);
+        if (!isUsed) 
+            unusedConfig.put(key, value);
+        //if (!isNew && !isUsed) log.debug("updating config value which has already been used");
+        return old;
+    }
+    public Object putStringKeyIfHasValue(String key, Maybe<?> value) {
+        if (value.isPresent())
+            return putStringKey(key, value.get());
+        return null;
+    }
+    public Object putStringKeyIfNotNull(String key, Object value) {
+        if (value!=null)
+            return putStringKey(key, value);
+        return null;
+    }
+
+    public boolean containsKey(HasConfigKey<?> key) {
+        return containsKey(key.getConfigKey());
+    }
+
+    public boolean containsKey(ConfigKey<?> key) {
+        return containsKey(key.getName());
+    }
+
+    public synchronized boolean containsKey(String key) {
+        return config.containsKey(key);
+    }
+
+    /** returns the value of this config key, falling back to its default (use containsKey to see whether it was contained);
+     * also marks it as having been used (use peek to prevent marking as used)
+     */
+    public <T> T get(ConfigKey<T> key) {
+        return get(key, true);
+    }
+
+    /** gets a value from a string-valued key or null; ConfigKey is preferred, but this is useful in some contexts (e.g. setting from flags) */
+    public Object getStringKey(String key) {
+        return getStringKeyMaybe(key).orNull();
+    }
+    /** gets a {@link Maybe}-wrapped value from a string-valued key; ConfigKey is preferred, but this is useful in some contexts (e.g. setting from flags) */
+    public @Nonnull Maybe<Object> getStringKeyMaybe(String key) {
+        return getStringKeyMaybe(key, true);
+    }
+
+    /** gets a {@link Maybe}-wrapped value from a key, inferring the type of that key (e.g. {@link ConfigKey} or {@link String}) */
+    @Beta
+    public Maybe<Object> getObjKeyMaybe(Object key) {
+        if (key instanceof HasConfigKey<?>) key = ((HasConfigKey<?>)key).getConfigKey();
+        if (key instanceof ConfigKey<?>) key = ((ConfigKey<?>)key).getName();
+        if (key instanceof String) {
+            return getStringKeyMaybe((String)key, true);
+        } else {
+            logInvalidKey(key);
+            return Maybe.absent();
+        }
+    }
+
+    /** like get, but without marking it as used */
+    public <T> T peek(ConfigKey<T> key) {
+        return get(key, false);
+    }
+
+    /** returns the first key in the list for which a value is explicitly set, then defaulting to defaulting value of preferred key */
+    public synchronized <T> T getFirst(ConfigKey<T> preferredKey, ConfigKey<T> ...otherCurrentKeysInOrderOfPreference) {
+        if (containsKey(preferredKey)) 
+            return get(preferredKey);
+        for (ConfigKey<T> key: otherCurrentKeysInOrderOfPreference) {
+            if (containsKey(key)) 
+                return get(key);
+        }
+        return get(preferredKey);
+    }
+
+    /** convenience for @see #getWithDeprecation(ConfigKey[], ConfigKey...) */
+    public Object getWithDeprecation(ConfigKey<?> key, ConfigKey<?> ...deprecatedKeys) {
+        return getWithDeprecation(new ConfigKey[] { key }, deprecatedKeys);
+    }
+
+    /** returns the value for the first key in the list for which a value is set,
+     * warning if any of the deprecated keys have a value which is different to that set on the first set current key
+     * (including warning if a deprecated key has a value but no current key does) */
+    public synchronized Object getWithDeprecation(ConfigKey<?>[] currentKeysInOrderOfPreference, ConfigKey<?> ...deprecatedKeys) {
+        // Get preferred key (or null)
+        ConfigKey<?> preferredKeyProvidingValue = null;
+        Object result = null;
+        boolean found = false;
+        for (ConfigKey<?> key: currentKeysInOrderOfPreference) {
+            if (containsKey(key)) {
+                preferredKeyProvidingValue = key;
+                result = get(preferredKeyProvidingValue);
+                found = true;
+                break;
+            }
+        }
+        
+        // Check if any deprecated keys are set
+        ConfigKey<?> deprecatedKeyProvidingValue = null;
+        Object deprecatedResult = null;
+        boolean foundDeprecated = false;
+        for (ConfigKey<?> deprecatedKey: deprecatedKeys) {
+            Object x = null;
+            boolean foundX = false;
+            if (containsKey(deprecatedKey)) {
+                x = get(deprecatedKey);
+                foundX = true;
+            }
+            if (foundX) {
+                if (found) {
+                    if (!Objects.equal(result, x)) {
+                        log.warn("Conflicting value from deprecated key " +deprecatedKey+", value "+x+
+                                "; using preferred key "+preferredKeyProvidingValue+" value "+result);
+                    } else {
+                        log.info("Deprecated key " +deprecatedKey+" ignored; has same value as preferred key "+preferredKeyProvidingValue+" ("+result+")");
+                    }
+                } else if (foundDeprecated) {
+                    if (!Objects.equal(result, x)) {
+                        log.warn("Conflicting values from deprecated keys: using " +deprecatedKeyProvidingValue+" instead of "+deprecatedKey+
+                                " (value "+deprecatedResult+" instead of "+x+")");
+                    } else {
+                        log.info("Deprecated key " +deprecatedKey+" ignored; has same value as other deprecated key "+preferredKeyProvidingValue+" ("+deprecatedResult+")");
+                    }
+                } else {
+                    // new value, from deprecated key
+                    log.warn("Deprecated key " +deprecatedKey+" detected (supplying value "+x+"), "+
+                            "; recommend changing to preferred key '"+currentKeysInOrderOfPreference[0]+"'; this will not be supported in future versions");
+                    deprecatedResult = x;
+                    deprecatedKeyProvidingValue = deprecatedKey;
+                    foundDeprecated = true;
+                }
+            }
+        }
+        
+        if (found) {
+            return result;
+        } else if (foundDeprecated) {
+            return deprecatedResult;
+        } else {
+            return currentKeysInOrderOfPreference[0].getDefaultValue();
+        }
+    }
+
+    protected <T> T get(ConfigKey<T> key, boolean markUsed) {
+        // TODO for now, no evaluation -- maps / closure content / other smart (self-extracting) keys are NOT supported
+        // (need a clean way to inject that behaviour, as well as desired TypeCoercions)
+        // this method, and the coercion, is not synchronized, nor does it need to be, because the "get" is synchronized. 
+        return coerceFirstNonNullKeyValue(key, getStringKey(key.getName(), markUsed));
+    }
+
+    /** returns the first non-null value to be the type indicated by the key, or the keys default value if no non-null values are supplied */
+    public static <T> T coerceFirstNonNullKeyValue(ConfigKey<T> key, Object ...values) {
+        for (Object o: values)
+            if (o!=null) return TypeCoercions.coerce(o, key.getTypeToken());
+        return TypeCoercions.coerce(key.getDefaultValue(), key.getTypeToken());
+    }
+
+    protected Object getStringKey(String key, boolean markUsed) {
+        return getStringKeyMaybe(key, markUsed).orNull();
+    }
+    protected synchronized Maybe<Object> getStringKeyMaybe(String key, boolean markUsed) {
+        if (config.containsKey(key)) {
+            if (markUsed) markUsed(key);
+            return Maybe.of(config.get(key));
+        }
+        return Maybe.absent();
+    }
+
+    /** indicates that a string key in the config map has been accessed */
+    public synchronized void markUsed(String key) {
+        unusedConfig.remove(key);
+    }
+
+    public synchronized void clear() {
+        if (sealed) 
+            throw new IllegalStateException("Cannot clear this config bag has been sealed and is now immutable.");
+        config.clear();
+        unusedConfig.clear();
+    }
+    
+    public ConfigBag removeAll(ConfigKey<?> ...keys) {
+        for (ConfigKey<?> key: keys) remove(key);
+        return this;
+    }
+
+    public synchronized void remove(ConfigKey<?> key) {
+        remove(key.getName());
+    }
+
+    public ConfigBag removeAll(Iterable<String> keys) {
+        for (String key: keys) remove(key);
+        return this;
+    }
+
+    public synchronized void remove(String key) {
+        if (sealed) 
+            throw new IllegalStateException("Cannot remove "+key+": this config bag has been sealed and is now immutable.");
+        config.remove(key);
+        unusedConfig.remove(key);
+    }
+
+    public ConfigBag copy(ConfigBag other) {
+        // ensure locks are taken in a canonical order to prevent deadlock
+        if (other==null) {
+            synchronized (this) {
+                return copyWhileSynched(other);
+            }
+        }
+        if (System.identityHashCode(other) < System.identityHashCode(this)) {
+            synchronized (other) {
+                synchronized (this) {
+                    return copyWhileSynched(other);
+                }
+            }
+        } else {
+            synchronized (this) {
+                synchronized (other) {
+                    return copyWhileSynched(other);
+                }
+            }
+        }
+    }
+    
+    protected ConfigBag copyWhileSynched(ConfigBag other) {
+        if (sealed) 
+            throw new IllegalStateException("Cannot copy "+other+" to "+this+": this config bag has been sealed and is now immutable.");
+        putAll(other.getAllConfig());
+        markAll(Sets.difference(other.getAllConfig().keySet(), other.getUnusedConfig().keySet()));
+        setDescription(other.getDescription());
+        return this;
+    }
+
+    public synchronized int size() {
+        return config.size();
+    }
+    
+    public synchronized boolean isEmpty() {
+        return config.isEmpty();
+    }
+    
+    public ConfigBag markAll(Iterable<String> usedFlags) {
+        for (String flag: usedFlags)
+            markUsed(flag);
+        return this;
+    }
+
+    public synchronized boolean isUnused(ConfigKey<?> key) {
+        return unusedConfig.containsKey(key.getName());
+    }
+    
+    /** makes this config bag immutable; any attempts to change subsequently 
+     * (apart from marking fields as used) will throw an exception
+     * <p>
+     * copies will be unsealed however
+     * <p>
+     * returns this for convenience (fluent usage) */
+    public ConfigBag seal() {
+        sealed = true;
+        if (live) {
+            // TODO How to ensure sealed?!
+        } else {
+            config = getAllConfig();
+        }
+        return this;
+    }
+
+    // TODO why have both this and mutable
+    /** @see #getAllConfigMutable() */
+    public Map<String, Object> getAllConfigRaw() {
+        return getAllConfigMutable();
+    }
+    
+    @Override
+    public String toString() {
+        return JavaClassNames.simpleClassName(this)+"["+getAllConfigRaw()+"]";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/crypto/FluentKeySigner.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/crypto/FluentKeySigner.java b/core/src/main/java/org/apache/brooklyn/core/util/crypto/FluentKeySigner.java
new file mode 100644
index 0000000..1d0b030
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/crypto/FluentKeySigner.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.crypto;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.brooklyn.core.internal.BrooklynInitialization;
+import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
+import org.bouncycastle.asn1.x509.X509Extension;
+import org.bouncycastle.jce.X509Principal;
+
+import brooklyn.util.exceptions.Exceptions;
+
+/** A fluent API which simplifies generating certificates (signed keys) */
+/* NB - re deprecation - we use deprecated X509V3CertificateGenerator still
+ * because the official replacement, X509v3CertificateBuilder, 
+ * drags in an add'l dependency (bcmail) and is harder to use. */
+public class FluentKeySigner {
+
+    static { BrooklynInitialization.initSecureKeysBouncyCastleProvider(); }
+
+    protected X500Principal issuerPrincipal;
+    protected KeyPair issuerKey;
+
+    protected SecureRandom srand = new SecureRandom();
+    
+    protected Date validityStartDate, validityEndDate;
+    protected BigInteger serialNumber;
+    
+    protected String signatureAlgorithm = "MD5WithRSAEncryption";
+    protected AuthorityKeyIdentifier authorityKeyIdentifier;
+    protected X509Certificate authorityCertificate;
+
+    public FluentKeySigner(X500Principal issuerPrincipal, KeyPair issuerKey) {
+        this.issuerPrincipal = issuerPrincipal;
+        this.issuerKey = issuerKey;
+        validFromDaysAgo(7);
+        validForYears(10);
+    }
+    public FluentKeySigner(String issuerCommonName, KeyPair issuerKey) {
+        this(SecureKeys.getX500PrincipalWithCommonName(issuerCommonName), issuerKey);
+    }
+    
+    public FluentKeySigner(String issuerCommonName) {
+        this(issuerCommonName, SecureKeys.newKeyPair());
+    }
+
+    public FluentKeySigner(X509Certificate caCert, KeyPair caKey) {
+        this(caCert.getIssuerX500Principal(), caKey);
+        authorityCertificate(caCert);
+    }
+    
+    public KeyPair getKey() {
+        return issuerKey;
+    }
+    
+    public X500Principal getPrincipal() {
+        return issuerPrincipal;
+    }
+    
+    @SuppressWarnings("deprecation")
+    public String getCommonName() {
+//        TODO see deprecation note at top of file
+        // for modernising, would RFC4519Style.cn work ?
+        return (String) new X509Principal(issuerPrincipal.getName()).getValues(org.bouncycastle.asn1.x509.X509Name.CN).elementAt(0);
+    }
+    
+    public X509Certificate getAuthorityCertificate() {
+        return authorityCertificate;
+    }
+    
+    public FluentKeySigner validFromDaysAgo(long days) {
+        return validFrom(new Date( (System.currentTimeMillis() / (1000L*60*60*24) - days) * 1000L*60*60*24));            
+    }
+
+    public FluentKeySigner validFrom(Date d) {
+        validityStartDate = d;
+        return this;
+    }
+
+    public FluentKeySigner validForYears(long years) {
+        return validUntil(new Date( (System.currentTimeMillis() / (1000L*60*60*24) + 365*years) * 1000L*60*60*24));            
+    }
+
+    public FluentKeySigner validUntil(Date d) {
+        validityEndDate = d;
+        return this;
+    }
+
+    /** use a hard-coded serial number; or make one up, if null */
+    public FluentKeySigner serialNumber(BigInteger serialNumber) {
+        this.serialNumber = serialNumber;
+        return this;
+    }
+
+    public FluentKeySigner signatureAlgorithm(String signatureAlgorithm) {
+        this.signatureAlgorithm = signatureAlgorithm;
+        return this;
+    }
+
+    @SuppressWarnings("deprecation")
+    public FluentKeySigner authorityCertificate(X509Certificate certificate) {
+        try {
+            authorityKeyIdentifier(new org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure(certificate));
+            this.authorityCertificate = certificate;
+            return this;
+        } catch (CertificateParsingException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    public FluentKeySigner authorityKeyIdentifier(AuthorityKeyIdentifier authorityKeyIdentifier) {
+        this.authorityKeyIdentifier = authorityKeyIdentifier;
+        return this;
+    }
+    
+    public FluentKeySigner selfsign() {
+        if (authorityCertificate!=null) throw new IllegalStateException("Signer already has certificate");
+        authorityCertificate(newCertificateFor(getCommonName(), getKey()));
+        return this;
+    }
+
+    // TODO see note re deprecation at start of file
+    @SuppressWarnings("deprecation")
+    public X509Certificate newCertificateFor(X500Principal subject, PublicKey keyToCertify) {
+        try {
+            org.bouncycastle.x509.X509V3CertificateGenerator v3CertGen = new org.bouncycastle.x509.X509V3CertificateGenerator();
+
+            v3CertGen.setSerialNumber(
+                    serialNumber != null ? serialNumber :
+                        // must be positive
+                        BigInteger.valueOf(srand.nextLong()).abs().add(BigInteger.ONE));  
+            v3CertGen.setIssuerDN(issuerPrincipal);  
+            v3CertGen.setNotBefore(validityStartDate);  
+            v3CertGen.setNotAfter(validityEndDate);
+            v3CertGen.setSignatureAlgorithm(signatureAlgorithm);   
+
+            v3CertGen.setSubjectDN(subject);  
+            v3CertGen.setPublicKey(keyToCertify);  
+
+            v3CertGen.addExtension(X509Extension.subjectKeyIdentifier, false,
+                    new org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure(keyToCertify));
+
+            if (authorityKeyIdentifier!=null)
+                v3CertGen.addExtension(X509Extension.authorityKeyIdentifier, false,
+                        authorityKeyIdentifier);
+
+            X509Certificate pkCertificate = v3CertGen.generate(issuerKey.getPrivate(), "BC");
+            return pkCertificate;
+            
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    public X509Certificate newCertificateFor(String commonName, PublicKey key) {
+//        SecureKeys.getX509PrincipalWithCommonName(commonName)
+        return newCertificateFor(
+                SecureKeys.getX500PrincipalWithCommonName(commonName)
+//                new X509Principal("CN=" + commonName + ", OU=None, O=None, L=None, C=None")
+                , key);
+    }
+
+    public X509Certificate newCertificateFor(String commonName, KeyPair key) {
+        return newCertificateFor(commonName, key.getPublic());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/crypto/SecureKeys.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/crypto/SecureKeys.java b/core/src/main/java/org/apache/brooklyn/core/util/crypto/SecureKeys.java
new file mode 100644
index 0000000..5e630a8
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/crypto/SecureKeys.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Security;
+
+import org.apache.brooklyn.core.internal.BrooklynInitialization;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.jce.X509Principal;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMDecryptorProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.PEMWriter;
+import org.bouncycastle.openssl.PasswordFinder;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.crypto.AuthorizedKeysParser;
+import brooklyn.util.crypto.SecureKeysWithoutBouncyCastle;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.stream.Streams;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Throwables;
+
+/**
+ * Utility methods for generating and working with keys,
+ * extending the parent class with useful things provided by BouncyCastle crypto library.
+ * (Parent class is in a different project where BC is not included as a dependency.)
+ */
+public class SecureKeys extends SecureKeysWithoutBouncyCastle {
+
+    private static final Logger log = LoggerFactory.getLogger(SecureKeys.class);
+    
+    static { BrooklynInitialization.initSecureKeysBouncyCastleProvider(); }
+    
+    public static void initBouncyCastleProvider() {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+    
+    public static class PassphraseProblem extends IllegalStateException {
+        private static final long serialVersionUID = -3382824813899223447L;
+        public PassphraseProblem(String message) { super("Passphrase problem with this key: "+message); }
+        public PassphraseProblem(String message, Exception cause) { super("Passphrase problem with this key: "+message, cause); }
+    }
+    
+    private SecureKeys() {}
+    
+    /** RFC1773 order, with None for other values. Normally prefer X500Principal. */
+    public static X509Principal getX509PrincipalWithCommonName(String commonName) {
+        return new X509Principal("" + "C=None," + "L=None," + "O=None," + "OU=None," + "CN=" + commonName);
+    }
+
+    /** reads RSA or DSA / pem style private key files (viz {@link #toPem(KeyPair)}), extracting also the public key if possible
+     * @throws IllegalStateException on errors, in particular {@link PassphraseProblem} if that is the problem */
+    public static KeyPair readPem(InputStream input, final String passphrase) {
+        // TODO cache is only for fallback "reader" strategy (2015-01); delete when Parser confirmed working
+        byte[] cache = Streams.readFully(input);
+        input = new ByteArrayInputStream(cache);
+
+        try {
+            PEMParser pemParser = new PEMParser(new InputStreamReader(input));
+
+            Object object = pemParser.readObject();
+            pemParser.close();
+
+            JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
+            KeyPair kp = null;
+            if (object==null) {
+                throw new IllegalStateException("PEM parsing failed: missing or invalid data");
+            } else if (object instanceof PEMEncryptedKeyPair) {
+                if (passphrase==null) throw new PassphraseProblem("passphrase required");
+                try {
+                    PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(passphrase.toCharArray());
+                    kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv));
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    throw new PassphraseProblem("wrong passphrase", e);
+                }
+            } else  if (object instanceof PEMKeyPair) {
+                kp = converter.getKeyPair((PEMKeyPair) object);
+            } else if (object instanceof PrivateKeyInfo) {
+                PrivateKey privKey = converter.getPrivateKey((PrivateKeyInfo) object);
+                kp = new KeyPair(null, privKey);
+            } else {
+                throw new IllegalStateException("PEM parser support missing for: "+object);
+            }
+
+            return kp;
+
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+
+            // older code relied on PEMReader, now deprecated
+            // replaced with above based on http://stackoverflow.com/questions/14919048/bouncy-castle-pemreader-pemparser
+            // passes the same tests (Jan 2015) but leaving the old code as a fallback for the time being 
+
+            input = new ByteArrayInputStream(cache);
+            try {
+                Security.addProvider(new BouncyCastleProvider());
+                @SuppressWarnings("deprecation")
+                org.bouncycastle.openssl.PEMReader pr = new org.bouncycastle.openssl.PEMReader(new InputStreamReader(input), new PasswordFinder() {
+                    public char[] getPassword() {
+                        return passphrase!=null ? passphrase.toCharArray() : new char[0];
+                    }
+                });
+                @SuppressWarnings("deprecation")
+                KeyPair result = (KeyPair) pr.readObject();
+                pr.close();
+                if (result==null)
+                    throw Exceptions.propagate(e);
+                
+                log.warn("PEMParser failed when deprecated PEMReader succeeded, with "+result+"; had: "+e);
+
+                return result;
+
+            } catch (Exception e2) {
+                Exceptions.propagateIfFatal(e2);
+                throw Exceptions.propagate(e);
+            }
+        }
+    }
+
+    /** because KeyPair.equals is not implemented :( */
+    public static boolean equal(KeyPair k1, KeyPair k2) {
+        return Objects.equal(k2.getPrivate(), k1.getPrivate()) && Objects.equal(k2.getPublic(), k1.getPublic());
+    }
+
+    /** returns the PEM (base64, ie for id_rsa) string for the private key / key pair;
+     * this starts -----BEGIN PRIVATE KEY----- and ends similarly, like id_rsa.
+     * also see {@link #readPem(InputStream, String)} */
+    public static String toPem(KeyPair key) {
+        return stringPem(key);
+    }
+
+    /** returns id_rsa.pub style file, of public key */
+    public static String toPub(KeyPair key) {
+        return AuthorizedKeysParser.encodePublicKey(key.getPublic());
+    }
+    
+    /** opposite of {@link #toPub(KeyPair)}, given text */
+    public static PublicKey fromPub(String pubText) {
+        return AuthorizedKeysParser.decodePublicKey(pubText);
+    }
+
+    /** @deprecated since 0.7.0, use {@link #toPem(KeyPair)} */ @Deprecated
+    public static String stringPem(KeyPair key) {
+        try {
+            StringWriter sw = new StringWriter();
+            PEMWriter w = new PEMWriter(sw);
+            w.writeObject(key);
+            w.close();
+            return sw.toString();
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveBuilder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveBuilder.java b/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveBuilder.java
new file mode 100644
index 0000000..069f10b
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveBuilder.java
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.file;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.brooklyn.core.util.file.ArchiveUtils.ArchiveType;
+
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.os.Os;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.io.Files;
+
+/**
+ * Build a Zip or Jar archive.
+ * <p>
+ * Supports creating temporary archives that will be deleted on exit, if no name is
+ * specified. The created file must be a Java archive type, with the extension {@code .zip},
+ * {@code .jar}, {@code .war} or {@code .ear}.
+ * <p>
+ * Example:
+ * <pre> File zip = ArchiveBuilder.archive("data/archive.zip")
+ *         .addAt(new File("./pom.xml"), "")
+ *         .addDirContentsAt(new File("./src"), "src/")
+ *         .addAt(new File("/tmp/Extra.java"), "src/main/java/")
+ *         .addDirContentsAt(new File("/tmp/overlay/"), "")
+ *         .create();
+ * </pre>
+ * <p>
+ */
+@Beta
+public class ArchiveBuilder {
+
+    /**
+     * Create an {@link ArchiveBuilder} for an archive with the given name.
+     */
+    public static ArchiveBuilder archive(String archive) {
+        return new ArchiveBuilder(archive);
+    }
+
+    /**
+     * Create an {@link ArchiveBuilder} for a {@link ArchiveType#ZIP Zip} format archive.
+     */
+    public static ArchiveBuilder zip() {
+        return new ArchiveBuilder(ArchiveType.ZIP);
+    }
+
+    /**
+     * Create an {@link ArchiveBuilder} for a {@link ArchiveType#JAR Jar} format archive.
+     */
+    public static ArchiveBuilder jar() {
+        return new ArchiveBuilder(ArchiveType.JAR);
+    }
+
+    // TODO would be nice to support TAR and TGZ
+    // e.g. using commons-compress
+    // TarArchiveOutputStream out = new TarArchiveOutputStream(new GZIPOutputStream(bytes));
+    // but I think the way entries are done is slightly different so we'd need a bit of refactoring
+    
+    private final ArchiveType type;
+    private File archive;
+    private Manifest manifest;
+    private Multimap<String, File> entries = LinkedHashMultimap.create();
+
+    private ArchiveBuilder() {
+        this(ArchiveType.ZIP);
+    }
+
+    private ArchiveBuilder(String filename) {
+        this(ArchiveType.of(filename));
+
+        named(filename);
+    }
+
+    private ArchiveBuilder(ArchiveType type) {
+        checkNotNull(type);
+        checkArgument(ArchiveType.ZIP_ARCHIVES.contains(type));
+
+        this.type = type;
+        this.manifest = new Manifest();
+    }
+
+    /**
+     * Set the location of the generated archive file.
+     */
+    public ArchiveBuilder named(String name) {
+        checkNotNull(name);
+        String ext = Files.getFileExtension(name);
+        if (ext.isEmpty()) {
+            name = name + "." + type.toString();
+        } else if (type != ArchiveType.of(name)) {
+            throw new IllegalArgumentException(String.format("Extension for '%s' did not match archive type of %s", ext, type));
+        }
+        this.archive = new File(Os.tidyPath(name));
+        return this;
+    }
+
+    /**
+     * @see #named(String)
+     */
+    public ArchiveBuilder named(File file) {
+        checkNotNull(file);
+        return named(file.getPath());
+    }
+
+    /**
+     * Add a manifest entry with the given {@code key} and {@code value}.
+     */
+    public ArchiveBuilder manifest(Object key, Object value) {
+        checkNotNull(key, "key");
+        checkNotNull(value, "value");
+        manifest.getMainAttributes().put(key, value);
+        return this;
+    }
+
+    /**
+     * Add the file located at the {@code filePath} to the archive, 
+     * with some complicated base-name strategies.
+     *
+     * @deprecated since 0.7.0 use one of the other add methods which makes the strategy explicit */ @Deprecated
+    public ArchiveBuilder add(String filePath) {
+        checkNotNull(filePath, "filePath");
+        return add(new File(Os.tidyPath(filePath)));
+    }
+
+    /**
+     * Add the {@code file} to the archive.
+     * <p>
+     * If the file path is absolute, or points to a file above the current directory,
+     * the file is added to the archive as a top-level entry, using the file name only.
+     * For relative {@code filePath}s below the current directory, the file is added
+     * using the path given and is assumed to be located relative to the current
+     * working directory.
+     * <p>
+     * No checks for file existence are made at this stage.
+     *
+     * @see #entry(String, File)
+     * @deprecated since 0.7.0 use one of the other add methods which makes the strategy explicit */ @Deprecated
+    public ArchiveBuilder add(File file) {
+        checkNotNull(file, "file");
+        String filePath = Os.tidyPath(file.getPath());
+        if (file.isAbsolute() || filePath.startsWith("../")) {
+            return entry(Os.mergePaths(".", file.getName()), file);
+        } else {
+            return entry(Os.mergePaths(".", filePath), file);
+        }
+    }
+
+    /**
+     * Add the file located at the {@code fileSubPath}, relative to the {@code baseDir} on the local system,
+     * to the archive.
+     * <p>
+     * Uses the {@code fileSubPath} as the name of the file in the archive. Note that the
+     * file is found by concatenating the two path components using {@link Os#mergePaths(String...)},
+     * thus {@code fileSubPath} should not be absolute or point to a location above the current directory.
+     * <p>
+     * Use {@link #entry(String, String)} directly or {@link #entries(Map)} for complete
+     * control over file locations and names in the archive.
+     *
+     * @see #entry(String, String)
+     */
+    public ArchiveBuilder addFromLocalBaseDir(File baseDir, String fileSubPath) {
+        checkNotNull(baseDir, "baseDir");
+        checkNotNull(fileSubPath, "filePath");
+        return entry(Os.mergePaths(".", fileSubPath), Os.mergePaths(baseDir.getPath(), fileSubPath));
+    }
+    /** @deprecated since 0.7.0 use {@link #addFromLocalBaseDir(File, String)}, or
+     * one of the other add methods if adding relative to baseDir was not intended */ @Deprecated
+    public ArchiveBuilder addFromLocalBaseDir(String baseDir, String fileSubPath) {
+        return addFromLocalBaseDir(new File(baseDir), fileSubPath);
+    }
+    /** @deprecated since 0.7.0 use {@link #addFromLocalBaseDir(File, String)}, or
+     * one of the other add methods if adding relative to baseDir was not intended */ @Deprecated
+    public ArchiveBuilder add(String baseDir, String fileSubPath) {
+        return addFromLocalBaseDir(baseDir, fileSubPath);
+    }
+     
+    /** adds the given file to the archive, preserving its name but putting under the given directory in the archive (may be <code>""</code> or <code>"./"</code>) */
+    public ArchiveBuilder addAt(File file, String archiveParentDir) {
+        checkNotNull(archiveParentDir, "archiveParentDir");
+        checkNotNull(file, "file");
+        return entry(Os.mergePaths(archiveParentDir, file.getName()), file);
+    }
+
+    /**
+     * Add the contents of the directory named {@code dirName} to the archive.
+     *
+     * @see #addDir(File)
+     * @deprecated since 0.7.0 use {@link #addDirContentsAt(File, String) */ @Deprecated
+    public ArchiveBuilder addDir(String dirName) {
+        checkNotNull(dirName, "dirName");
+        return addDir(new File(Os.tidyPath(dirName)));
+    }
+
+    /**
+     * Add the contents of the directory {@code dir} to the archive.
+     * The directory's name is not included; use {@link #addAtRoot(File)} if you want that behaviour. 
+     * <p>
+     * Uses {@literal .} as the parent directory name for the contents.
+     *
+     * @see #entry(String, File)
+     */
+    public ArchiveBuilder addDirContentsAt(File dir, String archiveParentDir) {
+        checkNotNull(dir, "dir");
+        if (!dir.isDirectory()) throw new IllegalArgumentException(dir+" is not a directory; cannot add contents to archive");
+        return entry(archiveParentDir, dir);
+    }
+    /**
+     * As {@link #addDirContentsAt(File, String)}, 
+     * using {@literal .} as the parent directory name for the contents.
+     * 
+     * @deprecated since 0.7.0 use {@link #addDirContentsAt(File, String)
+     * to clarify API, argument types, and be explicit about where it should be installed,
+     * because JARs seem to require <code>""<code> whereas ZIPs might want <code>"./"</code>. */ @Deprecated
+    public ArchiveBuilder addDir(File dir) {
+        return addDirContentsAt(dir, ".");
+    }
+
+    /**
+     * Add the collection of {@code files} to the archive.
+     *
+     * @see #add(String)
+     * @deprecated since 0.7.0 use one of the other add methods if keeping this file's path was not intended */ @Deprecated
+    public ArchiveBuilder add(Iterable<String> files) {
+        checkNotNull(files, "files");
+        for (String filePath : files) {
+            add(filePath);
+        }
+        return this;
+    }
+
+    /**
+     * Add the collection of {@code files}, relative to the {@code baseDir}, to
+     * the archive.
+     *
+     * @see #add(String, String)
+     * @deprecated since 0.7.0 use one of the other add methods if keeping this file's path was not intended */ @Deprecated
+    public ArchiveBuilder add(String baseDir, Iterable<String> files) {
+        checkNotNull(baseDir, "baseDir");
+        checkNotNull(files, "files");
+        for (String filePath : files) {
+            add(baseDir, filePath);
+        }
+        return this;
+    }
+
+    /**
+     * Add the {@code file} to the archive with the path {@code entryPath}.
+     *
+     * @see #entry(String, File)
+     */
+    public ArchiveBuilder entry(String entryPath, String filePath) {
+        checkNotNull(entryPath, "entryPath");
+        checkNotNull(filePath, "filePath");
+        return entry(entryPath, new File(filePath));
+    }
+
+    /**
+     * Add the {@code file} to the archive with the path {@code entryPath}.
+     */
+    public ArchiveBuilder entry(String entryPath, File file) {
+        checkNotNull(entryPath, "entryPath");
+        checkNotNull(file, "file");
+        this.entries.put(entryPath, file);
+        return this;
+    }
+
+    /**
+     * Add a {@link Map} of entries to the archive.
+     * <p>
+     * The keys should be the names of the file entries to be added to the archive and
+     * the value should point to the actual {@link File} to be added.
+     * <p>
+     * This allows complete control over the directory structure of the eventual archive,
+     * as the entry names do not need to bear any relationship to the name or location
+     * of the files on the filesystem.
+     */
+    public ArchiveBuilder entries(Map<String, File> entries) {
+        checkNotNull(entries, "entries");
+        for (Map.Entry<String, File> entry: entries.entrySet())
+            this.entries.put(entry.getKey(), entry.getValue());
+        return this;
+    }
+
+    /**
+     * Generates the archive and outputs it to the given stream, ignoring any file name.
+     * <p>
+     * This will add a manifest file if the type is a Jar archive.
+     */
+    public void stream(OutputStream output) {
+        try {
+            ZipOutputStream target;
+            if (type == ArchiveType.ZIP) {
+                target = new ZipOutputStream(output);
+            } else {
+                manifest(Attributes.Name.MANIFEST_VERSION, "1.0");
+                target = new JarOutputStream(output, manifest);
+            }
+            for (String entry : entries.keySet()) {
+                addToArchive(entry, entries.get(entry), target);
+            }
+            target.close();
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+    }
+
+    /**
+     * Generates the archive, saving it with the given name.
+     */
+    public File create(String archiveFile) {
+        return named(archiveFile).create();
+    }
+
+    /**
+     * Generates the archive.
+     * <p>
+     * If no name has been specified, the archive will be created as a temporary file with
+     * a unique name, that is deleted on exit. Otherwise, the given name will be used.
+     */
+    public File create() {
+        if (archive == null) {
+            File temp = Os.newTempFile("brooklyn-archive", type.toString());
+            temp.deleteOnExit();
+            named(temp);
+        }
+        try {
+            OutputStream output = new FileOutputStream(archive);
+            stream(output);
+            output.close();
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+        return archive;
+    }
+
+    /**
+     * Recursively add files to the archive.
+     * <p>
+     * Code adapted from this <a href="http://stackoverflow.com/questions/1281229/how-to-use-jaroutputstream-to-create-a-jar-file">example</a>
+     * <p>
+     * <strong>Note</strong> {@link File} provides no support for symbolic links, and as such there is
+     * no way to ensure that a symbolic link to a directory is not followed when traversing the
+     * tree. In this case, iterables created by this traverser could contain files that are
+     * outside of the given directory or even be infinite if there is a symbolic link loop.
+     */
+    private void addToArchive(String path, Iterable<File> sources, ZipOutputStream target) throws IOException {
+        int size = Iterables.size(sources);
+        if (size==0) return;
+        boolean isDirectory;
+        if (size>1) {
+            // it must be directories if we are putting multiple things here 
+            isDirectory = true;
+        } else {
+            isDirectory = Iterables.getOnlyElement(sources).isDirectory();
+        }
+        
+        String name = path.replace("\\", "/");
+        if (isDirectory) {
+            name += "/";
+            JarEntry entry = new JarEntry(name);
+            
+            long lastModified=-1;
+            for (File source: sources)
+                if (source.lastModified()>lastModified)
+                    lastModified = source.lastModified();
+            
+            entry.setTime(lastModified);
+            target.putNextEntry(entry);
+            target.closeEntry();
+
+            for (File source: sources) {
+                if (!source.isDirectory()) {
+                    throw new IllegalStateException("Cannot add multiple items at a path in archive unless they are directories: "+sources+" at "+path+" is not valid.");
+                }
+                Iterable<File> children = Files.fileTreeTraverser().children(source);
+                for (File child : children) {
+                    addToArchive(Os.mergePaths(path, child.getName()), Collections.singleton(child), target);
+                }
+            }
+            return;
+        }
+
+        File source = Iterables.getOnlyElement(sources);
+        JarEntry entry = new JarEntry(name);
+        entry.setTime(source.lastModified());
+        target.putNextEntry(entry);
+        Files.asByteSource(source).copyTo(target);
+        target.closeEntry();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveTasks.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveTasks.java b/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveTasks.java
new file mode 100644
index 0000000..b58359a
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveTasks.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.file;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.management.TaskAdaptable;
+import org.apache.brooklyn.api.management.TaskFactory;
+import org.apache.brooklyn.core.util.ResourceUtils;
+import org.apache.brooklyn.core.util.task.Tasks;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import brooklyn.util.net.Urls;
+
+public class ArchiveTasks {
+
+    /** as {@link #deploy(ResourceUtils, Map, String, SshMachineLocation, String, String, String)} with the most common parameters */
+    public static TaskFactory<?> deploy(final ResourceUtils optionalResolver, final String archiveUrl, final SshMachineLocation machine, final String destDir) {
+        return deploy(optionalResolver, null, archiveUrl, machine, destDir, false, null, null);
+    }
+    
+    /** returns a task which installs and unpacks the given archive, as per {@link ArchiveUtils#deploy(ResourceUtils, Map, String, SshMachineLocation, String, String, String)};
+     * if allowNonarchivesOrKeepArchiveAfterDeploy is false, this task will fail if the item is not an archive;
+     * in cases where the download type is not clear in the URL but is known by the caller, supply a optionalDestFile including the appropriate file extension */
+    public static TaskFactory<?> deploy(final ResourceUtils resolver, final Map<String, ?> props, final String archiveUrl, final SshMachineLocation machine, final String destDir, final boolean allowNonarchivesOrKeepArchiveAfterDeploy, final String optionalTmpDir, final String optionalDestFile) {
+        return new TaskFactory<TaskAdaptable<?>>() {
+            @Override
+            public TaskAdaptable<?> newTask() {
+                return Tasks.<Void>builder().name("deploying "+Urls.getBasename(archiveUrl)).description("installing "+archiveUrl+" and unpacking to "+destDir).body(new Runnable() {
+                    @Override
+                    public void run() {
+                        boolean unpacked = ArchiveUtils.deploy(resolver, props, archiveUrl, machine, destDir, allowNonarchivesOrKeepArchiveAfterDeploy, optionalTmpDir, optionalDestFile);
+                        if (!unpacked && !allowNonarchivesOrKeepArchiveAfterDeploy) {
+                            throw new IllegalStateException("Unable to unpack archive from "+archiveUrl+"; not able to infer archive type");
+                        }
+                    }
+                }).build();
+            }
+        };
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveUtils.java b/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveUtils.java
new file mode 100644
index 0000000..8277a0d
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/file/ArchiveUtils.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.file;
+
+import static java.lang.String.format;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.core.util.ResourceUtils;
+import org.apache.brooklyn.core.util.task.DynamicTasks;
+import org.apache.brooklyn.core.util.task.Tasks;
+import org.apache.brooklyn.core.util.task.ssh.SshTasks;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.javalang.StackTraceSimplifier;
+import brooklyn.util.net.Urls;
+import brooklyn.util.os.Os;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.common.io.Files;
+
+public class ArchiveUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(ArchiveUtils.class);
+
+    // TODO Make this a ConfigKey on the machine location
+    /** Number of attempts when copying a file to a remote server. */
+    public static final int NUM_RETRIES_FOR_COPYING = 5;
+
+    /**
+     * The types of archive that are supported by Brooklyn.
+     */
+    public static enum ArchiveType {
+        TAR,
+        TGZ,
+        TBZ,
+        ZIP,
+        JAR,
+        WAR,
+        EAR,
+        UNKNOWN;
+
+        /**
+         * Zip format archives used by Java.
+         */
+        public static Set<ArchiveType> ZIP_ARCHIVES = EnumSet.of(ArchiveType.ZIP, ArchiveType.JAR, ArchiveType.WAR, ArchiveType.EAR);
+
+        public static ArchiveUtils.ArchiveType of(String filename) {
+            if (filename == null) return null;
+            String ext = Files.getFileExtension(filename);
+            try {
+                return valueOf(ext.toUpperCase());
+            } catch (IllegalArgumentException iae) {
+                if (filename.toLowerCase().endsWith(".tar.gz")) {
+                    return TGZ;
+                } else if (filename.toLowerCase().endsWith(".tar.bz") ||
+                        filename.toLowerCase().endsWith(".tar.bz2") ||
+                        filename.toLowerCase().endsWith(".tar.xz")) {
+                    return TBZ;
+                } else {
+                    return UNKNOWN;
+                }
+            }
+        }
+
+        @Override
+        public String toString() {
+            if (UNKNOWN.equals(this)) {
+                return "";
+            } else {
+                return name().toLowerCase();
+            }
+        }
+    }
+
+    /**
+     * Returns the list of commands used to install support for an archive with the given name.
+     */
+    public static List<String> installCommands(String fileName) {
+        List<String> commands = new LinkedList<String>();
+        switch (ArchiveType.of(fileName)) {
+            case TAR:
+            case TGZ:
+            case TBZ:
+                commands.add(BashCommands.INSTALL_TAR);
+                break;
+            case ZIP:
+                commands.add(BashCommands.INSTALL_UNZIP);
+                break;
+            case JAR:
+            case WAR:
+            case EAR:
+            case UNKNOWN:
+                break;
+        }
+        return commands;
+    }
+
+    /**
+     * Returns the list of commands used to extract the contents of the archive with the given name.
+     * <p>
+     * Optionally, Java archives of type
+     *
+     * @see #extractCommands(String, String)
+     */
+    public static List<String> extractCommands(String fileName, String sourceDir, String targetDir, boolean extractJar) {
+        return extractCommands(fileName, sourceDir, targetDir, extractJar, true);
+    }
+    
+    /** as {@link #extractCommands(String, String, String, boolean)}, but also with option to keep the original */
+    public static List<String> extractCommands(String fileName, String sourceDir, String targetDir, boolean extractJar, boolean keepOriginal) {
+        List<String> commands = new LinkedList<String>();
+        commands.add("cd " + targetDir);
+        String sourcePath = Os.mergePathsUnix(sourceDir, fileName);
+        switch (ArchiveType.of(fileName)) {
+            case TAR:
+                commands.add("tar xvf " + sourcePath);
+                break;
+            case TGZ:
+                commands.add("tar xvfz " + sourcePath);
+                break;
+            case TBZ:
+                commands.add("tar xvfj " + sourcePath);
+                break;
+            case ZIP:
+                commands.add("unzip " + sourcePath);
+                break;
+            case JAR:
+            case WAR:
+            case EAR:
+                if (extractJar) {
+                    commands.add("jar -xvf " + sourcePath);
+                    break;
+                }
+            case UNKNOWN:
+                if (!sourcePath.equals(Urls.mergePaths(targetDir, fileName))) {
+                    commands.add("cp " + sourcePath + " " + targetDir);
+                } else {
+                    keepOriginal = true;
+                    // else we'd just end up deleting it!
+                    // this branch will often lead to errors in any case, see the allowNonarchivesOrKeepArchiveAfterDeploy parameter 
+                    // in ArchiveTasks which calls through to here and then fails in the case corresponding to this code branch
+                }
+                break;
+        }
+        if (!keepOriginal && !commands.isEmpty())
+            commands.add("rm "+sourcePath);
+        return commands;
+    }
+
+    /**
+     * Returns the list of commands used to extract the contents of the archive with the given name.
+     * <p>
+     * The archive will be extracted in its current directory unless it is a Java archive of type {@code .jar},
+     * {@code .war} or {@code .ear}, which will be left as is.
+     *
+     * @see #extractCommands(String, String, String, boolean)
+     */
+    public static List<String> extractCommands(String fileName, String sourceDir) {
+        return extractCommands(fileName, sourceDir, ".", false);
+    }
+
+    /**
+     * Deploys an archive file to a remote machine and extracts the contents.
+     */
+    public static void deploy(String archiveUrl, SshMachineLocation machine, String destDir) {
+        deploy(MutableMap.<String, Object>of(), archiveUrl, machine, destDir);
+    }
+
+    /**
+     * Deploys an archive file to a remote machine and extracts the contents.
+     * <p>
+     * Copies the archive file from the given URL to the destination directory and extracts
+     * the contents. If the URL is a local directory, the contents are packaged as a Zip archive first.
+     *
+     * @see #deploy(String, SshMachineLocation, String, String)
+     * @see #deploy(Map, String, SshMachineLocation, String, String, String)
+     */
+    public static void deploy(Map<String, ?> props, String archiveUrl, SshMachineLocation machine, String destDir) {
+        if (Urls.isDirectory(archiveUrl)) {
+            File zipFile = ArchiveBuilder.zip().entry(".", Urls.toFile(archiveUrl)).create();
+            archiveUrl = zipFile.getAbsolutePath();
+        }
+
+        // Determine filename
+        String destFile = archiveUrl.contains("?") ? archiveUrl.substring(0, archiveUrl.indexOf('?')) : archiveUrl;
+        destFile = destFile.substring(destFile.lastIndexOf('/') + 1);
+
+        deploy(props, archiveUrl, machine, destDir, destFile);
+    }
+
+    /**
+     * Deploys an archive file to a remote machine and extracts the contents.
+     * <p>
+     * Copies the archive file from the given URL to a file in the destination directory and extracts
+     * the contents.
+     *
+     * @see #deploy(String, SshMachineLocation, String)
+     * @see #deploy(Map, String, SshMachineLocation, String, String, String)
+     */
+    public static void deploy(String archiveUrl, SshMachineLocation machine, String destDir, String destFile) {
+        deploy(MutableMap.<String, Object>of(), archiveUrl, machine, destDir, destDir, destFile);
+    }
+    public static void deploy(Map<String, ?> props, String archiveUrl, SshMachineLocation machine, String destDir, String destFile) {
+        deploy(props, archiveUrl, machine, destDir, destDir, destFile);
+    }
+    public static void deploy(Map<String, ?> props, String archiveUrl, SshMachineLocation machine, String tmpDir, String destDir, String destFile) {
+        deploy(null, props, archiveUrl, machine, destDir, true, tmpDir, destFile);
+    }
+    
+    /**
+     * Deploys an archive file to a remote machine and extracts the contents.
+     * <p>
+     * Copies the archive file from the given URL to a file in a temporary directory and extracts
+     * the contents in the destination directory. For Java archives of type {@code .jar},
+     * {@code .war} or {@code .ear} the file is simply copied.
+     * 
+     * @return true if the archive is downloaded AND unpacked; false if it is downloaded but not unpacked; 
+     * throws if there was an error downloading or, for known archive types, unpacking.
+     *
+     * @see #deploy(String, SshMachineLocation, String)
+     * @see #deploy(Map, String, SshMachineLocation, String, String, String)
+     * @see #install(SshMachineLocation, String, String, int)
+     */
+    public static boolean deploy(ResourceUtils resolver, Map<String, ?> props, String archiveUrl, SshMachineLocation machine, String destDir, boolean keepArchiveAfterUnpacking, String optionalTmpDir, String optionalDestFile) {
+        String destFile = optionalDestFile;
+        if (destFile==null) destFile = Urls.getBasename(Preconditions.checkNotNull(archiveUrl, "archiveUrl"));
+        if (Strings.isBlank(destFile)) 
+            throw new IllegalStateException("Not given filename and cannot infer archive type from '"+archiveUrl+"'");
+        
+        String tmpDir = optionalTmpDir;
+        if (tmpDir==null) tmpDir=Preconditions.checkNotNull(destDir, "destDir");
+        if (props==null) props = MutableMap.of();
+        String destPath = Os.mergePaths(tmpDir, destFile);
+
+        // Use the location mutex to prevent package manager locking issues
+        machine.acquireMutex("installing", "installing archive");
+        try {
+            int result = install(resolver, props, machine, archiveUrl, destPath, NUM_RETRIES_FOR_COPYING);
+            if (result != 0) {
+                throw new IllegalStateException(format("Unable to install archive %s to %s", archiveUrl, machine));
+            }
+
+            // extract, now using task if available
+            MutableList<String> commands = MutableList.copyOf(installCommands(destFile))
+                    .appendAll(extractCommands(destFile, tmpDir, destDir, false, keepArchiveAfterUnpacking));
+            if (DynamicTasks.getTaskQueuingContext()!=null) {
+                result = DynamicTasks.queue(SshTasks.newSshExecTaskFactory(machine, commands.toArray(new String[0])).summary("extracting archive").requiringExitCodeZero()).get();
+            } else {
+                result = machine.execCommands(props, "extracting content", commands);
+            }
+            if (result != 0) {
+                throw new IllegalStateException(format("Failed to expand archive %s on %s", archiveUrl, machine));
+            }
+            return ArchiveType.of(destFile)!=ArchiveType.UNKNOWN;
+        } finally {
+            machine.releaseMutex("installing");
+        }
+    }
+
+    /**
+     * Installs a URL onto a remote machine.
+     *
+     * @see #install(Map, SshMachineLocation, String, String, int)
+     */
+    public static int install(SshMachineLocation machine, String urlToInstall, String target) {
+        return install(MutableMap.<String, Object>of(), machine, urlToInstall, target, NUM_RETRIES_FOR_COPYING);
+    }
+
+    /**
+     * Installs a URL onto a remote machine.
+     *
+     * @see #install(SshMachineLocation, String, String)
+     * @see SshMachineLocation#installTo(Map, String, String)
+     */
+    public static int install(Map<String, ?> props, SshMachineLocation machine, String urlToInstall, String target, int numAttempts) {
+        return install(null, props, machine, urlToInstall, target, numAttempts);
+    }
+    
+    public static int install(ResourceUtils resolver, Map<String, ?> props, SshMachineLocation machine, String urlToInstall, String target, int numAttempts) {
+        if (resolver==null) resolver = ResourceUtils.create(machine);
+        Exception lastError = null;
+        int retriesRemaining = numAttempts;
+        int attemptNum = 0;
+        do {
+            attemptNum++;
+            try {
+                Tasks.setBlockingDetails("Installing "+urlToInstall+" at "+machine);
+                // TODO would be nice to have this in a task (and the things within it!)
+                return machine.installTo(resolver, props, urlToInstall, target);
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                lastError = e;
+                String stack = StackTraceSimplifier.toString(e);
+                if (stack.contains("net.schmizz.sshj.sftp.RemoteFile.write")) {
+                    log.warn("Failed to transfer "+urlToInstall+" to "+machine+", retryable error, attempt "+attemptNum+"/"+numAttempts+": "+e);
+                    continue;
+                }
+                log.warn("Failed to transfer "+urlToInstall+" to "+machine+", not a retryable error so failing: "+e);
+                throw Exceptions.propagate(e);
+            } finally {
+                Tasks.resetBlockingDetails();
+            }
+        } while (retriesRemaining --> 0);
+        throw Exceptions.propagate(lastError);
+    }
+
+    /**
+     * Copies the entire contents of a file to a String.
+     *
+     * @see com.google.common.io.Files#toString(File, java.nio.charset.Charset)
+     */
+    public static String readFullyString(File sourceFile) {
+        try {
+            return Files.toString(sourceFile, Charsets.UTF_8);
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/flags/ClassCoercionException.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/util/flags/ClassCoercionException.java b/core/src/main/java/org/apache/brooklyn/core/util/flags/ClassCoercionException.java
new file mode 100644
index 0000000..72c8698
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/util/flags/ClassCoercionException.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.flags;
+
+/**
+ * Thrown to indicate that {@link TypeCoercions} could not cast an object from one
+ * class to another.
+ */
+public class ClassCoercionException extends ClassCastException {
+    public ClassCoercionException() {
+        super();
+    }
+
+    /**
+     * Constructs a <code>ClassCoercionException</code> with the specified
+     * detail message.
+     *
+     * @param s the detail message.
+     */
+    public ClassCoercionException(String s) {
+        super(s);
+    }
+}