All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.davidbracewell.config.Config Maven / Gradle / Ivy

There is a newer version: 0.5
Show newest version
/*
 * (c) 2005 David B. Bracewell
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.davidbracewell.config;

import com.davidbracewell.Language;
import com.davidbracewell.SystemInfo;
import com.davidbracewell.application.Application;
import com.davidbracewell.application.CommandLineApplication;
import com.davidbracewell.cli.CommandLineParser;
import com.davidbracewell.cli.NamedOption;
import com.davidbracewell.conversion.Cast;
import com.davidbracewell.conversion.Convert;
import com.davidbracewell.conversion.Val;
import com.davidbracewell.io.Resources;
import com.davidbracewell.io.resource.ClasspathResource;
import com.davidbracewell.io.resource.Resource;
import com.davidbracewell.logging.LogFormatter;
import com.davidbracewell.logging.LogManager;
import com.davidbracewell.logging.Logger;
import com.davidbracewell.parsing.ParseException;
import com.davidbracewell.reflection.BeanUtils;
import com.davidbracewell.reflection.ReflectionException;
import com.davidbracewell.scripting.ScriptEnvironment;
import com.davidbracewell.scripting.ScriptEnvironmentManager;
import com.davidbracewell.string.StringPredicates;
import com.davidbracewell.string.StringUtils;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import lombok.NonNull;

import javax.script.ScriptException;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.Predicate;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 

A complete configuration class that allows for inheritance, multiline inputs, variable interpolation, and * scripting.

  • ${var} will substitute the value of "var"

* * @author David B. Bracewell */ public final class Config implements Serializable { private static final String BEAN_PROPERTY = "@{"; private static final Pattern BEAN_SUBSTITUTION = Pattern.compile(Pattern.quote(BEAN_PROPERTY) + "(.+?)\\}"); private static final String DEFAULT_CONFIG_FILE_NAME = "default.conf"; private static final String SCRIPT_PROPERTY = "script["; private static final Pattern STRING_SUBSTITUTION = Pattern.compile("\\$\\{(.+?)\\}"); private static final String SYSTEM_PROPERTY = "system."; private static final long serialVersionUID = 6875819132224789761L; private static final Logger log = Logger.getLogger(Config.class); private static Resource localConfigDirectory = Resources.fromFile(SystemInfo.USER_HOME + "/config/"); private static ClassLoader defaultClassLoader = Config.class.getClassLoader(); private volatile static Config INSTANCE; private final Map properties = new ConcurrentHashMap<>(); private final Set loaded = new ConcurrentSkipListSet<>(); /** * Gets instance. * * @return the instance */ public static Config getInstance() { if (INSTANCE == null) { synchronized (Config.class) { INSTANCE = new Config(); } } return INSTANCE; } /** * Sets instance. * * @param config the config * @return the instance */ public static Config setInstance(Config config) { synchronized (Config.class) { INSTANCE = config; } return INSTANCE; } /** * Is config loaded. * * @param configResource the config resource * @return the boolean */ public static boolean isConfigLoaded(Resource configResource) { return getInstance().loaded.contains(configResource.path()); } private static Val getBean(String value) { List parts = StringUtils.split(value, ','); List beans = new ArrayList<>(); for (String beanName : parts) { Matcher m = BEAN_SUBSTITUTION.matcher(beanName); if (m.find()) { try { beans.add(BeanUtils.getNamedBean(m.group(1), Object.class)); } catch (ReflectionException e) { throw Throwables.propagate(e); } } else { beans.add(beanName); } } return beans.size() == 1 ? Val.of(beans.get(0)) : Val.of(beans); } /** * Clears all set properties */ public static void clear() { getInstance().loaded.clear(); getInstance().properties.clear(); } /** * Gets map. * * @param the type parameter * @param the type parameter * @param prefix the prefix * @param keyClass the key class * @param valueClass the value class * @return the map */ public static Map getMap(String prefix, Class keyClass, Class valueClass) { Map map = new HashMap<>(); getPropertiesMatching(StringPredicates.STARTS_WITH(prefix, true)).forEach(property -> { String k = property.substring(prefix.length() + 1); map.put(Convert.convert(k, keyClass), get(property).as(valueClass)); }); return map; } /** * Checks if a property is in the config or or set on the system. The property name is constructed as * propertyPrefix + . + propertyComponent[0] + . + propertyComponent[1] + ... * * @param propertyPrefix The prefix * @param propertyComponents The components. * @return True if the property is known, false if not. */ public static boolean hasProperty(String propertyPrefix, String... propertyComponents) { String propertyName = propertyPrefix; if (propertyComponents != null && propertyComponents.length > 0) { propertyName += "." + Joiner.on('.').join(propertyComponents); } return getInstance().properties.containsKey(propertyName) || System.getProperties().contains(propertyName); } /** * Checks if a property is in the config or or set on the system. The property name is constructed as * clazz.getName() + . + propertyComponent[0] + . + propertyComponent[1] + ... * * @param clazz The class with which the property is associated. * @param propertyComponents The components. * @return True if the property is known, false if not. */ public static boolean hasProperty(Class clazz, String... propertyComponents) { return hasProperty(clazz.getName(), propertyComponents); } /** *

Checks if a property is in the config or or set on the system. The property name is constructed as * clazz.getName() + . + propertyComponent[0] + . + propertyComponent[1] + ... + * (language.toString()|language.getCode().toLowerCase()) This will return true if the language specific * config * option is set or a default (i.e. no-language specified) version is set.

* * @param propertyPrefix The prefix * @param language The language we would like the config for * @param propertyComponents The components. * @return True if the property is known, false if not. */ public static boolean hasProperty(String propertyPrefix, Language language, String... propertyComponents) { return findKey(propertyPrefix, language, propertyComponents) != null; } /** * Closest key. * * @param propertyPrefix the property prefix * @param language the language * @param propertyComponents the property components * @return the string */ public static String closestKey(String propertyPrefix, Language language, String... propertyComponents) { return findKey(propertyPrefix, language, propertyComponents); } private static String findKey(String propertyPrefix, Language language, String... propertyComponents) { if (propertyComponents == null || propertyComponents.length == 0) { for (String key : new String[]{ propertyPrefix + "." + language, propertyPrefix + "." + language.toString().toLowerCase(), propertyPrefix + "." + language.getCode(), propertyPrefix + "." + language.getCode().toLowerCase(), propertyPrefix } ) { if (hasProperty(key)) { return key; } } return null; } String components = Joiner.on('.').join(propertyComponents); for (String key : new String[]{ propertyPrefix + "." + language + "." + components, propertyPrefix + "." + language.toString().toLowerCase() + "." + components, propertyPrefix + "." + language.getCode() + "." + components, propertyPrefix + "." + language.getCode().toLowerCase() + "." + components, propertyPrefix + "." + components + "." + language, propertyPrefix + "." + components + "." + language.toString().toLowerCase(), propertyPrefix + "." + components + "." + language.getCode(), propertyPrefix + "." + components + "." + language.getCode().toLowerCase(), propertyPrefix + "." + components } ) { if (hasProperty(key)) { return key; } } return null; } /** *

Checks if a property is in the config or or set on the system. The property name is constructed as * clazz.getName() + . + propertyComponent[0] + . + propertyComponent[1] + ... + * (language.toString()|language.getCode().toLowerCase()) This will return true if the language specific * config * option is set or a default (i.e. no-language specified) version is set.

* * @param clazz The class with which the property is associated. * @param language The language we would like the config for * @param propertyComponents The components. * @return True if the property is known, false if not. */ public static boolean hasProperty(Class clazz, Language language, String... propertyComponents) { return hasProperty(clazz.getName(), language, propertyComponents); } /** * Gets the value of a property for a given class and language (the language is optional) * * @param clazz The class * @param language The language * @param propertyComponents The components. * @return The value associated with clazz.propertyName.language exists or clazz.propertyName */ public static Val get(Class clazz, Language language, String... propertyComponents) { return get(clazz.getName(), language, propertyComponents); } /** * Gets the value of a property for a given class * * @param clazz The class * @param propertyComponents The components * @return The value associated with clazz.propertyName */ public static Val get(Class clazz, String... propertyComponents) { return get(clazz.getName(), propertyComponents); } /** * Gets the value of a property for a given class and language (the language is optional) * * @param propertyPrefix The prefix * @param language The language * @param propertyComponents The components. * @return The value associated with clazz.propertyName.language exists or clazz.propertyName */ public static Val get(String propertyPrefix, Language language, String... propertyComponents) { String key = findKey(propertyPrefix, language, propertyComponents); return key == null ? Val.of(null) : get(key); } /** *

Gets the value associated with a property. The property name is constructed as propertyPrefix + . + * propertyComponent[0] + . + propertyComponent[1] + ...

* * @param propertyPrefix The prefix * @param propertyComponents The components * @return The value for the property */ public static Val get(String propertyPrefix, String... propertyComponents) { String propertyName = propertyPrefix; if (propertyComponents != null && propertyComponents.length > 0) { propertyName += "." + Joiner.on('.').join(propertyComponents); } if (StringUtils.isNullOrBlank(propertyName)) { return new Val(null); } String value; if (getInstance().properties.containsKey(propertyName)) { value = getInstance().properties.get(propertyName); } else if (System.getProperty(propertyName) != null) { value = System.getProperty(propertyName); } else { value = System.getenv(propertyName); } if (value == null) { return new Val(null); } //resolve variables if (STRING_SUBSTITUTION.matcher(value).find()) { value = resolveVariables(getInstance().properties.get(propertyName)); } if (value == null) { return new Val(null); } //If the value is a script then process it if (value.startsWith(SCRIPT_PROPERTY)) { return Val.of(processScript(value)); } if (value.contains(BEAN_PROPERTY)) { return getBean(value); } return Val.of(value); } /** *

Gets a list of properties whose name is matched using the supplied StringMatcher. *

* * @param matcher The StringMatcher to use for matching property names. * @return A List of properties matched using the StringMatcher. */ public static List getPropertiesMatching(Predicate matcher) { if (matcher != null) { return getInstance().properties.keySet().parallelStream().filter(matcher).collect(Collectors.toList()); } return Collections.emptyList(); } /** * Load package config. * * @param packageName the package name */ public static void loadPackageConfig(@NonNull String packageName) { loadConfig(Resources.fromClasspath(packageName.replace('.', '/') + "/default.conf")); } /** * Loads a config file * * @param resource The config file */ public static void loadConfig(Resource resource) { if (resource != null && resource.exists()) { loadConfig(resource, ConfigSettingFunction.INSTANCE); } } /** * Loads a config file * * @param resource The config file * @param propertySetter the property setter */ static void loadConfig(Resource resource, ConfigPropertySetter propertySetter) { if (resource == null || !resource.exists()) { return; } if (resource.path() != null && getInstance().loaded.contains(resource.path())) { return; //Only load once! } try { new ConfigParser(resource, propertySetter).parse(); if (resource.path() != null) { getInstance().loaded.add(resource.path()); } } catch (ParseException | IOException e) { log.severe("ERROR LOADING CONFIG: '{0}'", resource.path()); throw Throwables.propagate(e); } } private static String getCallingClass() { // Auto-discover the package of the calling class. StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); for (int i = 1; i < stackTrace.length; i++) { StackTraceElement ste = stackTrace[i]; // ignore the config class if (!ste.getClassName().equals(Config.class.getName()) && !ste.getClassName().equals(CommandLineApplication.class.getName()) && !ste.getClassName().equals(Application.class.getName()) ) { return ste.getClassName(); } } return null; } /** * Sets all command line. * * @param args the args */ public static void setAllCommandLine(String[] args) { if (args != null) { CommandLineParser parser = new CommandLineParser(); parser.parse(args); parser.getSetEntries().forEach(entry -> { ConfigSettingFunction.INSTANCE.setProperty(entry.getKey(), entry.getValue(), "CommandLine"); }); } } /** *

Initializes the configuration.

Looks for a properties (programName.conf) in the classpath, the user * home directory, and in the run directory to load

The command line arguments are parsed and added to the * configuration.

* * @param programName the program name * @param args the command line arguments * @param parser the to use for parsing the arguments * @return Non config/option parameters from command line */ public static String[] initialize(String programName, String[] args, CommandLineParser parser) { String[] rval = null; if (args != null) { rval = parser.parse(args); } //Check if we should only explain the config final ConfigPropertySetter setterFunction = NamedOption.CONFIG_EXPLAIN.getValue() ? ConfigExplainSettingFunction.INSTANCE : ConfigSettingFunction.INSTANCE; // Auto-discover the package of the calling class. String className = getCallingClass(); if (className != null) { try { loadDefaultConf(className, setterFunction); } catch (ParseException e) { throw Throwables.propagate(e); } } //Look for application specific properties Stream.of( new ClasspathResource(programName.replace(".", "/") + ".conf", defaultClassLoader), Resources.fromFile(new File(SystemInfo.USER_HOME, programName + ".conf")), localConfigDirectory.getChild(programName + ".conf"), Resources.fromFile(new File(programName + ".conf")) ) .filter(Resource::exists) .forEach(resource -> { log.finest("Loading {0}.conf from :", programName); loadConfig(resource, setterFunction); }); // Store the command line arguments as a config settings. if (args != null) { parser.getSetEntries().forEach(entry -> { ConfigSettingFunction.INSTANCE.setProperty(entry.getKey(), entry.getValue(), "CommandLine"); }); } if (parser.isSet(NamedOption.CONFIG)) { loadConfig(NamedOption.CONFIG.getValue(), setterFunction); } // If config-explain was set then output the config recording and then quit if (parser.isSet(NamedOption.CONFIG_EXPLAIN)) { ConfigExplainSettingFunction settings = (ConfigExplainSettingFunction) setterFunction; for (String key : new TreeSet<>(settings.properties.keySet())) { System.err.println(key); int max = settings.properties.get(key).size(); int i = 1; for (String prop : settings.properties.get(key)) { System.err.println("\t" + (i == max ? "*" : "") + prop.replaceAll("\r?\n", " ")); i++; } System.err.println("--------------------------------------------------"); } System.exit(0); } return rval; } /** * Load default conf. * * @param packageName the package name * @param propertySetter the property setter * @return the boolean * @throws ParseException the parse exception */ protected static boolean loadDefaultConf(String packageName, ConfigPropertySetter propertySetter) throws ParseException { if (packageName.endsWith(".conf")) { return false; } packageName = packageName.replaceAll("\\$[0-9]+$", ""); Resource defaultConf = new ClasspathResource((packageName.replaceAll("\\.", "/") + "/" + DEFAULT_CONFIG_FILE_NAME).trim(), Config.getDefaultClassLoader()); // Go through each level of the package until we find one that // has a default properties file or we cannot go any further. while (!defaultConf.exists()) { int idx = packageName.lastIndexOf('.'); if (idx == -1) { defaultConf = new ClasspathResource(packageName + "/" + DEFAULT_CONFIG_FILE_NAME, Config.getDefaultClassLoader()); break; } packageName = packageName.substring(0, idx); defaultConf = new ClasspathResource(packageName.replaceAll("\\.", "/") + "/" + DEFAULT_CONFIG_FILE_NAME, Config.getDefaultClassLoader()); } if (defaultConf.exists()) { loadConfig(defaultConf, propertySetter); return true; } return false; } /** * Initializes the config file without specifying any command line arguments * * @param programName The program name * @return An empty array */ public static String[] initialize(String programName) { return initialize(programName, new String[0], new CommandLineParser()); } /** * Initialize test. */ public static void initializeTest() { clear(); initialize("Test"); } /** *

Initializes the configuration.

Looks for a properties (programName.conf) in the classpath, the user * home directory, and in the run directory to load

The command line arguments are parsed and added to the * configuration.

* * @param programName the program name * @param args the command line arguments * @return Non config/option parameters from command line */ public static String[] initialize(String programName, String[] args) { return initialize(programName, args, new CommandLineParser()); } private static Object processScript(String scriptStatement) { int idx = scriptStatement.indexOf(']'); if (idx == -1) { throw new RuntimeException(scriptStatement + " is malformed (missing closing ']'')"); } String extension = scriptStatement.substring(SCRIPT_PROPERTY.length(), idx).trim(); String objectName = null; if (extension.contains(",")) { String[] splits = extension.split(","); extension = splits[0].trim(); objectName = splits[1].trim(); } ScriptEnvironment env = ScriptEnvironmentManager.getInstance().getEnvironment(extension); if (!StringUtils.isNullOrBlank(objectName)) { return env.getObject(objectName); } idx = scriptStatement.indexOf(':'); if (idx == -1) { throw new RuntimeException(scriptStatement + " is malformed (missing :)"); } String script = scriptStatement.substring(idx + 1).trim(); try { return env.eval(script); } catch (ScriptException e) { throw Throwables.propagate(e); } } /** * Resolve variables string. * * @param string the string * @return the string */ static String resolveVariables(String string) { if (string == null) { return null; } String rval = string; Matcher m = STRING_SUBSTITUTION.matcher(string); while (m.find()) { if (getInstance().properties.containsKey(m.group(1))) { rval = rval.replaceAll(Pattern.quote(m.group(0)), get(m.group(1)).asString()); } else if (System.getProperties().contains(m.group(1))) { rval = rval.replaceAll(Pattern.quote(m.group(0)), System.getProperties().get(m.group(1)).toString()); } else if (System.getenv().containsKey(m.group(1))) { rval = rval.replaceAll(Pattern.quote(m.group(0)), System.getenv().get(m.group(1))); } } return rval; } /** * Sets the value of a property. * * @param name the name of the property * @param value the value of the property */ public static void setProperty(String name, String value) { getInstance().properties.put(name, value); if (name.toLowerCase().endsWith(".level")) { String className = name.substring(0, name.length() - ".level".length()); LogManager.getLogManager().setLevel(className, Level.parse(value.trim().toUpperCase())); } if (name.equals("com.davidbracewell.logging.Logger.logfile")) { FileHandler handler; try { handler = new FileHandler(value); } catch (IOException e) { throw Throwables.propagate(e); } handler.setFormatter(new LogFormatter()); LogManager.addHandler(handler); } if (name.toLowerCase().startsWith(SYSTEM_PROPERTY)) { String systemSetting = name.substring(SYSTEM_PROPERTY.length()); System.setProperty(systemSetting, value); } log.finest("Setting property {0} to value of {1}", name, value); } /** * Determines if the value of the given property is a script or not * * @param propertyName The name of the property to check * @return True if the property exists and its value is a script */ public static boolean valueIsScript(String propertyName) { return (getInstance().properties.containsKey(propertyName) && getInstance().properties.get(propertyName).startsWith(SCRIPT_PROPERTY)); } /** * Gets default class loader. * * @return the default class loader */ public static ClassLoader getDefaultClassLoader() { return defaultClassLoader; } /** * Sets the default ClassLoader to use with Classpath resources * * @param newDefaultClassLoader The new ClassLoader */ public static void setDefaultClassLoader(@NonNull ClassLoader newDefaultClassLoader) { defaultClassLoader = newDefaultClassLoader; } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeObject(properties); out.writeObject(loaded); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); getInstance().properties.putAll(Cast.>as(in.readObject())); getInstance().loaded.addAll(Cast.>as(in.readObject())); } /** * A ConfigPropertySetter implementation that records which config file set a property. This class is used with the * config-explain command line option. */ enum ConfigExplainSettingFunction implements ConfigPropertySetter { /** * The INSTANCE. */ INSTANCE; /** * The Properties. */ final Map> properties = new HashMap<>(); @Override public void setProperty(String name, String value, String resourceName) { Config.setProperty(name, value); if (!properties.containsKey(name)) { properties.put(name, new LinkedHashSet()); } properties.get(name).add(resourceName + "::" + value); } } /** * Standard implementation of ConfigPropertySetter */ enum ConfigSettingFunction implements ConfigPropertySetter { /** * The INSTANCE. */ INSTANCE; @Override public void setProperty(String name, String value, String resourceName) { Config.setProperty(name, value); } } /** * A ConfigPropertySetter takes care of setting properties and their values are they are parsed by the * {@link ConfigParser}. * * @author David B. Bracewell */ interface ConfigPropertySetter { /** * Sets a property * * @param name The name of the property * @param value The value of the property * @param resourceName The resource that is responsible for this property */ void setProperty(String name, String value, String resourceName); }// END OF ConfigPropertySetter }// END OF Config