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 <ConfigKey,Object> or <String,Object> 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);
+ }
+}