net.opentsdb.tools.ConfigArgP.java Source code

Java tutorial

Introduction

Here is the source code for net.opentsdb.tools.ConfigArgP.java

Source

// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tools;

import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import net.opentsdb.utils.Config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * <p>Title: ConfigArgP</p>
 * <p>Description: Wraps {@link Config} and {@link ArgP} instances for a consolidated configuration and command line handler</p> 
 * @author Whitehead (nwhitehead AT heliosdev DOT org)
 * <p><code>net.opentsdb.tools.ConfigArgP</code></p>
 */

public class ConfigArgP {
    /** Static class logger */
    protected static final Logger LOG = LoggerFactory.getLogger(ConfigArgP.class);
    /** The command line argument holder for all (default and extended) options */
    protected final ArgP argp = new ArgP();
    /** The command line argument holder for default options */
    protected final ArgP dargp = new ArgP();
    /** The non config option arguments */
    protected String[] nonOptionArgs = {};
    /** The base configuration */
    protected final Config config;
    /** The raw configuration items loaded from the json file */
    protected final TreeSet<ConfigurationItem> configItemsByKey = new TreeSet<ConfigurationItem>();
    /** The raw configuration items loaded from the json file  */
    protected final TreeSet<ConfigurationItem> configItemsByCl = new TreeSet<ConfigurationItem>();

    /** The regex pattern to perform a substitution for <b><pre><code>${&lt;sysprop&gt;:&lt;default&gt;}</code></pre></b> patterns in strings */
    public static final Pattern SYS_PROP_PATTERN = Pattern.compile("\\$\\{(.*?)(?::(.*?))??\\}");
    /** The regex pattern to perform a substitution for <b><pre><code>$[&lt;javascript snippet&gt;]</code></pre></b> patterns in strings */
    public static final Pattern JS_PATTERN = Pattern.compile("\\$\\[(.*?)\\]", Pattern.MULTILINE);

    /** The config key for the TSD RPC addin classes */
    public static final String TSD_RPC_ADDIN_KEY = "tsd.addins.rpcs";
    /** The config key for the TSD RPC addin classpath */
    public static final String TSD_RPC_ADDIN_CP_KEY = "tsd.addins.rpcs.classpath";

    /** Indicates if we're on Windows, in which case the SysProp handling needs a few tweaks */
    public static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");
    /** The JavaScript Engine to interpret <b><code>$[&lt;javascript snippet&gt;]</code></b> values */
    protected static final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByExtension("js");

    static {
        // Initialize js bindings
        Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
        bindings.put("bindings", bindings);
    }

