blob: 6e4704e8a71075fe7ea8e4c4169869a9fb48f399 [file] [log] [blame]
/*
* 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);
}
}
}