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