    /**
     * Creates a new ConfigArgP
     * @param args The command line arguments
     */
    public ConfigArgP(String... args) {
        InputStream is = null;

        try {
            final Config loadConfig = new NoLoadConfig();
            is = ConfigArgP.class.getClassLoader().getResourceAsStream("opentsdb.conf.json");
            ObjectMapper jsonMapper = new ObjectMapper();
            JsonNode root = jsonMapper.reader().readTree(is);
            JsonNode configRoot = root.get("config-items");
            scriptEngine.eval("var config = " + configRoot.toString() + ";");
            processBindings(jsonMapper, root);
            final ConfigurationItem[] loadedItems = jsonMapper.reader(ConfigurationItem[].class)
                    .readValue(configRoot);
            final TreeSet<ConfigurationItem> items = new TreeSet<ConfigurationItem>(Arrays.asList(loadedItems));
            LOG.info("Loaded [{}] Configuration Items from opentsdb.conf.json", items.size());
            //         if(LOG.isDebugEnabled()) {
            StringBuilder b = new StringBuilder("Configs:");
            for (ConfigurationItem ci : items) {
                b.append("\n\t").append(ci.toString());
            }
            b.append("\n");
            LOG.info(b.toString());
            //         }
            for (ConfigurationItem ci : items) {
                LOG.debug("Processing CI [{}]", ci.getKey());
                if (ci.meta != null) {
                    argp.addOption(ci.clOption, ci.meta, ci.description);
                    if ("default".equals(ci.help))
                        dargp.addOption(ci.clOption, ci.meta, ci.description);
                } else {
                    argp.addOption(ci.clOption, ci.description);
                    if ("default".equals(ci.help))
                        dargp.addOption(ci.clOption, ci.description);
                }
                if (!configItemsByKey.add(ci)) {
                    throw new RuntimeException("Duplicate configuration key [" + ci.key
                            + "] in opentsdb.conf.json. Programmer Error.");
                }
                if (!configItemsByCl.add(ci)) {
                    throw new RuntimeException("Duplicate configuration command line option [" + ci.clOption
                            + "] in opentsdb.conf.json. Programmer Error.");
                }
                if (ci.getDefaultValue() != null) {
                    ci.setValue(processConfigValue(ci.getDefaultValue()));
                    loadConfig.overrideConfig(ci.key, processConfigValue(ci.getValue()));
                }
            }
            //loadConfig.loadStaticVariables();
            // find --config and --include-config in argp and load into config 
            //      validate
            //argp.parse(args);
            this.config = new Config(loadConfig);
            nonOptionArgs = applyArgs(args);
        } catch (Exception ex) {
            if (ex instanceof IllegalArgumentException) {
                throw (IllegalArgumentException) ex;
            }
            throw new RuntimeException("Failed to read opentsdb.conf.json", ex);
        } finally {
            if (is != null)
                try {
                    is.close();
                } catch (Exception x) {
                    /* No Op */ }
        }
    }

    /**
     * <p>Title: NoLoadConfig</p>
     * <p>Description: A {@link Config} override that does not trigger {@link Config#loadStaticVariables()} when {@link Config#overrideConfig(String, String)} is called.</p> 
     * <p>Company: Helios Development Group LLC</p>
     * @author Whitehead (nwhitehead AT heliosdev DOT org)
     * <p><code>net.opentsdb.tools.ConfigArgP.NoLoadConfig</code></p>
     */
    private static class NoLoadConfig extends Config {
        /**
         * {@inheritDoc}
         * @see net.opentsdb.utils.Config#overrideConfig(java.lang.String, java.lang.String)
         */
        @Override
        public void overrideConfig(final String property, final String value) {
            this.properties.put(property, value);
        }
    }

    /**
     * Parses the command line arguments, and where the options are recognized config items, the value is validated, then applied to the config
     * @param args The command line arguments
     * @return The un-applied command line arguments
     */
    public String[] applyArgs(String[] args) {
        LOG.debug("Applying Command Line Args {}", Arrays.toString(args));
        String[] nonArgs = argp.parse(args);
        LOG.debug("Applying Command Line ArgP {}", argp);
        LOG.debug("configItemsByCl Keys: [{}]", configItemsByCl.toString());
        for (Map.Entry<String, String> entry : argp.getParsed().entrySet()) {
            String key = entry.getKey(), value = entry.getValue();
            ConfigurationItem citem = getConfigurationItemByClOpt(key);
            LOG.debug("Loaded CI for command line option [{}]: Found:{}", key, citem != null);
            if (citem.getMeta() == null) {
                citem.setValue(value != null ? value : "true");
            } else {
                if (value != null) {
                    citem.setValue(processConfigValue(value));
                }
            }
            //         log("CL Override [%s] --> [%s]", citem.getKey(), citem.getValue());
            config.overrideConfig(citem.getKey(), citem.getValue());
        }
        return nonArgs;
    }

    private ConfigurationItem getConfigurationItemByKey(final Set<ConfigurationItem> source, final String key) {
        for (final ConfigurationItem ci : source) {
            if (ci.getKey().equals(key))
                return ci;
        }
        return null;
    }

    /**
     * Returns the {@link ConfigurationItem} with the passed key
     * @param key The key of the item to fetch
     * @return The matching ConfigurationItem or null if one was not found
     */
    public ConfigurationItem getConfigurationItem(final String key) {
        if (key == null || key.trim().isEmpty())
            throw new IllegalArgumentException("The passed key was null or empty");
        return getConfigurationItemByKey(configItemsByKey, key);
    }

