| /* |
| * 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.internal; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.util.Arrays; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Properties; |
| import java.util.Set; |
| |
| import org.apache.brooklyn.config.ConfigKey; |
| import org.apache.brooklyn.config.ConfigKey.HasConfigKey; |
| import org.apache.brooklyn.core.config.BasicConfigKey; |
| import org.apache.brooklyn.util.collections.MutableMap; |
| import org.apache.brooklyn.util.core.ResourceUtils; |
| import org.apache.brooklyn.util.core.config.ConfigBag; |
| import org.apache.brooklyn.util.core.flags.TypeCoercions; |
| import org.apache.brooklyn.util.guava.Maybe; |
| import org.apache.brooklyn.util.os.Os; |
| import org.apache.brooklyn.util.text.StringFunctions; |
| import org.apache.brooklyn.util.text.Strings; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.annotations.Beta; |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.Maps; |
| |
| import groovy.lang.Closure; |
| |
| /** |
| * TODO methods in this class are not thread safe. |
| * intention is that they are set during startup and not modified thereafter. |
| */ |
| @SuppressWarnings("rawtypes") |
| public class BrooklynPropertiesImpl implements BrooklynProperties { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(BrooklynPropertiesImpl.class); |
| |
| public static class Factory { |
| /** creates a new empty {@link BrooklynPropertiesImpl} */ |
| public static BrooklynPropertiesImpl newEmpty() { |
| return new BrooklynPropertiesImpl(); |
| } |
| |
| /** creates a new {@link BrooklynPropertiesImpl} with contents loaded |
| * from the usual places, including *.properties files and environment variables */ |
| public static BrooklynPropertiesImpl newDefault() { |
| return new Builder(true).build(); |
| } |
| |
| public static Builder builderDefault() { |
| return new Builder(true); |
| } |
| |
| public static Builder builderEmpty() { |
| return new Builder(false); |
| } |
| |
| public static class Builder { |
| private String defaultLocationMetadataUrl; |
| private String globalLocationMetadataFile = null; |
| private String globalPropertiesFile = null; |
| private String localPropertiesFile = null; |
| private BrooklynPropertiesImpl originalProperties = null; |
| |
| private Builder(boolean setGlobalFileDefaults) { |
| resetDefaultLocationMetadataUrl(); |
| if (setGlobalFileDefaults) { |
| resetGlobalFiles(); |
| } |
| } |
| |
| public Builder resetDefaultLocationMetadataUrl() { |
| defaultLocationMetadataUrl = "classpath://brooklyn/location-metadata.properties"; |
| return this; |
| } |
| public Builder resetGlobalFiles() { |
| defaultLocationMetadataUrl = "classpath://brooklyn/location-metadata.properties"; |
| globalLocationMetadataFile = Os.mergePaths(Os.home(), ".brooklyn", "location-metadata.properties"); |
| globalPropertiesFile = Os.mergePaths(Os.home(), ".brooklyn", "brooklyn.properties"); |
| return this; |
| } |
| |
| /** |
| * Creates a Builder that when built, will return the BrooklynProperties passed to this constructor |
| */ |
| private Builder(BrooklynPropertiesImpl originalProperties) { |
| this.originalProperties = new BrooklynPropertiesImpl().addFromMap(originalProperties.asMapWithStringKeys()); |
| } |
| |
| /** |
| * The URL of a default location-metadata.properties (for meta-data about different locations, such as iso3166 and global lat/lon). |
| * Defaults to classpath://brooklyn/location-metadata.properties |
| */ |
| public Builder defaultLocationMetadataUrl(String val) { |
| defaultLocationMetadataUrl = checkNotNull(val, "file"); |
| return this; |
| } |
| |
| /** |
| * The URL of a location-metadata.properties file that appends to and overwrites values in the locationMetadataUrl. |
| * Defaults to ~/.brooklyn/location-metadata.properties |
| */ |
| public Builder globalLocationMetadataFile(String val) { |
| globalLocationMetadataFile = checkNotNull(val, "file"); |
| return this; |
| } |
| |
| /** |
| * The URL of a shared brooklyn.properties file. Defaults to ~/.brooklyn/brooklyn.properties. |
| * Can be null to disable. |
| */ |
| public Builder globalPropertiesFile(String val) { |
| globalPropertiesFile = val; |
| return this; |
| } |
| |
| @Beta |
| public boolean hasDelegateOriginalProperties() { |
| return this.originalProperties==null; |
| } |
| |
| /** |
| * The URL of a brooklyn.properties file specific to this launch. Appends to and overwrites values in globalPropertiesFile. |
| */ |
| public Builder localPropertiesFile(String val) { |
| localPropertiesFile = val; |
| return this; |
| } |
| |
| public BrooklynPropertiesImpl build() { |
| if (originalProperties != null) { |
| return new BrooklynPropertiesImpl().addFromMap(originalProperties.asMapWithStringKeys()); |
| } |
| |
| BrooklynPropertiesImpl properties = new BrooklynPropertiesImpl(); |
| |
| // TODO Could also read from http://brooklyn.io, for up-to-date values? |
| // But might that make unit tests run very badly when developer is offline? |
| addPropertiesFromUrl(properties, defaultLocationMetadataUrl, false); |
| |
| addPropertiesFromFile(properties, globalLocationMetadataFile); |
| addPropertiesFromFile(properties, globalPropertiesFile); |
| addPropertiesFromFile(properties, localPropertiesFile); |
| |
| properties.addEnvironmentVars(); |
| properties.addSystemProperties(); |
| |
| return properties; |
| } |
| |
| public static Builder fromProperties(BrooklynPropertiesImpl brooklynProperties) { |
| return new Builder(brooklynProperties); |
| } |
| |
| @Override |
| @SuppressWarnings("deprecation") |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .omitNullValues() |
| .add("originalProperties", originalProperties) |
| .add("defaultLocationMetadataUrl", defaultLocationMetadataUrl) |
| .add("globalLocationMetadataUrl", globalLocationMetadataFile) |
| .add("globalPropertiesFile", globalPropertiesFile) |
| .add("localPropertiesFile", localPropertiesFile) |
| .toString(); |
| } |
| } |
| |
| private static void addPropertiesFromUrl(BrooklynPropertiesImpl p, String url, boolean warnIfNotFound) { |
| if (url==null) return; |
| |
| try { |
| p.addFrom(ResourceUtils.create(BrooklynPropertiesImpl.class).getResourceFromUrl(url)); |
| } catch (Exception e) { |
| if (warnIfNotFound) |
| LOG.warn("Could not load {}; continuing", url); |
| if (LOG.isTraceEnabled()) LOG.trace("Could not load "+url+"; continuing", e); |
| } |
| } |
| |
| private static void addPropertiesFromFile(BrooklynPropertiesImpl p, String file) { |
| if (file==null) return; |
| |
| String fileTidied = Os.tidyPath(file); |
| File f = new File(fileTidied); |
| |
| if (f.exists()) { |
| p.addFrom(f); |
| } |
| } |
| } |
| |
| /** |
| * The actual properties. |
| * <p> |
| * Care must be taken accessing/modifying these, to ensure it is correctly synchronized. |
| */ |
| private final Map<String, Object> contents = new LinkedHashMap<>(); |
| |
| protected BrooklynPropertiesImpl() { |
| } |
| |
| @Override |
| public BrooklynPropertiesImpl addEnvironmentVars() { |
| addFrom(System.getenv()); |
| return this; |
| } |
| |
| @Override |
| public BrooklynPropertiesImpl addSystemProperties() { |
| addFrom(System.getProperties()); |
| return this; |
| } |
| |
| @Override |
| public BrooklynPropertiesImpl addFrom(ConfigBag cfg) { |
| addFrom(cfg.getAllConfig()); |
| return this; |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public BrooklynPropertiesImpl addFrom(Map map) { |
| putAll(Maps.transformValues(map, x -> x instanceof String ? ((String)x).trim() : x)); |
| return this; |
| } |
| |
| @Override |
| public BrooklynPropertiesImpl addFrom(InputStream i) { |
| // Ugly way to load them in order, but Properties is a Hashtable so loses order otherwise. |
| @SuppressWarnings({ "serial" }) |
| Properties p = new Properties() { |
| @Override |
| public synchronized Object put(Object key, Object value) { |
| // Trim the string values to remove leading and trailing spaces |
| String s = (String) value; |
| if (Strings.isBlank(s)) { |
| s = Strings.EMPTY; |
| } else { |
| s = CharMatcher.breakingWhitespace().trimFrom(s); |
| } |
| return BrooklynPropertiesImpl.this.put(key, s); |
| } |
| }; |
| try { |
| p.load(i); |
| } catch (IOException e) { |
| throw Throwables.propagate(e); |
| } |
| return this; |
| } |
| |
| @Override |
| public BrooklynPropertiesImpl addFrom(File f) { |
| if (!f.exists()) { |
| LOG.warn("Unable to find file '"+f.getAbsolutePath()+"' when loading properties; ignoring"); |
| return this; |
| } else { |
| try { |
| return addFrom(new FileInputStream(f)); |
| } catch (FileNotFoundException e) { |
| throw Throwables.propagate(e); |
| } |
| } |
| } |
| @Override |
| public BrooklynPropertiesImpl addFrom(URL u) { |
| try { |
| return addFrom(u.openStream()); |
| } catch (IOException e) { |
| throw new RuntimeException("Error reading properties from "+u+": "+e, e); |
| } |
| } |
| /** |
| * @see ResourceUtils#getResourceFromUrl(String) |
| * |
| * of the form form file:///home/... or http:// or classpath://xx ; |
| * for convenience if not starting with xxx: it is treated as a classpath reference or a file; |
| * throws if not found (but does nothing if argument is null) |
| */ |
| @Override |
| public BrooklynPropertiesImpl addFromUrl(String url) { |
| try { |
| if (url==null) return this; |
| return addFrom(ResourceUtils.create(this).getResourceFromUrl(url)); |
| } catch (Exception e) { |
| throw new RuntimeException("Error reading properties from "+url+": "+e, e); |
| } |
| } |
| |
| /** expects a property already set in scope, whose value is acceptable to {@link #addFromUrl(String)}; |
| * if property not set, does nothing */ |
| @Override |
| public BrooklynPropertiesImpl addFromUrlProperty(String urlProperty) { |
| String url = (String) get(urlProperty); |
| if (url==null) addFromUrl(url); |
| return this; |
| } |
| |
| /** |
| * adds the indicated properties |
| */ |
| @Override |
| public BrooklynPropertiesImpl addFromMap(Map properties) { |
| putAll(properties); |
| return this; |
| } |
| |
| /** inserts the value under the given key, if it was not present */ |
| @Override |
| public boolean putIfAbsent(String key, Object value) { |
| if (containsKey(key)) return false; |
| put(key, value); |
| return true; |
| } |
| |
| /** @deprecated attempts to call get with this syntax are probably mistakes; get(key, defaultValue) is fine but |
| * Map is unlikely the key, much more likely they meant getFirst(flags, key). |
| */ |
| @Override |
| @Deprecated |
| public String get(Map flags, String key) { |
| LOG.warn("Discouraged use of 'BrooklynProperties.get(Map,String)' (ambiguous); use getFirst(Map,String) or get(String) -- assuming the former"); |
| LOG.debug("Trace for discouraged use of 'BrooklynProperties.get(Map,String)'", |
| new Throwable("Arguments: "+flags+" "+key)); |
| return getFirst(flags, key); |
| } |
| |
| /** returns the value of the first key which is defined |
| * <p> |
| * takes the following flags: |
| * 'warnIfNone', 'failIfNone' (both taking a boolean (to use default message) or a string (which is the message)); |
| * and 'defaultIfNone' (a default value to return if there is no such property); defaults to no warning and null response */ |
| @Override |
| public String getFirst(String ...keys) { |
| return getFirst(MutableMap.of(), keys); |
| } |
| @Override |
| public String getFirst(Map flags, String ...keys) { |
| for (String k: keys) { |
| if (k!=null && containsKey(k)) return (String) get(k); |
| } |
| if (flags.get("warnIfNone")!=null && !Boolean.FALSE.equals(flags.get("warnIfNone"))) { |
| if (Boolean.TRUE.equals(flags.get("warnIfNone"))) |
| LOG.warn("Unable to find Brooklyn property "+keys); |
| else |
| LOG.warn(""+flags.get("warnIfNone")); |
| } |
| if (flags.get("failIfNone")!=null && !Boolean.FALSE.equals(flags.get("failIfNone"))) { |
| Object f = flags.get("failIfNone"); |
| if (f instanceof Closure) { |
| LOG.warn("Use of groovy.lang.Closure is deprecated as value for 'failIfNone', in BrooklynProperties.getFirst()"); |
| ((Closure)f).call((Object[])keys); |
| } |
| if (Boolean.TRUE.equals(f)) |
| throw new NoSuchElementException("Brooklyn unable to find mandatory property "+keys[0]+ |
| (keys.length>1 ? " (or "+(keys.length-1)+" other possible names, full list is "+Arrays.asList(keys)+")" : "") ); |
| else |
| throw new NoSuchElementException(""+f); |
| } |
| if (flags.get("defaultIfNone")!=null) { |
| return (String) flags.get("defaultIfNone"); |
| } |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| return "BrooklynProperties["+size()+"]"; |
| } |
| |
| /** like normal map.put, except config keys are dereferenced on the way in */ |
| @Override |
| public Object put(Object rawKey, Object value) { |
| String key; |
| if (rawKey == null) { |
| throw new NullPointerException("Null key not permitted in BrooklynProperties"); |
| } else if (rawKey instanceof String) { |
| key = (String) rawKey; |
| } else if (rawKey instanceof CharSequence) { |
| key = rawKey.toString(); |
| } else if (rawKey instanceof HasConfigKey) { |
| key = ((HasConfigKey)rawKey).getConfigKey().getName(); |
| } else if (rawKey instanceof ConfigKey) { |
| key = ((ConfigKey)rawKey).getName(); |
| } else { |
| throw new IllegalArgumentException("Invalid key (value='" + rawKey + "', type=" + rawKey.getClass().getName() + ") for BrooklynProperties"); |
| } |
| return putImpl(key, value); |
| } |
| |
| /** like normal map.putAll, except config keys are dereferenced on the way in */ |
| @Override |
| public void putAll(Map vals) { |
| for (Map.Entry<?,?> entry : ((Map<?,?>)vals).entrySet()) { |
| put(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| @Override |
| public <T> Object put(HasConfigKey<T> key, T value) { |
| return putImpl(key.getConfigKey().getName(), value); |
| } |
| |
| @Override |
| public <T> Object put(ConfigKey<T> key, T value) { |
| return putImpl(key.getName(), value); |
| } |
| |
| @Override |
| public <T> boolean putIfAbsent(ConfigKey<T> key, T value) { |
| return putIfAbsent(key.getName(), value); |
| } |
| |
| @Override |
| public Object getConfig(String key) { |
| return get(key); |
| } |
| |
| @Override |
| public <T> T getConfig(ConfigKey<T> key) { |
| return getConfig(key, null); |
| } |
| |
| @Override |
| public <T> T getConfig(HasConfigKey<T> key) { |
| return getConfig(key.getConfigKey(), null); |
| } |
| |
| @Override |
| public <T> T getConfig(HasConfigKey<T> key, T defaultValue) { |
| return getConfig(key.getConfigKey(), defaultValue); |
| } |
| |
| @Override |
| public <T> T getConfig(ConfigKey<T> key, T defaultValue) { |
| // TODO does not support MapConfigKey etc where entries use subkey notation; for now, access using submap |
| if (!containsKey(key.getName())) { |
| if (defaultValue!=null) return defaultValue; |
| return key.getDefaultValue(); |
| } |
| Object value = get(key.getName()); |
| if (value==null) return null; |
| // no evaluation / key extraction here |
| return TypeCoercions.coerce(value, key.getTypeToken()); |
| } |
| |
| @Override |
| public Maybe<Object> getConfigRaw(ConfigKey<?> key) { |
| if (containsKey(key.getName())) return Maybe.of(get(key.getName())); |
| return Maybe.absent(); |
| } |
| |
| @Override |
| public Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited) { |
| return getConfigRaw(key); |
| } |
| |
| @Override |
| public Maybe<Object> getConfigLocalRaw(ConfigKey<?> key) { |
| return getConfigRaw(key); |
| } |
| |
| @Override |
| public Map<ConfigKey<?>,Object> getAllConfigLocalRaw() { |
| Map<ConfigKey<?>, Object> result = new LinkedHashMap<>(); |
| synchronized (contents) { |
| for (Map.Entry<String, Object> entry : contents.entrySet()) { |
| result.put(new BasicConfigKey<Object>(Object.class, entry.getKey()), entry.getValue()); |
| } |
| } |
| return result; |
| } |
| |
| @Override @Deprecated |
| public Map<ConfigKey<?>, Object> getAllConfig() { |
| return getAllConfigLocalRaw(); |
| } |
| |
| @Override |
| public Set<ConfigKey<?>> findKeysDeclared(Predicate<? super ConfigKey<?>> filter) { |
| Set<ConfigKey<?>> result = new LinkedHashSet<ConfigKey<?>>(); |
| synchronized (contents) { |
| for (Object entry: contents.entrySet()) { |
| ConfigKey<?> k = new BasicConfigKey<Object>(Object.class, ""+((Map.Entry)entry).getKey()); |
| if (filter.apply(k)) { |
| result.add(k); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public Set<ConfigKey<?>> findKeysPresent(Predicate<? super ConfigKey<?>> filter) { |
| return findKeysDeclared(filter); |
| } |
| |
| @Override |
| public BrooklynProperties submap(Predicate<ConfigKey<?>> filter) { |
| BrooklynPropertiesImpl result = Factory.newEmpty(); |
| synchronized (contents) { |
| for (Map.Entry<String, Object> entry : contents.entrySet()) { |
| ConfigKey<?> k = new BasicConfigKey<Object>(Object.class, entry.getKey()); |
| if (filter.apply(k)) { |
| result.put(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public BrooklynProperties submapByName(Predicate<? super String> filter) { |
| BrooklynPropertiesImpl result = Factory.newEmpty(); |
| synchronized (contents) { |
| for (Map.Entry<String, Object> entry : contents.entrySet()) { |
| if (filter.apply(entry.getKey())) { |
| result.put(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public Map<String, Object> asMapWithStringKeys() { |
| synchronized (contents) { |
| return MutableMap.copyOf(contents).asUnmodifiable(); |
| } |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| synchronized (contents) { |
| return contents.isEmpty(); |
| } |
| } |
| |
| @Override |
| public int size() { |
| synchronized (contents) { |
| return contents.size(); |
| } |
| } |
| |
| @Override |
| public boolean containsKey(String key) { |
| synchronized (contents) { |
| return contents.containsKey(key); |
| } |
| } |
| |
| @Override |
| public boolean containsKey(ConfigKey<?> key) { |
| return containsKey(key.getName()); |
| } |
| |
| @Override |
| public boolean remove(String key) { |
| synchronized (contents) { |
| boolean result = contents.containsKey(key); |
| contents.remove(key); |
| return result; |
| } |
| } |
| |
| @Override |
| public boolean remove(ConfigKey<?> key) { |
| return remove(key.getName()); |
| } |
| |
| protected Object get(String key) { |
| synchronized (contents) { |
| return contents.get(key); |
| } |
| } |
| |
| protected Object putImpl(String key, Object value) { |
| synchronized (contents) { |
| return contents.put(key, value); |
| } |
| } |
| } |