ServiceConfigGuide.java
package emissary.config;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StreamTokenizer;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Nullable;
/**
* This class implements the Configurator interface for services within the Emissary framework.
*/
public class ServiceConfigGuide implements Configurator, Serializable {
static final long serialVersionUID = 3906838615422657150L;
public static final char SLASH = '/';
public static final char COLON = ':';
public static final String DOUBLESLASH = "//";
protected static final Logger logger = LoggerFactory.getLogger(ServiceConfigGuide.class);
protected static final String DEFAULT_FILE_NAME = "default.cfg";
protected static final String POST_FILE_NAME = "post.cfg";
// Used on the RHS to make a null assignment
// Obsolete, use @{NULL}
protected static final String NULL_VALUE = "<null>";
// Hold all service specific parameters in a list
protected List<ConfigEntry> serviceParameters = new ArrayList<>();
// Hold all remove config entries, operator of !=
protected List<ConfigEntry> removeParameters = new ArrayList<>();
protected String operator;
// Start and end to a dynamic substitution
protected static final String VSTART = "@{";
protected static final String VEND = "}";
// Shared map of all environment properties
// Access them with @ENV{'os.name'} for example
// This is obsolete, all values from properties and
// environment are now in the main values map and available
// for immediate substitution
protected static final String ENVSTART = "@ENV{'";
protected static final String ENVSTOP = "'}";
// Map of last values seen
protected Map<String, String> values = new HashMap<>();
// Get this once per jvm
private static final String hostname;
// Grab the hostname for @{HOST} replacement
static {
String tmpHostname;
try {
tmpHostname = InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException e) {
logger.error("Error getting host name", e);
tmpHostname = "localhost";
}
hostname = tmpHostname;
}
/**
* Public default constructor
*/
public ServiceConfigGuide() {
initializeValues();
}
/**
* Public constructor with dir and filename
*
* @param path the directory where config files are
* @param file the name of te file in the directory
*/
public ServiceConfigGuide(final String path, final String file) throws IOException {
this(path + File.separator + file);
}
/**
* Public default constructor with file name
*
* @param filename the name of the disk file
*/
public ServiceConfigGuide(final String filename) throws IOException {
this();
try {
readConfigData(filename);
} catch (ConfigSyntaxException ex) {
throw new IOException("Cannot parse configuration file " + ex.getMessage(), ex);
}
}
/**
* Public default constructor with InputStream
*
* @param is the InputStream
*/
public ServiceConfigGuide(final InputStream is) throws IOException {
this();
try {
readConfigData(is);
} catch (ConfigSyntaxException ex) {
throw new IOException("Cannot parse configuration file " + ex.getMessage(), ex);
}
}
/**
* Public default constructor with InputStream and name
*
* @param is the InputStream
* @param name the name of the stream good for reporting errors
*/
public ServiceConfigGuide(final InputStream is, final String name) throws IOException {
this();
try {
readConfigData(is, name);
} catch (ConfigSyntaxException ex) {
logger.error("Caught ConfigSytaxException {}", ex.getMessage());
throw new IOException("Cannot parse configuration file " + ex.getMessage(), ex);
}
}
/**
* Initialize the values map, which is used to replace stuff in the configs
*/
protected void initializeValues() {
this.values.clear();
// TODO: see if we can stop adding all env variables and
// system properties to the replace values
// Add all the environment variables
this.values.putAll(System.getenv());
// Add all the system properties
final Properties props = System.getProperties();
for (Enumeration<?> e = props.propertyNames(); e.hasMoreElements();) {
final String key = (String) e.nextElement();
logger.trace("Adding {} to replaceable properties", key);
this.values.put(key, props.getProperty(key));
}
// used for substitution when reading cfg files
this.values.put("CONFIG_DIR", StringUtils.join(ConfigUtil.getConfigDirs(), ","));
this.values.put("PRJ_BASE", ConfigUtil.getProjectBase());
this.values.put("PROJECT_BASE", ConfigUtil.getProjectBase());
this.values.put("OUTPUT_ROOT", ConfigUtil.getOutputRoot());
this.values.put("BIN_DIR", ConfigUtil.getBinDir());
this.values.put("HOST", hostname);
this.values.put("/", File.separator);
this.values.put("TMPDIR", System.getProperty("java.io.tmpdir"));
this.values.put("NULL", null);
this.values.put("OS.NAME", System.getProperty("os.name").replace(' ', '_'));
this.values.put("OS.VER", System.getProperty("os.version").replace(' ', '_'));
this.values.put("OS.ARCH", System.getProperty("os.arch").replace(' ', '_'));
}
/**
* Reads the configuration file specified in the argument and sets the mandatory parameters.
*/
protected void readConfigData(final String filename) throws IOException, ConfigSyntaxException {
readSingleConfigFile(filename);
}
public void readConfigData(final InputStream is) throws IOException, ConfigSyntaxException {
readConfigData(is, "UNKNOWN");
}
protected void readConfigData(final InputStream is, final String filename) throws IOException, ConfigSyntaxException {
final Reader r = new BufferedReader(new InputStreamReader(is));
final StreamTokenizer in = new StreamTokenizer(r);
int nextToken = StreamTokenizer.TT_WORD;
String parmName;
String sval;
in.commentChar('#');
in.wordChars(33, 33);
in.wordChars(36, 47);
in.wordChars(58, 64);
in.wordChars(91, 96);
in.wordChars(123, 65536);
while (nextToken != StreamTokenizer.TT_EOF) {
// Read three tokens at a time (X = Y)
nextToken = in.nextToken();
// Make sure the first token in the tuple is a word
if (nextToken == StreamTokenizer.TT_EOF) {
break;
}
if (nextToken == StreamTokenizer.TT_NUMBER) {
throw new ConfigSyntaxException("Illegal token " + in.sval + ", missing quote on line " + in.lineno() + "?");
}
parmName = in.sval;
nextToken = in.nextToken();
this.operator = in.sval;
nextToken = in.nextToken();
if (nextToken == StreamTokenizer.TT_NUMBER) {
sval = Long.toString((long) in.nval);
} else {
sval = in.sval;
}
if (sval == null) {
// Problem is likely on previous line
throw new ConfigSyntaxException("Illegal token " + parmName + ", missing space or value on line " + (in.lineno() - 1) + "?");
}
handleNewEntry(parmName, sval, this.operator, filename, in.lineno() - 1, false);
}
r.close();
is.close();
}
protected void readSingleConfigFile(final String filename) throws IOException, ConfigSyntaxException {
logger.debug("Reading config file {}", filename);
final InputStream is = ConfigUtil.getConfigData(filename);
readConfigData(is, filename);
}
/**
* Handle a newly parsed or passed in entry. Substitutions are handled on both the LHS and RHS, then the values are
* stored as a ConfigEntry in our local list and map. Only the last value in the map is available for substitutions. LHS
* is analyzed before RHS and in L to R order.
*
* @param parmNameArg the LHS
* @param svalArg the raw RHS
* @param operatorArg the equation
* @param filename the filename we are parsing for error reporting
* @param lineno the line number we are currently reporting the error on
* @param merge true when adding in from a merge
* @return a new config entry with the expanded key and value
* @throws IOException when the key or value is malformed
*/
protected ConfigEntry handleNewEntry(final String parmNameArg, final String svalArg, final String operatorArg, final String filename,
final int lineno, final boolean merge) throws IOException {
final String parmName = handleReplacements(parmNameArg, filename, lineno);
final String sval = handleReplacements(svalArg, filename, lineno);
// Create a config entry from this
final ConfigEntry anEntry = new ConfigEntry(parmName, sval);
if ("!=".equals(operatorArg)) {
if ("*".equals(sval)) {
removeAllEntries(parmName);
this.values.remove(parmName);
} else {
removeEntry(anEntry);
if (sval.equals(this.values.get(parmName))) {
this.values.remove(parmName);
}
}
this.removeParameters.add(anEntry);
} else {
// Save the entry in the list
if (merge) {
this.serviceParameters.add(0, anEntry);
} else {
this.serviceParameters.add(anEntry);
}
// Save this pair in the map
this.values.put(parmName, sval);
}
if ("IMPORT_FILE".equals(parmName) || "OPT_IMPORT_FILE".equals(parmName)) {
final List<String> fileFlavorList = new ArrayList<>();
// Add the base file and then add all the flavor versions
fileFlavorList.add(sval);
final String[] fileFlavors = ConfigUtil.addFlavors(sval);
if (ArrayUtils.isNotEmpty(fileFlavors)) {
fileFlavorList.addAll(Arrays.asList(fileFlavors));
}
logger.debug("ServiceConfigGuide::handleNewEntry -- FileFlavorList = {}", fileFlavorList);
// loop through the files and attempt to read/merger the configurations.
for (int i = 0; i < fileFlavorList.size(); i++) {
final String fileFlavor = fileFlavorList.get(i);
// recursion alert: This could lead to getFile being called
try {
readConfigData(ConfigUtil.getConfigStream(fileFlavor), fileFlavor);
} catch (ConfigSyntaxException e) {
// whether opt or not, syntax errors are a problem
throw new IOException(parmName + " = " + sval + " from " + filename + " failed " + e.getMessage(), e);
} catch (IOException e) {
// Throw exception if it is an IMPORT_FILE and the base file is not found
if ("IMPORT_FILE".equals(parmName) && i == 0) {
String importFileName = Paths.get(svalArg).getFileName().toString();
throw new IOException("In " + filename + ", cannot find IMPORT_FILE: " + sval
+ " on the specified path. Make sure IMPORT_FILE (" + importFileName + ") exists, and the file path is correct.",
e);
}
}
}
return anEntry;
} else if ("CREATE_DIRECTORY".equals(parmName) && !createDirectory(sval)) {
logger.warn("{}: Cannot create directory {}", filename, sval);
} else if ("CREATE_FILE".equals(parmName) && !createFile(sval)) {
logger.warn("{}: Cannot create file {}", filename, sval);
}
return anEntry;
}
/**
* Handle all the possible replacements in a string value
*
* @param svalArg the raw value
* @param filename the filename we are parsing for error reporting
* @param lineno the line number we are currently reporting the error on
* @return the expanded value with all legal @{..} values replaced
* @throws IOException when the value is malformed
*/
protected String handleReplacements(final String svalArg, final String filename, final int lineno) throws IOException {
String sval = svalArg;
int startpos = 0;
while (sval != null && sval.indexOf(VSTART, startpos) > -1) {
final int ndx = sval.indexOf(VSTART, startpos);
final int edx = sval.indexOf(VEND, ndx + VSTART.length());
if (ndx == -1 && ndx >= edx) {
throw new IOException("Problem parsing line " + lineno + " " + sval);
}
final String tok = sval.substring(ndx + VSTART.length(), edx);
logger.debug("Replacement token is {}", tok);
final String mapval = this.values.get(tok);
if (mapval != null) {
sval = sval.substring(0, ndx) + mapval + sval.substring(edx + VEND.length());
} else {
logger.warn("Did not find replacement for '{}' in file {} at line {}", tok, filename, lineno);
startpos = edx + VEND.length();
}
}
// This is obsolete
if (sval != null && sval.contains(ENVSTART)) {
sval = substituteEnvProps(sval, filename, lineno);
}
// Do unicode stuff
if (sval != null && (sval.contains("\\u") || sval.contains("\\U"))) {
sval = substituteUtfChars(sval, filename, lineno);
}
// This is obsolete
if (sval != null && sval.equals(NULL_VALUE)) {
sval = null;
logger.debug("Using {} is deprecated, please just use {}NULL{}", NULL_VALUE, VSTART, VEND);
}
return sval;
}
/**
* Substitute any java unicode character values: \\uxxxx
*
* @param s the string to process
* @param filename the name of the file we are in for error reporting
* @param lnum the current line number for error reporting
* @return string with character values replaced
*/
protected String substituteUtfChars(final String s, final String filename, final int lnum) throws IOException {
final int slen = s.length();
final StringBuilder sb = new StringBuilder(slen);
for (int i = 0; i < slen; i++) {
if (s.charAt(i) != '\\') {
sb.append(s.charAt(i));
} else if ((i + 4) < slen && (s.charAt(i + 1) == 'u' || s.charAt(i + 1) == 'U')) {
int epos = i + 2;
final int max = (s.charAt(epos) == '1' || s.charAt(epos) == '0') ? (i + 7) : (i + 6);
while (epos < slen
&& epos < max
&& ((s.charAt(epos) >= '0' && s.charAt(epos) <= '9') || (s.charAt(epos) >= 'A' && s.charAt(epos) <= 'F')
|| (s.charAt(epos) >= 'a' && s
.charAt(epos) <= 'f'))) {
epos++;
}
if (epos <= slen) {
try {
final int digit = Integer.parseInt(s.substring(i + 2, epos), 16);
sb.appendCodePoint(digit);
i = epos - 1;
} catch (RuntimeException ex) {
throw new IOException("Unable to convert characters in " + s + ", from filename=" + filename + " line " + lnum, ex);
}
}
} else {
sb.append(s.charAt(i));
}
}
return sb.toString();
}
/**
* Substitute any referenced env properties with their values. Look for @ENV{'foo'} and replace foo with
* System.getProperty("foo") or System.getenv("foo") in that order.
*
* @param str the string to process
* @param filename the name of the file we are in for error reporting
* @param lnum the current line number for error reporting
* @return string with env values replaced
*/
protected String substituteEnvProps(final String str, final String filename, final int lnum) throws IOException {
int lastPos = -1;
int thisPos = 0;
int count = 0;
logger.debug("{}{} style substitution is deprecated. Please just use {}yourvalue{}", ENVSTART, ENVSTOP, VSTART, VEND);
String currentStr = str;
while ((thisPos = currentStr.indexOf(ENVSTART, thisPos)) > lastPos) {
final int start = thisPos + ENVSTART.length();
final int stop = currentStr.indexOf(ENVSTOP, thisPos);
count++;
// Pull out the env name they specified
if (stop > start) {
final String envName = currentStr.substring(start, stop);
String envVal = System.getProperty(envName);
if (envVal == null) {
envVal = System.getenv(envName);
}
// We got a replacement, do the subst
if (envVal != null) {
currentStr = currentStr.substring(0, thisPos) + // before
envVal + // replacement value
currentStr.substring(stop + ENVSTOP.length()); // tail
logger.debug("Replaced {} with {} at {}: {}", envName, envVal, filename, lnum);
} else {
logger.debug("No env value for {} at {}: {}", envName, filename, lnum);
}
} else {
throw new IOException("Runaway string on line ->" + currentStr + "<- at " + filename + ": " + lnum);
}
lastPos = thisPos;
}
logger.debug("Found {} env vars to subst --> {}", count, currentStr);
return currentStr;
}
/**
* Create a directory as specified by the config driver
*/
protected boolean createDirectory(final String sval) {
final String fixedSval = sval.replace('\\', '/');
logger.debug("Trying to create dir {}", fixedSval);
final File d = new File(fixedSval);
if (!d.exists() && !d.mkdirs()) {
logger.debug("Failed to create directory {}", fixedSval);
return false;
}
return true;
}
/**
* Create a file as specified by the config driver
*/
protected boolean createFile(final String sval) {
final String fixedSval = sval.replace('\\', '/');
logger.debug("Trying to create file {}", fixedSval);
final File d = new File(fixedSval);
FileWriter newFile = null;
if (!d.exists()) {
try {
// Ensure the directory exists to hold the file
final File parent = new File(new File(d.getCanonicalPath()).getParent());
if (!parent.exists() && !createDirectory(parent.toString())) {
logger.debug("Failed to create parent directory for {}", fixedSval);
return false;
}
// Create the file in the directory
newFile = new FileWriter(d);
} catch (IOException e) {
logger.debug("Failed to create file {}", fixedSval, e);
return false;
} finally {
if (newFile != null) {
try {
newFile.close();
} catch (IOException ioe) {
logger.debug("Error closing file", ioe);
}
}
}
}
return true;
}
/**
* Get the names of all entries for this config This set is not backed by the configuration and any changes to it are
* not reflected in the configuration.
*/
@Override
public Set<String> entryKeys() {
final Set<String> set = new HashSet<>();
for (final ConfigEntry curEntry : this.serviceParameters) {
set.add(curEntry.getKey());
}
return set;
}
/**
* Get all the entries for this config This is a copy and changes to it are not reflected in the configuration
*/
@Override
public List<ConfigEntry> getEntries() {
return new ArrayList<>(this.serviceParameters);
}
/**
* Remove entries, those with operators of '!=' are stored and can be retrieved for replay during merge. This method is
* not part of the Configurator interface.
*/
protected List<ConfigEntry> getRemoveEntries() {
return new ArrayList<>(this.removeParameters);
}
/**
* Add an entry to this config
*
* @param key the name of the entry
* @param value the value
* @return the new entry or null if it fails
*/
@Override
public ConfigEntry addEntry(final String key, final String value) {
ConfigEntry entry = null;
try {
entry = handleNewEntry(key, value, "=", "<user>", 1, false);
} catch (IOException ex) {
logger.error("Could not add entry for {}", key, ex);
}
return entry;
}
/**
* Add a list of entries for the same key
*
* @param key the name of the entry
* @param values the values
* @return the new entries or null if it fails
*/
@Override
public List<ConfigEntry> addEntries(final String key, final List<String> values) {
final List<ConfigEntry> list = new ArrayList<>();
try {
int i = 1;
for (final String value : values) {
final ConfigEntry entry = handleNewEntry(key, value, "=", "<user>", i++, false);
list.add(entry);
}
} catch (IOException ex) {
logger.error("Error adding entries for {}", key, ex);
}
return list;
}
/**
* Remove all entries by the given name
*
* @param key the name of the entry or entries
* @param value the value
*/
@Override
public void removeEntry(final String key, final String value) {
try {
handleNewEntry(key, value, "!=", "<user>", 1, false);
} catch (IOException ex) {
logger.warn("Cannot remove entry", ex);
}
}
/**
* Remove an entry from the list of parameters matching the ConfigEntry argument passed in.
*
* @param anEntry the entry to remove
*/
public void removeEntry(final ConfigEntry anEntry) {
// NB: enhanced for loop does not support remove
for (final Iterator<ConfigEntry> i = this.serviceParameters.iterator(); i.hasNext();) {
final ConfigEntry curEntry = i.next();
if (anEntry.getKey().equals(curEntry.getKey())
&& ((anEntry.getValue() == null && curEntry.getValue() == null) || (anEntry.getValue() != null && anEntry.getValue().equals(
curEntry.getValue())))) {
logger.debug("Removing {} = {}", curEntry.getKey(), curEntry.getValue());
i.remove();
}
}
}
/**
* Return a list containing all the parameter values matching the key argument passed in.
*
* @param theParameter the key to match
* @param defaultString value for list when no matches are found
* @return the list with all matching entries or the default value supplied
*/
@Override
public List<String> findEntries(final String theParameter, final String defaultString) {
final List<String> result = findEntries(theParameter);
if (result.isEmpty()) {
result.add(defaultString);
}
return result;
}
/**
* Return a list containing all the parameter values matching the key argument passed in
*
* @param theParameter the key to match
* @return list with all matching entries, or empty list if none
*/
@Override
public List<String> findEntries(final String theParameter) {
final List<String> matchingEntries = new ArrayList<>();
for (final ConfigEntry curEntry : this.serviceParameters) {
if (theParameter.equals(curEntry.getKey())) {
matchingEntries.add(curEntry.getValue());
}
}
return matchingEntries;
}
/**
* Remove all entries from the list of parameters matching the String argument passed in.
*
* @param theParameter key name to match, all matching will be removed
*/
public void removeAllEntries(final String theParameter) {
// NB: enhanced for loop does not support remove
for (final Iterator<ConfigEntry> i = this.serviceParameters.iterator(); i.hasNext();) {
final ConfigEntry curEntry = i.next();
if (theParameter.equals(curEntry.getKey())) {
logger.debug("Removing {} = {}", curEntry.getKey(), curEntry.getValue());
i.remove();
}
}
}
/**
* Return a set with all parameter values as members
*
* @param theParameter key value to match
* @return set of all entries found or empty set if none
*/
@Override
public Set<String> findEntriesAsSet(final String theParameter) {
final Set<String> matchingEntries = new HashSet<>();
for (final ConfigEntry curEntry : this.serviceParameters) {
if (theParameter.equals(curEntry.getKey())) {
matchingEntries.add(curEntry.getValue());
}
}
return matchingEntries;
}
/**
* Find entries beginning with the specified string
*
* @param theParameter key to match with a startsWith
* @return list of entries matching specified value or empty list if none
*/
@Override
public List<ConfigEntry> findStringMatchEntries(final String theParameter) {
final List<ConfigEntry> matchingEntries = new ArrayList<>();
for (final ConfigEntry curEntry : this.serviceParameters) {
if (curEntry.getKey().startsWith(theParameter)) {
matchingEntries.add(curEntry);
}
}
return matchingEntries;
}
/**
* Find entries beginning with the specified string and put them into a list with the specified part of the name
* stripped off like #findStringMatchMap
*
* @param theParameter key to match with a startsWith
* @return list of ConfigEntry
*/
@Override
public List<ConfigEntry> findStringMatchList(final String theParameter) {
final List<ConfigEntry> list = findStringMatchEntries(theParameter);
for (final ConfigEntry entry : list) {
entry.setKey(entry.getKey().substring(theParameter.length()));
}
return list;
}
/**
* Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
* value of the config line as the value of the hash
*
* @param theParameter the key to look for in the config file
*/
@Override
public Map<String, String> findStringMatchMap(final String theParameter) {
return findStringMatchMap(theParameter, false);
}
/**
* Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
* value of the config line as the value of the hash
*
* <pre>
* {@code
* Example config entries
* FOO_ONE: AAA
* FOO_TWO: BBB
* Calling findStringMatchMap("FOO_",true)
* will yield a map with
* ONE -> AAA
* TWO -> BBB
* }
* </pre>
*
* @param theParameter the key to look for in the config file
* @param preserveCase if false all keys will be upcased
* @return map where key is remainder after match and value is the config value, or an empty map if none found
*/
@Override
public Map<String, String> findStringMatchMap(final String theParameter, final boolean preserveCase) {
return findStringMatchMap(theParameter, preserveCase, false);
}
/**
* Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
* value of the config line as the value of the hash
*
* <pre>
* {@code
* Example config entries
* FOO_ONE: AAA
* FOO_TWO: BBB
* Calling findStringMatchMap("FOO_",true)
* will yield a map with
* ONE -> AAA
* TWO -> BBB
* }
* </pre>
*
* @param theParameter the key to look for in the config file
* @param preserveCase if false all keys will be upcased
* @param preserveOrder if true key ordering is preserved
* @return map where key is remainder after match and value is the config value, or an empty map if none found
*/
@Override
public Map<String, String> findStringMatchMap(@Nullable final String theParameter, final boolean preserveCase, final boolean preserveOrder) {
if (theParameter == null) {
return Collections.emptyMap();
}
final Map<String, String> theHash = preserveOrder ? new LinkedHashMap<>() : new HashMap<>();
final List<ConfigEntry> parameters = this.findStringMatchEntries(theParameter);
for (final ConfigEntry el : parameters) {
String key = el.getKey();
key = key.substring(theParameter.length());
if (!preserveCase) {
key = key.toUpperCase(Locale.getDefault());
}
theHash.put(key, el.getValue());
}
return theHash;
}
/**
* Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
* value of the config line as the value of the hash Multiple values for the same hash are allowed and returned as a
* Set.
*
* <pre>
* {@code
* Example config entries
* FOO_ONE: AAA
* FOO_TWO: BBB
* FOO_TWO: CCC
* Calling findStringMatchMap("FOO_",true)
* will yield a map with Sets
* ONE -> {AAA}
* TWO -> {BBB,CCC}
* }
* </pre>
*
* @param param the key to look for in the config file
* @return map where key is remainder after match and value is a Set of all found config values, or an empty map if none
* found
*/
@Override
public Map<String, Set<String>> findStringMatchMultiMap(@Nullable final String param) {
if (param == null) {
return Map.of();
}
final Map<String, Set<String>> theHash = new HashMap<>();
final List<ConfigEntry> parameters = this.findStringMatchEntries(param);
for (final ConfigEntry el : parameters) {
final String key = el.getKey().substring(param.length()).toUpperCase(Locale.getDefault());
if (theHash.containsKey(key)) {
theHash.get(key).add(el.getValue());
} else {
final Set<String> values = new HashSet<>();
values.add(el.getValue());
theHash.put(key, values);
}
}
return theHash;
}
/**
* Return the first string entry matching the key parameter
*
* @param theParameter key to match
* @return the first matching value
* @throws IllegalArgumentException if no non-blank value is found
*/
@Override
public String findRequiredStringEntry(final String theParameter) {
String value = findStringEntry(theParameter, null);
Validate.notBlank(value, "Missing required parameter [%s]", theParameter);
return value;
}
/**
* Return the first string entry matching the key parameter or the default if no match is found
*
* @param theParameter the key to match
* @param dflt string to use when no matches are found
* @return the first matching entry of the default if none found
*/
@Override
public String findStringEntry(final String theParameter, @Nullable final String dflt) {
final List<String> matchingEntries = findEntries(theParameter);
for (final String entry : matchingEntries) {
if (entry != null) {
return entry;
}
}
return dflt;
}
/**
* Return the first string entry matching the key parameter or null if no match is found
*
* @param theParameter key to match
* @return the first matching value or null if none
*/
@Override
public String findStringEntry(final String theParameter) {
return findStringEntry(theParameter, null);
}
/**
* Return the last (newest) string entry matching the key parameter or an empty string if no match is found
*
* @param theParameter the key to match
* @return the last matching value or empty string if none found
*/
@Override
public String findLastStringEntry(final String theParameter) {
String result = "";
for (final ConfigEntry curEntry : this.serviceParameters) {
if (theParameter.equals(curEntry.getKey())) {
result = curEntry.getValue();
}
}
return result;
}
/**
* Return a long from a string entry representing either an int, a long, a double, with or without a final letter
* designation such as "m" or "M" for megabytes, "g" or "G" for gigabytes, etc. Legal designations are bBkKmMgGTt or
* just a number.
*
* @param theParameter the config entry name
* @param dflt the default value when nothing found in config
* @return the long value of the size parameter
*/
@Override
public long findSizeEntry(final String theParameter, final long dflt) {
final List<String> matchingEntries = findEntries(theParameter);
if (!matchingEntries.isEmpty()) {
long val = dflt;
final String s = matchingEntries.get(0);
final char c = Character.toUpperCase(s.charAt(s.length() - 1));
final String ss = s.substring(0, s.length() - 1);
boolean broken = false;
switch (c) {
case 'T':
val = Long.parseLong(ss) * 1024 * 1024 * 1024 * 1024;
break;
case 'G':
val = Long.parseLong(ss) * 1024 * 1024 * 1024;
break;
case 'M':
val = Long.parseLong(ss) * 1024 * 1024;
break;
case 'K':
val = Long.parseLong(ss) * 1024;
break;
case 'B':
val = Long.parseLong(ss);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
val = Long.parseLong(s);
break;
default:
broken = true;
}
if (!broken) {
return val;
}
return dflt;
}
return dflt;
}
/**
* Return the string canonical file name of a matching entry
*
* @param theParameter the key to match
* @param dflt the string to use as a default when no matches are found
* @return the first matching value run through File.getCanonicalPath
*/
@Override
public String findCanonicalFileNameEntry(final String theParameter, final String dflt) {
final String fn = findStringEntry(theParameter, dflt);
if (fn != null && fn.length() > 0) {
try {
return new File(fn).getCanonicalPath();
} catch (IOException ex) {
logger.error("Cannot compute canonical path on {}", fn, ex);
}
}
return fn;
}
/**
* Return an int of the first entry matching the key parameter or the default if no match is found
*
* @param theParameter the key to match
* @param dflt the int to use when no matches are found
* @return the first matching value or the default when none found
*/
@Override
public int findIntEntry(final String theParameter, final int dflt) {
final List<String> matchingEntries = findEntries(theParameter);
if (!matchingEntries.isEmpty()) {
try {
return Integer.parseInt(matchingEntries.get(0));
} catch (NumberFormatException e) {
logger.warn("{} is non-numeric returning default value: {}", theParameter, dflt);
}
}
return dflt;
}
/**
* Return a long of the first entry matching the key parameter or the default if no match is found
*
* @param theParameter the key to match
* @param dflt the value to use when no matches are found
* @return the first matching value or the default when none found
*/
@Override
public long findLongEntry(final String theParameter, final long dflt) {
final List<String> matchingEntries = findEntries(theParameter);
if (!matchingEntries.isEmpty()) {
try {
return Long.parseLong(matchingEntries.get(0));
} catch (NumberFormatException e) {
logger.warn("{} is non-numeric returning default value: {}", theParameter, dflt);
}
}
return dflt;
}
/**
* Return a double of the first entry matching the key parameter or the default if no match is found
*
* @param theParameter the key to match
* @param dflt the value to use when no matches are found
* @return the first matching value or the default when none found
*/
@Override
public double findDoubleEntry(final String theParameter, final double dflt) {
final List<String> matchingEntries = findEntries(theParameter);
if (!matchingEntries.isEmpty()) {
try {
return Double.parseDouble(matchingEntries.get(0));
} catch (NumberFormatException e) {
logger.warn("{} is non-numeric returning default value: {}", theParameter, dflt);
}
}
return dflt;
}
/**
* Return boolean of the first entry matching the key parameter or the default if no match is found
*
* @param theParameter the key to match
* @param dflt the value to use when no matches are found
* @return the first matching value or the default when none found
*/
@Override
public boolean findBooleanEntry(final String theParameter, final boolean dflt) {
final List<String> matchingEntries = findEntries(theParameter);
if (!matchingEntries.isEmpty()) {
String el = matchingEntries.get(0);
el = el.toUpperCase(Locale.getDefault());
if (el.startsWith("F")) {
return false;
} else if (el.startsWith("T")) {
return true;
}
}
return dflt;
}
/**
* Return boolean of the first entry matching the key parameter or the default if no match is found
*
* @param theParameter the key to match
* @param dflt the value to use when no matches are found
* @return the first matching value or the default when none found
*/
@Override
public boolean findBooleanEntry(final String theParameter, final String dflt) {
return findBooleanEntry(theParameter, Boolean.parseBoolean(dflt));
}
/**
* Get the value of a parameter that is purported to be numeric
*
* @param name the name of the parameter
*/
protected int getNumericParameter(final String name) {
final String val = this.values.get(name);
int i = -1;
if (val != null) {
try {
i = Integer.parseInt(val);
} catch (NumberFormatException ex) {
logger.warn("{} is non-numeric: {}", name, val);
}
}
return i;
}
public boolean debug() {
return "TRUE".equalsIgnoreCase(this.values.get("DEBUG"));
}
/**
* Merge in a new configuration set with this one. New things are supposed to override older things in the sense of
* findStringEntry which only picks the top of the list, the new things should get added to the top. If the merged in
* Configurator contains remove entries (operator of '!=') then it only applies to entries in this instance, not in
* "other". This is slightly different than when a config is read in directly, but without that there would be no way to
* remove entries from a super-config and continue to supply entries here and still be able to use the wildcard remove
* (value of '*') The order in the merged config file is important. Any '!= "*"' operations must precede the new value
* being supplied since normal remove operations take place in each config before the merge.
*
* @param other the new entries to merge in
*/
@Override
public void merge(final Configurator other) throws IOException {
int i = 1;
// First handle the remove entries from "other"
if (other instanceof ServiceConfigGuide) {
for (final ConfigEntry entry : ((ServiceConfigGuide) other).getRemoveEntries()) {
handleNewEntry(entry.getKey(), entry.getValue(), "!=", "<merge>", i++, true);
}
}
// Add in new entries from "other" at the top of the list
for (final ConfigEntry entry : other.getEntries()) {
handleNewEntry(entry.getKey(), entry.getValue(), "=", "<merge>", i++, true);
}
}
/**
* Public main used to verify config file construction off-line
*/
public static void main(final String[] args) {
if (args.length < 1) {
logger.error("usage: java ServiceConfigGuide configfile");
return;
}
for (String arg : args) {
try {
final ServiceConfigGuide sc = new ServiceConfigGuide(arg);
logger.info("Config File:{} ", arg);
for (int i = 0; i < sc.serviceParameters.size(); i++) {
final ConfigEntry c = sc.serviceParameters.get(i);
logger.info("{}: {}", c.getKey(), c.getValue());
}
logger.info("---");
} catch (IOException e) {
logger.info("Cannot process {}:{}", arg, e.getLocalizedMessage());
}
}
}
}