    /**
     * Returns the {@link ConfigurationItem} with the passed cl-option
     * @param clopt The cl-option of the item to fetch
     * @return The matching ConfigurationItem or null if one was not found
     */
    public ConfigurationItem getConfigurationItemByClOpt(final String clopt) {
        if (clopt == null || clopt.trim().isEmpty())
            throw new IllegalArgumentException("The passed cl-opt was null or empty");
        for (final ConfigurationItem ci : configItemsByCl) {
            if (ci.getClOption().equals(clopt))
                return ci;
        }
        return null;

    }

    /**
     * {@inheritDoc}
     */
    public String toString() {
        StringBuilder b = new StringBuilder();
        for (ConfigurationItem ci : configItemsByKey) {
            b.append(ci.toString()).append("\n");
        }
        return b.toString();
    }

    /**
     * Returns a default usage banner with optional prefixed messages, one per line.
     * @param msgs The optional message
     * @return the formatted usage banner
     */
    public String getDefaultUsage(String... msgs) {
        StringBuilder b = new StringBuilder("\n");
        for (String msg : msgs) {
            b.append(msg).append("\n");
        }
        b.append(dargp.usage());
        return b.toString();
    }

    /**
     * Returns an extended usage banner with optional prefixed messages, one per line.
     * @param msgs The optional message
     * @return the formatted usage banner
     */
    public String getExtendedUsage(String... msgs) {
        StringBuilder b = new StringBuilder("\n");
        for (String msg : msgs) {
            b.append(msg).append("\n");
        }
        b.append(argp.usage());
        return b.toString();
    }

    public static void main(String args[]) {
        log("JSON Config Test");

        ConfigArgP cp = new ConfigArgP();
        log(cp.toString());
        log("=======");
        log(cp.getDefaultUsage());
    }

    public static void log(String format, Object... args) {
        System.out.println(String.format(format, args));
    }

    /**
     * Performs sys-prop and js evals on the passed value
     * @param text The value to process
     * @return the processed value
     */
    public static String processConfigValue(CharSequence text) {
        return evaluate(tokenReplaceSysProps(text));
    }

    /**
     * Replaces all matched tokens with the matching system property value or a configured default
     * @param text The text to process
     * @return The substituted string
     */
    public static String tokenReplaceSysProps(CharSequence text) {
        if (text == null)
            return null;
        Matcher m = SYS_PROP_PATTERN.matcher(text);
        StringBuffer ret = new StringBuffer();
        while (m.find()) {
            String replacement = decodeToken(m.group(1), m.group(2) == null ? "<null>" : m.group(2));
            if (replacement == null) {
                throw new IllegalArgumentException(
                        "Failed to fill in SystemProperties for expression with no default [" + text + "]");
            }
            if (IS_WINDOWS) {
                replacement = replacement.replace(File.separatorChar, '/');
            }
            m.appendReplacement(ret, replacement);
        }
        m.appendTail(ret);
        return ret.toString();
    }

    /**
     * Evaluates JS expressions defines as configuration values
     * @param text The value of a configuration item to evaluate for JS expressions
     * @return The passed value with any embedded JS expressions evaluated and replaced
     */
    public static String evaluate(CharSequence text) {
        if (text == null)
            return null;
        Matcher m = JS_PATTERN.matcher(text);
        StringBuffer ret = new StringBuffer();
        final boolean isNas = scriptEngine.getFactory().getEngineName().toLowerCase().contains("nashorn");
        while (m.find()) {
            String source = (isNas ? "load(\"nashorn:mozilla_compat.js\");\nimportPackage(java.lang); "
                    : "\nimportPackage(java.lang); ") + m.group(1);
            try {

                Object obj = scriptEngine.eval(source);
                if (obj != null) {
                    //log("Evaled [%s] --> [%s]", source, obj);
                    m.appendReplacement(ret, obj.toString());
                } else {
                    m.appendReplacement(ret, "");
                }
            } catch (Exception ex) {
                ex.printStackTrace(System.err);
                throw new IllegalArgumentException("Failed to evaluate expression [" + text + "]");
            }
        }
        m.appendTail(ret);
        return ret.toString();
    }

