ConfigUtil.java
package emissary.config;
import emissary.core.EmissaryException;
import emissary.util.io.ResourceReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Nullable;
/**
* This configuration utility collection helps to find configuration for various classes and objects. It responds to
* -Demissary.config.dir=value and treats it as a local directory in which to find configuration files. Failing to find
* a local file, many of these methods will try to retrieve config data from a resource stream (i.e. the classpath). The
* package name to use can be prefixed with some package of your choosing by setting -Demissary.config.pkg=value.
*/
@SuppressWarnings("NonFinalStaticField")
public class ConfigUtil {
/** Our logger */
protected static final Logger logger = LoggerFactory.getLogger(ConfigUtil.class);
/** Constant string for files that end with {@value} */
public static final String CONFIG_FILE_ENDING = ResourceReader.CONFIG_SUFFIX;
/** Constant string for files that end with {@value} */
public static final String PROP_FILE_ENDING = ResourceReader.PROP_SUFFIX;
/** Constant string for files that end with {@value} */
public static final String XML_FILE_ENDING = ResourceReader.XML_SUFFIX;
/** Constant string for files that end with {@value} */
public static final String JS_FILE_ENDING = ResourceReader.JS_SUFFIX;
/** Constant string for inventory files name prefix */
public static final String INVENTORY_FILE_PREFIX = "emissary.admin.ClassNameInventory";
/**
* This property specifies the config override directory. When present, we look here first for config info. If not
* present, we try and load from the classpath as a resource. The property is set with -D{@value}
*/
public static final String CONFIG_DIR_PROPERTY = "emissary.config.dir";
/**
* This property specifies the config class package prefix specifier Use -D{@value} to change the class value
*/
public static final String CONFIG_PKG_PROPERTY = "emissary.config.pkg";
/**
* This property specified the install/config flavor to run. This will opt-import and merge config files special to the
* desired flavor allowing multiple configurations to be kept in the same configuration package
*/
public static final String CONFIG_FLAVOR_PROPERTY = "emissary.config.flavor";
/**
* This property is the BIN_DIR, the root directory of all binary scripts and executables used by places.
*/
public static final String CONFIG_BIN_PROPERTY = "emissary.bin.dir";
/**
* This property is the OUTPUT_ROOT, the root directory of local output
*/
public static final String CONFIG_OUTPUT_ROOT_PROPERTY = "emissary.output.root";
public static final String PROJECT_BASE_ENV = "PROJECT_BASE";
/** The package name where config stuff may be found */
@Nullable
private static String configPkg = null;
/** The directory where config stuff may be found */
@Nullable
private static String configDirProperty = null;
/** The directories where config stuff may be found, searched in order */
@Nullable
private static List<String> configDirs = null;
/** The project root directory */
@Nullable
private static String projectRoot = null;
/** The output root */
@Nullable
private static String outputRoot = null;
/** The bin dir */
@Nullable
private static String binDir = null;
/**
* The configuration flavor, allows multiple layers of config stuff to be stored together in one package or directory,
* sort of like a mini single inheritance model. It is a comma separated, ordered list of subtypes to try to merge into
* the current config.
*/
@Nullable
private static String configFlavors = null;
/*
* Perform initialization
*/
static {
try {
initialize();
} catch (EmissaryException e) {
logger.error("Error in ConfigUtil static", e);
}
}
/**
* Remove the trailing slash from a string, if present.
*
* @param in The input string.
* @return If {@code in} ends with a slash, this returns a string with the same content except without the trailing
* slash. Otherwise, this returns a string with the same content as {@code in}.
*/
private static String removeTrailingSlash(final String in) {
if (in.endsWith("/")) {
return in.substring(0, in.length() - 1);
} else {
return in;
}
}
/**
* Initialize system properties for this class, but make it so that test cases (and other interested parties) can reset
* system properties and re-call this method when they need to.
*/
public static void initialize() throws EmissaryException {
// throws NPE if not defined
projectRoot = System.getenv(ConfigUtil.PROJECT_BASE_ENV);
configDirProperty = System.getProperty(CONFIG_DIR_PROPERTY, "").replace('\\', '/');
if (configDirProperty.equals("")) {
logger.error("You must set -Demissary.config.dir, it was empty");
throw new EmissaryException("-Demissary.config.dir was not set");
} else if (configDirProperty.equals("/tmp")) {
logger.error("You probably don't want to use /tmp as the emissary.config.dir");
throw new EmissaryException("-Demissary.config.dir was /tmp");
}
logger.debug("Configured configDirProperty {}", configDirProperty);
outputRoot = System.getProperty(CONFIG_OUTPUT_ROOT_PROPERTY);
binDir = System.getProperty(CONFIG_BIN_PROPERTY);
configFlavors = System.getProperty(CONFIG_FLAVOR_PROPERTY, null);
configPkg = System.getProperty(CONFIG_PKG_PROPERTY, configPkg);
configDirs = new ArrayList<>();
final String[] dirs = configDirProperty.split(",");
for (final String dir : dirs) {
final String dirNoTrailingSlash = removeTrailingSlash(dir);
// only add directories that exist
if (Files.exists(Paths.get(dirNoTrailingSlash))) {
configDirs.add(dirNoTrailingSlash);
} else {
logger.warn("Directory configured but didn't exist: {}", dirNoTrailingSlash);
}
}
}
/**
* Give the project root directory
*/
public static String projectRootDirectory() {
return projectRoot;
}
/**
* Return project base
*/
public static String getProjectBase() {
return projectRoot;
}
/**
* Return output root
*/
public static String getOutputRoot() {
return outputRoot;
}
/**
* Return bin dir
*/
public static String getBinDir() {
return binDir;
}
/**
* Get a List of config directories.
*/
public static List<String> getConfigDirs() {
if (configDirs == null) {
throw new IllegalStateException("No config directory specified");
}
return configDirs;
}
/**
* Get the first config dir.
* <p>
* If there is only one config dir, it just returned
*/
public static String getFirstConfigDir() {
return getConfigDirs().get(0);
}
/**
* Get the named config file from the config directory
*
* @param file the config file name to get, path part ignored and replace with normal config dir if not absolute
*/
public static String getConfigFile(final String file) {
if (file.startsWith("/")) {
return file;
}
if (getConfigDirs().size() > 1) {
final List<String> candidates = new ArrayList<>();
for (final String dir : getConfigDirs()) {
final String fname = dir + "/" + file;
if (Files.exists(Paths.get(fname))) {
candidates.add(fname);
}
}
if (candidates.isEmpty()) {
logger.debug("No file found in any of the configured directories: {}", file);
return getFirstConfigDir() + "/" + file;
} else if (candidates.size() > 1) {
logger.error("Multiple files found in the configured directories: {}, returning the first.", file);
}
logger.trace("Returning {}", candidates.get(0));
return candidates.get(0);
} else { // much more efficient to do this, no file check each time
final String cfgFile = getFirstConfigDir() + "/" + file;
logger.trace("Returning {}", cfgFile);
return cfgFile;
}
}
/**
* Get the named config file from the named config path
*
* @param path the config path to use
* @param file the file to get,
*/
public static String getConfigFile(final String path, final String file) {
if (file.startsWith("/")) {
return file;
}
return removeTrailingSlash(path) + "/" + file;
}
/**
* Get the config file for the specified name without package naming
*/
private static String getOldStyleConfigFile(final String name) {
String file = name;
// Chomp the file suffix
if (file.endsWith(CONFIG_FILE_ENDING)) {
file = file.substring(0, file.length() - CONFIG_FILE_ENDING.length());
}
if (file.contains("$")) {
file = file.replace('$', '_');
}
if (file.contains(".")) {
file = file.substring(file.lastIndexOf(".") + 1);
}
return getConfigFile(file + CONFIG_FILE_ENDING);
}
/**
* Get the ServiceConfigGuide for the named class
*/
public static Configurator getConfigInfo(final Class<?> c) throws IOException {
final String name = c.getName() + CONFIG_FILE_ENDING;
logger.debug("Loading config for (class) {}", name);
return getConfigInfo(getConfigStream(name), name);
}
/**
* Get configurator by trying the list of preferences in order and using the first one that is found. Sometimes and
* array signature can be easier to use from a static context.
*
* @param preferences array of string names to try
* @return the configurator
* @throws IOException if none of the prefs can be found
* @deprecated use {@link #getConfigInfo(List)}
*/
@Deprecated
@SuppressWarnings("AvoidObjectArrays")
public static Configurator getConfigInfo(final String[] preferences) throws IOException {
return getConfigInfo(Arrays.asList(preferences));
}
/**
* Get configurator by trying the list of preferences in order and using the first one that is found.
*
* @param preferences string names of configs to try
* @return the configurator
* @throws IOException if none of the prefs can be found
*/
public static Configurator getConfigInfo(final List<String> preferences) throws IOException {
Configurator c;
for (final String s : preferences) {
try {
c = getConfigInfo(s);
return c;
} catch (IOException ex) {
String exception = ex.getMessage();
if (exception.contains("IMPORT_FILE")) {
exception = exception.replace("<none>", s);
logger.debug("IMPORT_FILE not found in {}", s);
throw new IOException(exception);
} else {
logger.debug("Preference {} not found", s);
}
}
}
throw new IOException("None of the " + preferences.size() + " preferences could be found: " + preferences);
}
/**
* Get Configurator for named object
*
* @param name object name to get config info for
*/
public static Configurator getConfigInfo(final String name) throws IOException {
logger.debug("Loading config for (string) {}", name);
return getConfigInfo(getConfigStream(name), name);
}
/**
* Get the configurator on the specified stream
*
* @param is the stream of data
* @return configurator object
*/
public static Configurator getConfigInfo(final InputStream is) throws IOException {
return getConfigInfo(is, "<none>");
}
/**
* Get the configurator on the specified stream
*
* @param is the stream of data
* @param name the name of the stream for debugging
* @return configurator object
*/
public static Configurator getConfigInfo(final InputStream is, final String name) throws IOException {
final ServiceConfigGuide scg = new ServiceConfigGuide(is, name);
final String[] flavoredNames = addFlavors(name);
for (final String flavoredName : flavoredNames) {
try {
logger.debug("Attempting flavor merge on {}", flavoredName);
final Configurator flavoredConfig = getConfigInfo(flavoredName);
scg.merge(flavoredConfig);
logger.debug("Merged config with {}", flavoredName);
} catch (IOException iox) {
logger.debug("Unable to opt import flavor config {}", flavoredName);
}
}
return scg;
}
/**
* Get the last modified time of a config file resource
*
* @param name the name of the config resource
* @return the lastModified timestamp or -1L if it doesn't exist
*/
public static long getConfigFileLastModified(final String name) {
final String sname = getConfigFile(name);
final File f = new File(sname);
if (f.exists() && f.canRead()) {
return f.lastModified();
}
return -1L;
}
/**
* Get input stream of config data for name
*
* @param name the name of the stream to look for
* @return an InputStream caller must close
*/
public static InputStream getConfigStream(final String name) throws IOException {
// Try the new style override name first ( with package )
String sname = getConfigFile(name);
File f = new File(sname);
if (f.exists() && f.canRead()) {
logger.debug("Found config data as file {}", f.getPath());
return Files.newInputStream(f.toPath());
}
logger.debug("No file config found using new style {}", f.getName());
// Try the classpath loader
final List<String> reznames = toResourceName(name);
for (final String rezname : reznames) {
final URL url = new ResourceReader().getResource(rezname);
if (url != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found config data as resource {}", url.toExternalForm());
}
try {
return url.openStream();
} catch (IOException ex) {
logger.warn("IOException opening stream for resource for {}", rezname);
}
}
logger.debug("No config data as resource for {}", rezname);
}
logger.trace("No stream config found using {}", reznames);
// Try the old style override name ( no package )
sname = getOldStyleConfigFile(name);
f = new File(sname);
if (f.exists() && f.canRead()) {
logger.debug("Found config data as file old style {}", f.getPath());
return Files.newInputStream(f.toPath());
}
logger.debug("No file config found using old style {}", f.getName());
throw new IOException("No config stream available for " + name);
}
/**
* Convert a name to work on the classpath as a resource
*
* @param name the name to convert
* @return name with optional config package prepended and s#.#/#g
*/
private static List<String> toResourceName(final String name) {
String r = name.replace('.', '/');
if (r.toUpperCase(Locale.getDefault()).endsWith("/CFG")) {
r = r.substring(0, r.length() - CONFIG_FILE_ENDING.length()) + CONFIG_FILE_ENDING;
} else if (r.toUpperCase(Locale.getDefault()).endsWith("/XML")) {
r = r.substring(0, r.length() - XML_FILE_ENDING.length()) + XML_FILE_ENDING;
} else if (r.toUpperCase(Locale.getDefault()).endsWith("/PROPERTIES")) {
r = r.substring(0, r.length() - PROP_FILE_ENDING.length()) + PROP_FILE_ENDING;
} else if (r.toUpperCase(Locale.getDefault()).endsWith("/JS")) {
r = r.substring(0, r.length() - JS_FILE_ENDING.length()) + JS_FILE_ENDING;
}
final List<String> prefs = new ArrayList<>();
if (configPkg != null) {
prefs.add(configPkg.replace('.', '/') + "/" + r);
}
prefs.add(r);
return prefs;
}
/**
* Get the named resource as a Properties object
*/
public static Properties getPropertyInfo(final String name) throws IOException {
final Properties props = new Properties();
try (InputStream is = getPropertyStream(name, new File(getConfigFile(name)))) {
if (is != null) {
props.load(is);
}
}
return props;
}
protected static InputStream getPropertyStream(final String name, final File f) throws IOException {
InputStream is = null;
if (f.exists() && f.canRead()) {
is = Files.newInputStream(f.toPath());
} else {
final List<String> cnameprefs = toResourceName(name);
for (final String cname : cnameprefs) {
is = new ResourceReader().getResourceAsStream(cname);
if (is != null) {
break;
}
}
}
return is;
}
/**
* Return the in-use config flavors
*/
@Nullable
public static List<String> getFlavors() {
if (configFlavors == null) {
return null;
}
return Collections.singletonList(configFlavors);
}
/**
* Add the current config Flavor to the name of the resource passed in. E.g. emissary.pkg.Foo.cfg =>
* emissary.pkg.Foo-${FLAVOR}.cfg
*
* @param name the base resource or config name
* @return the name with the flavor in it
*/
@SuppressWarnings("AvoidObjectArrays")
public static String[] addFlavors(final String name) {
if (configFlavors == null || configFlavors.length() == 0) {
return new String[0];
}
final int pos = name.lastIndexOf('.');
final String base = pos > -1 ? name.substring(0, pos) : name;
final String suffix = pos > -1 ? name.substring(pos) : "";
final String[] flavor = configFlavors.split(",");
final String[] flavoredNames = new String[flavor.length];
for (int i = 0; i < flavor.length; i++) {
flavoredNames[i] = base + "-" + flavor[i] + suffix;
}
return flavoredNames;
}
/**
* Get the config data for the config file named
*
* @param f the named config item (path not necessary, but will be used if present)
* @return Input Stream, caller must close.
*/
public static InputStream getConfigData(final String f) throws IOException {
logger.debug("Request for config data from {}", f);
// Add config.dir part if not already absolute
final String filename = f.startsWith("/") ? f : getConfigFile(f);
return Files.newInputStream(Paths.get(filename));
}
/**
* Gets all ClassNameInventory from configured file.
* <p>
* For a single entry in 'emissary.config.dir' or comma separated list of config directories, every file that starts
* with 'emissary.admin.ClassNameInventory' will be combined into a Configurator. This means files like
* 'emissary.admin.ClassNameInventory.cfg', 'emissary.admin.ClassNameInventory-module1.cfg' and
* 'emissary.admin.ClassNameInventory-whatever.cfg' will be used. The concept of flavoring no longer applies to the
* ClassNameInventory.
*
* @return Configurator with all emissary.admin.ClassNameInventory
* @throws IOException If there is some I/O problem.
* @throws EmissaryException If no config files are found.
*/
public static Configurator getClassNameInventory() throws IOException, EmissaryException {
final List<File> classNameInventory = new ArrayList<>();
for (final String dir : getConfigDirs()) {
final File[] files = new File(dir).listFiles((dir1, name) -> name.startsWith(INVENTORY_FILE_PREFIX) && name.endsWith(CONFIG_FILE_ENDING));
// sort the files, to put emissary.admin.ClassNameInventory.cfg before emissary.admin.ClassNameInventory-blah.cfg
if (files != null) {
Arrays.sort(files);
classNameInventory.addAll(Arrays.asList(files));
}
}
// check to make sure we have at least one
if (classNameInventory.isEmpty()) {
throw new EmissaryException(String.format("No %s%s files found. No places to start.", INVENTORY_FILE_PREFIX, CONFIG_FILE_ENDING));
}
ServiceConfigGuide scg = null;
for (final File f : classNameInventory) {
if (!f.exists() || !f.canRead()) {
logger.warn("Could not read ClassNameInventory from {}", f.getAbsolutePath());
} else {
logger.debug("Reading ClassNameInventory from {}", f.getAbsolutePath());
}
if (null != configFlavors) {
final String cfgFlavor = getFlavorsFromCfgFile(f);
if (configFlavors.equals(cfgFlavor) || Arrays.asList(configFlavors.split(",")).contains(cfgFlavor)) {
logger.warn("Config file {} appeared to be flavored with {}.", f.getName(), cfgFlavor);
}
}
if (scg == null) { // first one
scg = new ServiceConfigGuide(Files.newInputStream(f.toPath()), "ClassNameInventory");
} else {
final Set<String> existingKeys = scg.entryKeys();
final Configurator scgToMerge = new ServiceConfigGuide(Files.newInputStream(f.toPath()), "ClassNameInventory");
boolean noErrorsForFile = true;
for (final String key : scgToMerge.entryKeys()) {
if (existingKeys.contains(key)) {
logger.error("Tried to overwrite existing key from ClassNameInventory:{} in {}", key, f.getAbsolutePath());
noErrorsForFile = false;
// System.exit(43); // this is swallowed in JettyServer in jetty 6
}
}
// only merge if there are no errors
if (noErrorsForFile) {
scg.merge(scgToMerge);
}
}
}
return scg;
}
/**
* Gets the flavors as specified by the filename.
* <p>
* Returns the portion between the last - and .cfg in the file name
*
* @param f The file of interest.
* @return String with parsed flavor name(s)
*/
static String getFlavorsFromCfgFile(final File f) {
final String filename = f.getName();
if (!filename.endsWith(".cfg")) {
logger.warn("Not a cfg file: {}", filename);
return "";
}
final String[] parts = filename.split("-");
if (parts.length == 1) {
// no flavor
return "";
}
if (parts.length > 2) {
logger.warn("Filename {} had multiple - characters, using the last to determine the flavor", filename);
}
return parts[parts.length - 1].replaceAll(".cfg", "");
}
/** This class is not meant to be instantiated. */
private ConfigUtil() {}
/**
* Read a merged config setup and print the results
*/
public static void main(final String[] args) {
if (args.length < 1) {
logger.error("usage: java {} configfile", ConfigUtil.class.getName());
return;
}
for (String arg : args) {
try {
final Configurator config = ConfigUtil.getConfigInfo(arg);
logger.info("Config File: {}", arg);
for (final ConfigEntry c : config.getEntries()) {
logger.info("{}: {}", c.getKey(), c.getValue());
}
logger.info("---");
} catch (IOException e) {
logger.error("Cannot process {}: {}", arg, e.getLocalizedMessage());
}
}
}
}