package org.ovirt.engine.core.uutils.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Shell like configuration file parsing.
* This class parses a configuration file in a format in which shell uses.
* It is far from being complete, so caution should be taken. The format which is
* supported is double quotes, $ to exapnd variables and \ to escape.
* <p>Examples:</p>
* {@code}<pre>
* # comment
* key00=value0
* key01=
* key02=value2
* key03=value31 value32
* key04="value41 value42"
* key05="value51\"value52\"value53"
* key06="value61#value62"
* key07="value71#value72"# comment
* key08="value81#value82" # comment
* key10="prefix ${key01} ${key02} ${unknown} ${key03} ${key04} suffix"
* key11="\${key02}"
* </pre>{@code}
*/
public class ShellLikeConfd {
private static final Logger log = LoggerFactory.getLogger(ShellLikeConfd.class);
private static final String SENSITIVE_KEYS = "SENSITIVE_KEYS";
// Compile regular expressions:
private static final Pattern EMPTY_LINE = Pattern.compile("^\\s*(#.*)?$");
private static final Pattern KEY_VALUE_EXPRESSION = Pattern.compile("^\\s*(\\w+)=(.*)$");
// The properties object storing the current values of the parameters:
private Map<String, String> values = new HashMap<>();
/**
* Use configuration from map.
* @param values map.
*/
protected void setConfig(Map<String, String> values) {
this.values = values;
dumpConfig();
}
/**
* Use configuration from files.
* @param defaultsPath path to file containing the defaults.
* @param varsPath path to file and directory of file.d.
*/
protected void loadConfig(String defaultsPath, String varsPath) {
// This is the list of configuration files that will be loaded and
// merged (the initial size is 2 because usually we will have only two
// configuration files to merge, the defaults and the variables):
List<File> configFiles = new ArrayList<>(2);
if (!StringUtils.isEmpty(defaultsPath)) {
File defaultsFile = new File(defaultsPath);
configFiles.add(defaultsFile);
}
if (!StringUtils.isEmpty(varsPath)) {
File varsFile = new File(varsPath);
configFiles.add(varsFile);
// Locate the override values directory and add the .conf files inside
// to the list, sorted alphabetically:
File[] varsFiles = new File(varsPath + ".d").listFiles((parent, name) -> name.endsWith(".conf"));
if (varsFiles != null) {
Arrays.sort(varsFiles, Comparator.comparing(File::getName));
for (File file : varsFiles) {
configFiles.add(file);
}
}
}
// Load the configuration files in the order they are in the list:
for (File configFile : configFiles) {
try {
loadProperties(configFile);
}
catch (IOException exception) {
String message = "Can't load configuration file.";
log.error(message, exception);
throw new IllegalStateException(message, exception);
}
}
dumpConfig();
}
/**
* Dump all configuration to the log.
* this should probably be DEBUG, but as it will usually happen only once,
* during the startup, is not that a roblem to use INFO.
*/
private void dumpConfig() {
if (log.isInfoEnabled()) {
Set<String> keys = values.keySet();
List<String> list = new ArrayList<>(keys.size());
List<String> sensitiveKeys = Arrays.asList(getSensitiveKeys());
list.addAll(keys);
Collections.sort(list);
for (String key : list) {
String value = "***";
if (!sensitiveKeys.contains(key)) {
value = values.get(key);
}
log.info("Value of property '{}' is '{}'.", key, value);
}
}
}
/**
* Load the contents of the properties file located by the given environment
* variable or file.
*
* @param file the file that will be used to load the properties if the given
* environment variable doesn't have a value
*/
private void loadProperties(File file) throws IOException {
// Do nothing if the file doesn't exist or isn't readable:
if (!file.canRead()) {
log.info("The file '{}' doesn't exist or isn't readable. Will return an empty set of properties.", file.getAbsolutePath());
return;
}
// Load the file:
int index = 0;
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))
) {
String line = null;
while ((line = reader.readLine()) != null) {
index++;
loadLine(line);
}
log.info("Loaded file '{}'.", file.getAbsolutePath());
}
catch (Exception e) {
String msg = String.format(
"Can't load file '%s' line %d: %s",
file.getAbsolutePath(),
index,
e
);
log.error(msg, e);
throw new RuntimeException(msg, e);
}
}
/**
* Expand string using current variables.
*
* @param value String.
* @return Expanded string.
*/
public String expandString(String value) {
StringBuilder ret = new StringBuilder();
boolean escape = false;
boolean inQuotes = false;
int index = 0;
while (index < value.length()) {
char c = value.charAt(index++);
if (escape) {
escape = false;
ret.append(c);
}
else {
switch(c) {
case '\\':
escape = true;
break;
case '$':
if (value.charAt(index++) != '{') {
throw new RuntimeException("Malformed variable assignement");
}
int i = value.indexOf('}', index);
if (i == -1) {
throw new RuntimeException("Malformed variable assignement");
}
String name = value.substring(index, i);
index = i+1;
String v = values.get(name);
if (v != null) {
ret.append(v);
}
else {
v = System.getProperty(name);
if (v != null) {
ret.append(v);
}
}
break;
case '"':
inQuotes = !inQuotes;
break;
case ' ':
case '#':
if (inQuotes) {
ret.append(c);
}
else {
index = value.length();
}
break;
default:
ret.append(c);
break;
}
}
}
return ret.toString();
}
/**
* Load the contents of a line from a properties file, expanding
* references to variables.
*
* @param line the line from the properties file
*/
private void loadLine(String line) throws IOException {
Matcher blankMatch = EMPTY_LINE.matcher(line);
if (!blankMatch.find()) {
Matcher keyValueMatch = KEY_VALUE_EXPRESSION.matcher(line);
if (!keyValueMatch.find()) {
throw new RuntimeException("Invalid line");
}
values.put(
keyValueMatch.group(1),
expandString(keyValueMatch.group(2))
);
}
}
/**
* Get all properties.
*
* @return map of all properties.
*/
public Map<String, String> getProperties() {
return Collections.unmodifiableMap(values);
}
/**
* Get the value of a property given its name and an optional underscore separated suffix.
* if key_optionalSuffix has a value return it. otherwise return the value of key.
*
* @param key the name of the property
* @param optionalSuffix the suffix of the property, not including an underscore.
* @param allowMissing define the behaviour if both key and key_optionalSuffix are not associated with a value
* @return the value associated with key_optionalSuffix if it is defined or the one associated with key otherwise.
* @throws java.lang.IllegalArgumentException
* if both key_optionalSuffix and key are not associated with a value and allowMissing is false.
*/
public String getProperty(String key, String optionalSuffix, boolean allowMissing) {
String property = getProperty(key + "_" + optionalSuffix, true);
if (StringUtils.isEmpty(property)) {
property = getProperty(key, allowMissing);
}
return property;
}
/**
* Get the value of a property given its name.
*
* @param key the name of the property
* @param allowMissing return null if missing
* @return the value of the property as contained in the configuration file
* @throws java.lang.IllegalStateException if the property doesn't have a
* value
*/
public String getProperty(String key, boolean allowMissing) {
String value = values.get(key);
if (value == null && !allowMissing) {
// Loudly alert in the log and throw an exception:
String message = "The property \"" + key + "\" doesn't have a value.";
log.error(message);
throw new IllegalArgumentException(message);
// Or maybe kill ourselves, as a missing configuration parameter is
// a serious error:
// System.exit(1)
}
return value;
}
/**
* Get the value of a property given its name.
*
* @param key the name of the property
* @return the value of the property as contained in the configuration file
* @throws java.lang.IllegalStateException if the property doesn't have a
* value
*/
public String getProperty(String key) {
return getProperty(key, false);
}
// Accepted values for boolean properties (please keep them sorted as we use
// a binary search to check if a given property matches one of these
// values):
private static final String[] TRUE_VALUES = { "1", "t", "true", "y", "yes" };
private static final String[] FALSE_VALUES = { "0", "f", "false", "n", "no" };
/**
* Get the value of a boolean property given its name. It will take the text
* of the property and return <code>true</code> if it is <code>true</code> if the text
* of the property is <code>true</code>, <code>t</code>, <code>yes</code>,
* <code>y</code> or <code>1</code> (ignoring case).
*
* @param key the name of the property
* @param defaultValue default value to return, null do not allow.
* @return the boolean value of the property
* @throws java.lang.IllegalArgumentException if the properties doesn't have
* a value or if the value is not a valid boolean
*/
public boolean getBoolean(String key, Boolean defaultValue) {
boolean ret;
// Get the text of the property and convert it to lowercase:
String value = getProperty(key, defaultValue != null);
if (StringUtils.isEmpty(value)) {
ret = defaultValue.booleanValue();
}
else {
value = value.trim().toLowerCase();
// Check if it is one of the true values:
if (Arrays.binarySearch(TRUE_VALUES, value) >= 0) {
ret = true;
}
// Check if it is one of the false values:
else if (Arrays.binarySearch(FALSE_VALUES, value) >= 0) {
ret = false;
}
else {
// No luck, will alert in the log that the text is not valid and throw
// an exception:
String message = "The value \"" + value + "\" for property \"" + key + "\" is not a valid boolean.";
log.error(message);
throw new IllegalArgumentException(message);
}
}
return ret;
}
/**
* Get the value of a boolean property given its name. It will take the text
* of the property and return <code>true</code> if it is <code>true</code> if the text
* of the property is <code>true</code>, <code>t</code>, <code>yes</code>,
* <code>y</code> or <code>1</code> (ignoring case).
*
* @param key the name of the property
* @param defaultValue default value to return, null do not allow.
* @return the boolean value of the property
* @throws java.lang.IllegalArgumentException if the properties doesn't have
* a value or if the value is not a valid boolean
*/
public boolean getBoolean(String key) {
return getBoolean(key, null);
}
/**
* Get the value of an integer property given its name. If the text of the
* value can't be converted to an integer a message will be sent to the log
* and an exception thrown.
*
* @param key the name of the property
* @param defaultValue default value to return, null do not allow.
* @return the integer value of the property
* @throws java.lang.IllegalArgumentException if the property doesn't have a
* value or the value is not a valid integer
*/
public int getInteger(String key, Integer defaultValue) {
int ret;
String value = getProperty(key, defaultValue != null);
if (StringUtils.isEmpty(value)) {
ret = defaultValue.intValue();
}
else {
try {
ret = Integer.parseInt(value);
}
catch (NumberFormatException exception) {
String message = "The value \"" + value + "\" for property \"" + key + "\" is not a valid integer.";
log.error(message, exception);
throw new IllegalArgumentException(message, exception);
}
}
return ret;
}
/**
* Get the value of an integer property given its name. If the text of the
* value can't be converted to an integer a message will be sent to the log
* and an exception thrown.
*
* @param key the name of the property
* @return the integer value of the property
* @throws java.lang.IllegalArgumentException if the property doesn't have a
* value or the value is not a valid integer
*/
public int getInteger(String key) {
return getInteger(key, null);
}
/**
* Get the value of an long property given its name. If the text of the
* value can't be converted to an long a message will be sent to the log
* and an exception thrown.
*
* @param key the name of the property
* @param defaultValue default value to return, null do not allow.
* @return the long value of the property
* @throws java.lang.IllegalArgumentException if the property doesn't have a
* value or the value is not a valid long
*/
public long getLong(String key, Long defaultValue) {
long ret;
String value = getProperty(key, defaultValue != null);
if (StringUtils.isEmpty(value)) {
ret = defaultValue.intValue();
}
else {
try {
ret = Long.parseLong(value);
}
catch (NumberFormatException exception) {
String message = "The value \"" + value + "\" for property \"" + key + "\" is not a valid long integer.";
log.error(message, exception);
throw new IllegalArgumentException(message, exception);
}
}
return ret;
}
/**
* Get the value of an long property given its name. If the text of the
* value can't be converted to an long a message will be sent to the log
* and an exception thrown.
*
* @param key the name of the property
* @return the long value of the property
* @throws java.lang.IllegalArgumentException if the property doesn't have a
* value or the value is not a valid long
*/
public long getLong(String key) {
return getLong(key, null);
}
/**
* Get the value of a property corresponding to a file or directory name.
*
* @param key the name of the property
* @return the file object corresponding to the value of the property
*/
public File getFile(String key) {
String value = getProperty(key);
return new File(value);
}
public String[] getSensitiveKeys() {
String sensitiveKeys = values.get(SENSITIVE_KEYS);
if (sensitiveKeys == null) {
return new String[] {};
}
else {
return sensitiveKeys.split(",");
}
}
}