    /**
     * Attempts to decode the passed dot delimited as a system property, and if not found, attempts a decode as an 
     * environmental variable, replacing the dots with underscores. e.g. for the key: <b><code>buffer.size.max</b></code>,
     * a system property named <b><code>buffer.size.max</b></code> will be looked up, and then an environmental variable
     * named <b><code>buffer.size.max</b></code> will be looked up.
     * @param key The dot delimited key to decode
     * @param defaultValue The default value returned if neither source can decode the key
     * @return the decoded value or the default value if neither source can decode the key
     */
    public static String decodeToken(String key, String defaultValue) {
        String value = System.getProperty(key, System.getenv(key.replace('.', '_')));
        return value != null ? value : defaultValue;
    }

    /**
     * <p>Title: ConfigurationItem</p>
     * <p>Description: A container class for deserialized configuration items from <b><code>opentsdb.conf.json</code></b>.</p> 
     * @author Whitehead (nwhitehead AT heliosdev DOT org)
     * <p><code>net.opentsdb.tools.ConfigArp.ConfigurationItem</code></p>
     */
    public static class ConfigurationItem implements Comparable<ConfigurationItem> {
        /** The internal configuration key */
        @JsonProperty("key")
        protected String key;
        /** The command line option key that maps to this item */
        @JsonProperty("cl-option")
        protected String clOption;
        /** The original value, loaded from opentsdb.conf.json, and never overwritten */
        @JsonProperty("defaultValue")
        protected String defaultValue;
        /** A description of the configuration item */
        @JsonProperty("description")
        protected String description;
        /** The command line help level at which this item will be displayed ('default' or 'extended') */
        @JsonProperty("help")
        protected String help;
        /** The meta symbol representing the type of value expected for a parameterized command line arg */
        @JsonProperty("meta")
        protected String meta;

        /** The decoded or overriden value */
        protected String value;

        /**
         * Creates a new ConfigurationItem
         */
        public ConfigurationItem() {
        }

        /**
         * Creates a new ConfigurationItem
         * @param key The internal configuration key
         * @param clOption The command line option key that maps to this item
         * @param defaultValue The original value, loaded from opentsdb.conf.json, and never overwritten 
         * @param description A description of the configuration item
         * @param help The command line help level at which this item will be displayed ('default' or 'extended')
         * @param meta The meta symbol representing the type of value expected for a parameterized command line arg
         */
        public ConfigurationItem(String key, String clOption, String defaultValue, String description, String help,
                String meta) {
            super();
            this.key = key;
            this.clOption = clOption;
            this.defaultValue = defaultValue;
            this.description = description;
            this.help = help;
            this.meta = meta;
        }

        /**
         * Validates the value
         */
        public void validate() {
            if (meta != null && value != null) {
                ConfigMetaType.byName(meta).validate(this);
            }
        }

        /**
         * Returns a descriptive name with the cl option and key
         * @return a descriptive name
         */
        public String getName() {
            return String.format("cl: %s, key: %s", clOption, key);
        }

        /**
         * Returns the item key name
         * @return the itemName
         */
        public String getKey() {
            return key;
        }

        /**
         * Returns the command line option mapping to this item
         * @return the clOption
         */
        public String getClOption() {
            return clOption;
        }

        /**
         * Returns the item current value
         * @return the value
         */
        public String getValue() {
            return value != null ? value : defaultValue;
        }

        /**
         * Sets a new value for this item
         * @param newValue The new value
         */
        public void setValue(final String newValue) {
            final String currValue = newValue;
            value = newValue.trim();
            try {
                validate();
            } catch (IllegalArgumentException ex) {
                value = currValue;
                throw ex;
            }
        }

        /**
         * Returns the original raw value loaded from opentsdb.conf.json
         * @return the original raw value
         */
        public String getDefaultValue() {
            return defaultValue;
        }

