ro.pippo.core.PippoSettings Maven / Gradle / Ivy
/*
* Copyright (C) 2014 the original author or authors.
*
* Licensed 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 ro.pippo.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.pippo.core.util.ClasspathUtils;
import ro.pippo.core.util.StringUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
/**
* A simple properties-file based settings class for Pippo applications.
*
* The properties file is automatically loaded from one of the following
* sources:
*
* - from the
-Dpippo.settings
system property
* - from the current working directory (application.properties)
*
- from the application classpath (conf/application.properties)
*
- using the built-in Pippo properties
*
*
*
* A settings file located on the filesystem is automatically reloaded, if
* modified, on the next setting lookup.
*
*
* All settings support runtime-mode configuration allowing you to specify the
* same setting with mode-dependent values.
*
*
* Settings without a mode prefix are the shared for all modes unless explicitly
* overridden by a mode-specific setting.
*
*
*
* example.value = my example value
* %prod.example.value = my production value
* %dev.example.value = my development value
*
*
* Setting files support including/importing settings from other setting files
* using the include= setting. The include= setting is a
* comma-delimited field allowing importing settings from multiple sources.
* Inclusions may be recursive.
*
*
* @author James Moger
*/
public class PippoSettings {
private static final Logger log = LoggerFactory.getLogger(PippoSettings.class);
private static final String USING_DEFAULT_OF = " using default of ";
private static final String DEFAULT_LIST_DELIMITER = ",";
private final RuntimeMode runtimeMode;
private final Properties properties;
private final Properties overrides;
private final Map interpolationValues;
private final URL propertiesUrl;
private final boolean isFile;
private volatile long lastModified;
public PippoSettings(RuntimeMode runtimeMode) {
this.runtimeMode = runtimeMode;
this.propertiesUrl = getPropertiesUrl();
this.isFile = propertiesUrl.getProtocol().equals("file:");
this.interpolationValues = new HashMap<>();
loadInterpolationValues();
this.properties = loadProperties(propertiesUrl);
this.overrides = new Properties();
}
public RuntimeMode getRuntimeMode() {
return runtimeMode;
}
public boolean isDev() {
return RuntimeMode.DEV == runtimeMode;
}
public boolean isTest() {
return RuntimeMode.TEST == runtimeMode;
}
public boolean isProd() {
return RuntimeMode.PROD == runtimeMode;
}
public String getLocalHostname() {
// try InetAddress.LocalHost first;
// NOTE -- InetAddress.getLocalHost().getHostName() will not work in
// certain environments.
try {
String result = InetAddress.getLocalHost().getHostName();
if (!StringUtils.isNullOrEmpty(result))
return result;
} catch (UnknownHostException e) {
// failed; try alternate means.
}
// try environment properties.
//
String host = System.getenv("COMPUTERNAME");
if (host != null)
return host;
host = System.getenv("HOSTNAME");
if (host != null)
return host;
// undetermined.
return "Pippo";
}
private URL getPropertiesUrl() {
URL url = null;
try {
// System property
String systemProperty = System.getProperty(PippoConstants.SYSTEM_PROPERTY_PIPPO_SETTINGS, null);
if (!StringUtils.isNullOrEmpty(systemProperty)) {
url = URI.create(systemProperty).toURL();
log.debug("Located '{}' using the '{}' system property", url, PippoConstants.SYSTEM_PROPERTY_PIPPO_SETTINGS);
}
} catch (MalformedURLException e) {
log.error("Failed to parse '{}' system property!", PippoConstants.SYSTEM_PROPERTY_PIPPO_SETTINGS);
}
try {
// working directory
Path path = Paths.get(System.getProperty("user.dir"), PippoConstants.APPLICATION_PROPERTIES);
if (path.toFile().exists()) {
url = path.toUri().toURL();
log.debug("Located '{}' in the current working directory", url);
}
} catch (MalformedURLException e) {
log.error("Failed to parse working directory properties!", e);
}
if (url == null) {
// try locating an application classpath properties file
url = ClasspathUtils.locateOnClasspath(PippoConstants.LOCATION_OF_PIPPO_CLASSPATH_PROPERTIES);
}
if (url == null) {
// last resort, use built-in properties
url = ClasspathUtils.locateOnClasspath(PippoConstants.LOCATION_OF_PIPPO_BUILTIN_PROPERTIES);
}
return url;
}
private synchronized Properties getProperties() {
if (isFile) {
File file = new File(propertiesUrl.getPath());
if (file.lastModified() > lastModified) {
Properties reloadedProperties = loadProperties(propertiesUrl);
properties.clear();
properties.putAll(reloadedProperties);
lastModified = file.lastModified();
}
}
return properties;
}
private synchronized Properties loadProperties(URL propertiesUrl) {
final Properties properties = new Properties();
try (InputStream is = propertiesUrl.openStream()) {
log.debug("loading {}", propertiesUrl);
Properties props = new Properties();
props.load(is);
File baseDir;
if (isFile) {
baseDir = new File(propertiesUrl.getPath()).getParentFile();
} else {
// use current working directory for includes/overrides
baseDir = new File(System.getProperty("user.dir"));
}
// allow including other properties files
props = loadIncludes(baseDir, props);
// allow overriding properties
props = loadOverrides(baseDir, props);
// collect settings for the current runtime mode
String prefix = "%" + runtimeMode.toString() + ".";
// sort names, mode-specific names are first
Set names = new TreeSet<>(props.stringPropertyNames());
for (String name : names) {
if (name.charAt(0) == '%') {
// mode-specific setting
if (name.startsWith(prefix)) {
// setting for the current runtime mode
properties.put(name.substring(prefix.length()), props.get(name));
}
} else if (!properties.containsKey(name)) {
// setting for all modes
properties.put(name, props.get(name));
}
}
} catch (FileNotFoundException f) {
throw new PippoRuntimeException(f, "Failed to find {}", propertiesUrl);
} catch (Exception t) {
throw new PippoRuntimeException(t, "Failed to read {}", propertiesUrl);
}
return properties;
}
/**
* Recursively read "include" properties files.
*
* "Include" properties are the base properties which are overwritten by
* the provided properties.
*
*
* @param baseDir
* @param properties
* @return the merged properties
* @throws IOException
*/
private Properties loadIncludes(File baseDir, Properties properties) throws IOException {
return loadProperties(baseDir, properties, "include", false);
}
/**
* Recursively read "override" properties files.
*
* "Override" properties overwrite the provided properties.
*
*
* @param baseDir
* @param properties
* @return the merged properties
* @throws IOException
*/
private Properties loadOverrides(File baseDir, Properties properties) throws IOException {
return loadProperties(baseDir, properties, "override", true);
}
/**
* Recursively read referenced properties files.
*
* @param baseDir
* @param currentProperties
* @param key
* @param override
* @return the merged properties
* @throws IOException
*/
private Properties loadProperties(File baseDir, Properties currentProperties, String key, boolean override) throws IOException {
Properties loadedProperties = new Properties();
String include = (String) currentProperties.remove(key);
if (!StringUtils.isNullOrEmpty(include)) {
// allow for multiples
List names = StringUtils.getList(include, DEFAULT_LIST_DELIMITER);
for (String name : names) {
if (StringUtils.isNullOrEmpty(name)) {
continue;
}
// interpolate any variables
final String fileName = interpolateString(name);
// try co-located
File file = new File(baseDir, fileName);
if (!file.exists()) {
// try absolute path
file = new File(fileName);
}
if (!file.exists()) {
log.warn("failed to locate {}", file);
continue;
}
// load properties
log.debug("loading {} settings from {}", key, file);
try (FileInputStream iis = new FileInputStream(file)) {
loadedProperties.load(iis);
}
// read nested properties
loadedProperties = loadProperties(file.getParentFile(), loadedProperties, key, override);
}
}
Properties merged = new Properties();
if (override) {
// "override" settings overwrite the current properties
merged.putAll(currentProperties);
merged.putAll(loadedProperties);
} else {
// "include" settings are overwritten by the current properties
merged.putAll(loadedProperties);
merged.putAll(currentProperties);
}
return merged;
}
/**
* Creates a map of prepared interpolation values.
*
* All values support the standard ${name} syntax but also support
* the @{name} syntax to cope with build systems with aggressive
* token filtering.
*
*/
protected void loadInterpolationValues() {
// System properties may be accessed using ${name} or @{name}
for (String propertyName : System.getProperties().stringPropertyNames()) {
String value = System.getProperty(propertyName);
if (!StringUtils.isNullOrEmpty(value)) {
addInterpolationValue(propertyName, value);
}
}
// Environment variables may be accessed using ${env.NAME} or @{env.NAME}
for (String variableName : System.getenv().keySet()) {
String value = System.getenv(variableName);
if (!StringUtils.isNullOrEmpty(value)) {
addInterpolationValue("env." + variableName, value);
}
}
}
/**
* Add a value that may be interpolated.
*
* @param name
* @param value
*/
protected void addInterpolationValue(String name, String value) {
interpolationValues.put(String.format("${%s}", name), value);
interpolationValues.put(String.format("@{%s}", name), value);
}
/**
* Interpolates a string value using System properties and Environment variables.
*
* @param value
* @return an interpolated string
*/
protected String interpolateString(String value) {
String interpolatedValue = value;
for (Map.Entry entry : interpolationValues.entrySet()) {
interpolatedValue = interpolatedValue.replace(entry.getKey(), entry.getValue());
}
return interpolatedValue;
}
/**
* Returns the list of all setting names.
*
* @return list of setting names
*/
public List getSettingNames() {
return getSettingNames(null);
}
/**
* Returns the list of settings whose name starts with the specified prefix. If
* the prefix is null or empty, all settings names are returned.
*
* @param startingWith
* @return list of setting names
*/
public List getSettingNames(String startingWith) {
List names = new ArrayList<>();
Properties props = getProperties();
if (StringUtils.isNullOrEmpty(startingWith)) {
names.addAll(props.stringPropertyNames());
} else {
startingWith = startingWith.toLowerCase();
for (Object o : props.keySet()) {
String name = o.toString();
if (name.toLowerCase().startsWith(startingWith)) {
names.add(name);
}
}
}
return names;
}
/**
* Returns the string value for the specified name. If the name does not exist
* or the value for the name can not be interpreted as a string, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public String getString(String name, String defaultValue) {
String value = getProperties().getProperty(name, defaultValue);
value = overrides.getProperty(name, value);
return value;
}
/**
* Returns a string value that has been interpolated from System Properties
* and Environment Variables using the ${name} or @{name} syntax.
*
* @param name
* @param defaultValue
* @return an interpolated string
*/
public String getInterpolatedString(String name, String defaultValue) {
String value = getString(name, defaultValue);
return interpolateString(value);
}
/**
* Returns the boolean value for the specified name. If the name does not
* exist or the value for the name can not be interpreted as a boolean, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public boolean getBoolean(String name, boolean defaultValue) {
String value = getString(name, null);
if (!StringUtils.isNullOrEmpty(value)) {
return Boolean.parseBoolean(value.trim());
}
return defaultValue;
}
/**
* Returns the integer value for the specified name. If the name does not
* exist or the value for the name can not be interpreted as an integer, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public int getInteger(String name, int defaultValue) {
try {
String value = getString(name, null);
if (!StringUtils.isNullOrEmpty(value)) {
return Integer.parseInt(value.trim().split(" ")[0]);
}
} catch (NumberFormatException e) {
log.warn("Failed to parse integer for " + name + USING_DEFAULT_OF
+ defaultValue);
}
return defaultValue;
}
/**
* Returns the long value for the specified name. If the name does not
* exist or the value for the name can not be interpreted as an long, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public long getLong(String name, long defaultValue) {
try {
String value = getString(name, null);
if (!StringUtils.isNullOrEmpty(value)) {
return Long.parseLong(value.trim().split(" ")[0]);
}
} catch (NumberFormatException e) {
log.warn("Failed to parse long for " + name + USING_DEFAULT_OF
+ defaultValue);
}
return defaultValue;
}
/**
* Returns the float value for the specified name. If the name does not
* exist or the value for the name can not be interpreted as a float, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public float getFloat(String name, float defaultValue) {
try {
String value = getString(name, null);
if (!StringUtils.isNullOrEmpty(value)) {
return Float.parseFloat(value.trim().split(" ")[0]);
}
} catch (NumberFormatException e) {
log.warn("Failed to parse float for " + name + USING_DEFAULT_OF
+ defaultValue);
}
return defaultValue;
}
/**
* Returns the double value for the specified name. If the name does not
* exist or the value for the name can not be interpreted as a double, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public double getDouble(String name, double defaultValue) {
try {
String value = getString(name, null);
if (!StringUtils.isNullOrEmpty(value)) {
return Double.parseDouble(value.trim().split(" ")[0]);
}
} catch (NumberFormatException e) {
log.warn("Failed to parse double for " + name + USING_DEFAULT_OF
+ defaultValue);
}
return defaultValue;
}
/**
* Returns the char value for the specified name. If the name does not exist
* or the value for the name can not be interpreted as a char, the
* defaultValue is returned.
*
* @param name
* @param defaultValue
* @return name value or defaultValue
*/
public char getChar(String name, char defaultValue) {
String value = getString(name, null);
if (!StringUtils.isNullOrEmpty(value)) {
return value.trim().charAt(0);
}
return defaultValue;
}
/**
* Returns the string value for the specified name. If the name does not
* exist an exception is thrown.
*
* @param name
* @return name value
*/
public String getRequiredString(String name) {
String value = getString(name, null);
if (value != null) {
return value.trim();
}
throw new PippoRuntimeException("Setting '{}' has not been configured!", name);
}
/**
* Returns a list of comma-delimited strings from the specified name.
*
* @param name
* @return list of strings
*/
public List getStrings(String name) {
return getStrings(name, DEFAULT_LIST_DELIMITER);
}
/**
* Returns a list of strings from the specified name using the specified delimiter.
*
* @param name
* @param delimiter
* @return list of strings
*/
public List getStrings(String name, String delimiter) {
String value = getString(name, null);
if (StringUtils.isNullOrEmpty(value)) {
return Collections.emptyList();
}
return StringUtils.getList(value, delimiter);
}
/**
* Returns a list of comma-delimited integers from the specified name.
*
* @param name
* @return list of strings
*/
public List getIntegers(String name) {
return getIntegers(name, DEFAULT_LIST_DELIMITER);
}
/**
* Returns a list of integers from the specified name using the specified delimiter.
*
* @param name
* @param delimiter
* @return list of integers
*/
public List getIntegers(String name, String delimiter) {
List strings = getStrings(name, delimiter);
if (strings.isEmpty()) {
return Collections.emptyList();
}
List ints = new ArrayList<>();
for (String value : strings) {
try {
int i = Integer.parseInt(value);
ints.add(i);
} catch (NumberFormatException e) {
}
}
return Collections.unmodifiableList(ints);
}
/**
* Returns a list of comma-delimited longs from the specified name.
*
* @param name
* @return list of strings
*/
public List getLongs(String name) {
return getLongs(name, DEFAULT_LIST_DELIMITER);
}
/**
* Returns a list of longs from the specified name using the specified delimiter.
*
* @param name
* @param delimiter
* @return list of longs
*/
public List getLongs(String name, String delimiter) {
List strings = getStrings(name, delimiter);
if (strings.isEmpty()) {
return Collections.emptyList();
}
List longs = new ArrayList<>();
for (String value : strings) {
try {
long i = Long.parseLong(value);
longs.add(i);
} catch (NumberFormatException e) {
}
}
return Collections.unmodifiableList(longs);
}
/**
* Gets the duration setting and converts it to milliseconds.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @return milliseconds
*/
public long getDurationInMilliseconds(String name) {
return getDurationInMilliseconds(name, 0);
}
/**
* Gets the duration setting and converts it to milliseconds.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @param defaultValue in milliseconds
* @return milliseconds
*/
public long getDurationInMilliseconds(String name, long defaultValue) {
TimeUnit timeUnit = extractTimeUnit(name, defaultValue + " MILLISECONDS");
long duration = getLong(name, defaultValue);
return timeUnit.toMillis(duration);
}
/**
* Gets the duration setting and converts it to seconds.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @return seconds
*/
public long getDurationInSeconds(String name) {
return getDurationInSeconds(name, 0);
}
/**
* Gets the duration setting and converts it to seconds.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @param defaultValue in seconds
* @return seconds
*/
public long getDurationInSeconds(String name, long defaultValue) {
TimeUnit timeUnit = extractTimeUnit(name, defaultValue + " SECONDS");
long duration = getLong(name, defaultValue);
return timeUnit.toSeconds(duration);
}
/**
* Gets the duration setting and converts it to minutes.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @return minutes
*/
public long getDurationInMinutes(String name) {
return getDurationInMinutes(name, 0);
}
/**
* Gets the duration setting and converts it to minutes.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @param defaultValue in minutes
* @return minutes
*/
public long getDurationInMinutes(String name, long defaultValue) {
TimeUnit timeUnit = extractTimeUnit(name, defaultValue + " MINUTES");
long duration = getLong(name, defaultValue);
return timeUnit.toMinutes(duration);
}
/**
* Gets the duration setting and converts it to hours.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @return hours
*/
public long getDurationInHours(String name) {
return getDurationInHours(name, 0);
}
/**
* Gets the duration setting and converts it to hours.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @param defaultValue in hours
* @return hours
*/
public long getDurationInHours(String name, long defaultValue) {
TimeUnit timeUnit = extractTimeUnit(name, defaultValue + " HOURS");
long duration = getLong(name, defaultValue);
return timeUnit.toHours(duration);
}
/**
* Gets the duration setting and converts it to days.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @return days
*/
public long getDurationInDays(String name) {
return getDurationInDays(name, 0);
}
/**
* Gets the duration setting and converts it to days.
*
* The setting must be use one of the following conventions:
*
* - n MILLISECONDS
*
- n SECONDS
*
- n MINUTES
*
- n HOURS
*
- n DAYS
*
*
* @param name
* @param defaultValue in days
* @return days
*/
public long getDurationInDays(String name, long defaultValue) {
TimeUnit timeUnit = extractTimeUnit(name, defaultValue + " DAYS");
long duration = getLong(name, defaultValue);
return timeUnit.toDays(duration);
}
/**
* Extracts the TimeUnit from the name.
*
* @param name
* @param defaultValue
* @return the extracted TimeUnit
*/
private TimeUnit extractTimeUnit(String name, String defaultValue) {
String value = getString(name, defaultValue);
try {
final String[] s = value.split(" ", 2);
return TimeUnit.valueOf(s[1].trim().toUpperCase());
} catch (Exception e) {
throw new PippoRuntimeException("{} must have format ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'", name);
}
}
/**
* Tests for the existence of a setting.
*
* @param name
* @return true if the setting exists
*/
public boolean hasSetting(String name) {
return getString(name, null) != null;
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, boolean value) {
overrides.put(name, Boolean.toString(value));
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, String value) {
overrides.put(name, value);
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, char value) {
overrides.put(name, Character.toString(value));
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, int value) {
overrides.put(name, Integer.toString(value));
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, long value) {
overrides.put(name, Long.toString(value));
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, float value) {
overrides.put(name, Float.toString(value));
}
/**
* Override the setting at runtime with the specified value.
* This change does not persist.
*
* @param name
* @param value
*/
public void overrideSetting(String name, double value) {
overrides.put(name, Double.toString(value));
}
}