        /**
         * Returns the item description
         * @return the description
         */
        public String getDescription() {
            return description;
        }

        /**
         * Returns the help level for this option
         * @return the help
         */
        public String getHelp() {
            return help;
        }

        /**
         * Returns the meta symbol
         * @return the meta
         */
        public String getMeta() {
            return meta;
        }

        /**
         * {@inheritDoc}
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return String.format(
                    "ConfigurationItem [key=%s, clOption=%s, value=%s, description=%s, help=%s, meta=%s, defaultValue=%s]",
                    key, clOption, value, description, help, meta, defaultValue);
        }

        /**
         * {@inheritDoc}
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((key == null) ? 0 : key.hashCode());
            return result;
        }

        /**
         * {@inheritDoc}
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ConfigurationItem other = (ConfigurationItem) obj;
            if (key == null) {
                if (other.key != null)
                    return false;
            } else if (!key.equals(other.key))
                return false;
            return true;
        }

        /**
         * <p>Sorts {@link ConfigurationItem}s by the underlying {@link ConfigMetaType}</p>
         * {@inheritDoc}
         * @see java.lang.Comparable#compareTo(java.lang.Object)
         */
        @Override
        public int compareTo(final ConfigurationItem other) {
            if (this == other)
                return 0;
            if ((other.meta == null || other.meta.isEmpty()) && (meta == null || meta.isEmpty())) {
                return this.key.compareTo(other.key);
            }
            if (other.meta == null || other.meta.isEmpty()) {
                return 1;
            }
            if (meta == null || meta.isEmpty()) {
                return -1;
            }
            final ConfigMetaType otherType = ConfigMetaType.byName(other.meta);
            final ConfigMetaType thisType = ConfigMetaType.byName(meta);
            int c = thisType.compareTo(otherType);
            if (c == 0) {
                c = this.key.compareTo(other.key);
            }
            return c;
        }
    }

    /**
     * Returns the
     * @return the argp
     */
    public ArgP getArgp() {
        return argp;
    }

    /**
     * Returns the
     * @return the dargp
     */
    public ArgP getDargp() {
        return dargp;
    }

    /**
     * Returns the
     * @return the config
     */
    public Config getConfig() {
        return config;
    }

    /**
     * Returns the non config option arguments
     * @return the non config option arguments
     */
    public String[] getNonOptionArgs() {
        return nonOptionArgs;
    }

    /**
     * Determines if the passed key is contained in the non option args
     * @param nonOptionKey The non option key to check for
     * @return true if the passed key is present, false otherwise
     */
    public boolean hasNonOption(String nonOptionKey) {
        if (nonOptionArgs == null || nonOptionArgs.length == 0 || nonOptionKey == null
                || nonOptionKey.trim().isEmpty())
            return false;
        return Arrays.binarySearch(nonOptionArgs, nonOptionKey) >= 0;
    }

    /**
     * Checks the <b><source>opentsdb.conf.json</source></b> document to see if it has a <b><source>bindings</source></b> segment
     * which contains JS statements to evaluate which will prime variables used by the configuration. 
     * @param jsonMapper The JSON mapper
     * @param root The root <b><source>opentsdb.conf.json</source></b> document 
     */
    protected void processBindings(ObjectMapper jsonMapper, JsonNode root) {
        try {
            if (root.has("bindings")) {
                JsonNode bindingsNode = root.get("bindings");
                if (bindingsNode.isArray()) {
                    String[] jsLines = jsonMapper.reader(String[].class).readValue(bindingsNode);
                    StringBuilder b = new StringBuilder();
                    for (String s : jsLines) {
                        b.append(s).append("\n");
                    }
                    scriptEngine.eval(b.toString());
                    LOG.info("Successfully evaluated [{}] lines of JS in bindings", jsLines.length);
                }
            }
        } catch (Exception ex) {
            throw new IllegalArgumentException("Failed to evaluate opentsdb.conf.json bindings", ex);
        }
    }

}