org.wso2.andes.configuration.AndesConfigurationManager.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.andes.configuration.AndesConfigurationManager.java

Source

/*
 * Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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 org.wso2.andes.configuration;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axiom.om.impl.llom.OMElementImpl;
import org.apache.axiom.om.xpath.AXIOMXPath;
import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jaxen.JaxenException;
import org.w3c.dom.Document;
import org.wso2.andes.configuration.enums.AndesConfiguration;
import org.wso2.andes.configuration.util.ConfigurationProperty;
import org.wso2.andes.kernel.AndesContext;
import org.wso2.andes.kernel.AndesException;
import org.wso2.carbon.utils.ServerConstants;
import org.wso2.securevault.SecretResolver;
import org.wso2.securevault.SecretResolverFactory;
import org.wso2.securevault.commons.MiscellaneousUtil;
import org.wso2.securevault.secret.SecretLoadingModule;
import org.wso2.securevault.secret.SingleSecretCallback;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLStreamException;

import static org.wso2.carbon.core.util.Utils.replaceSystemProperty;

/**
 * This class acts as a singleton access point for all config parameters used within MB.
 */
public class AndesConfigurationManager {

    private static Log log = LogFactory.getLog(AndesConfigurationManager.class);

    /**
     * Reserved Suffixes that activate different processing logic.
     */
    private static final String PORT_TYPE = "_PORT";

    /**
     * Reserved Prefixes that activate different processing logic.
     */
    private static final String LIST_TYPE = "LIST_";

    /**
     * Namespace and attribute used by secure vault to identify encrypted properties.
     */
    private static final QName SECURE_VAULT_QNAME = new QName("http://org.wso2.securevault/configuration",
            "secretAlias");

    /**
     * Package which contains custom classes that can be parsed by the AndesConfigurationManager.
     */
    private static final String CONFIG_MODULE_PACKAGE = "org.wso2.andes.configuration.modules";

    /**
     * Common Error states
     */
    private static final String GENERIC_CONFIGURATION_PARSE_ERROR = "Error occurred when trying to parse "
            + "configuration value {0}.";

    private static final String NO_CHILD_FOR_KEY_IN_PROPERTY = "There was no child at the given key {0} for the "
            + "parent property {1}.";

    private static final String PROPERTY_NOT_A_LIST = "The input property {0} does not contain a list of child "
            + "properties.";

    private static final String PROPERTY_NOT_A_PORT = "The input property {0} is not defined as an integer value. "
            + "Therefore it is not a port property.";

    /**
     * Main file path of configuration files into which all other andes-specific config files (if any)
     * should be linked.
     */
    public static final String CARBON_CONFIG_DIR_PATH = "carbon.config.dir.path";
    public static final String CARBON_HOME = "carbon.home";
    private static String componentsPath = System.getProperty(CARBON_CONFIG_DIR_PATH);

    /**
     * File name of the main configuration file.
     */
    private static final String ROOT_CONFIG_FILE_NAME = "broker.xml";

    /**
     * Apache commons composite configuration is used to collect and maintain properties from multiple configuration
     * sources.
     */
    private static CompositeConfiguration compositeConfiguration;

    /**
     * This hashmap is used to maintain any properties that were encrypted with ciphertool. key would be the
     * secretAlias, and the value would be the decrypted value. This will be cross-referenced when reading properties
     * from broker.xml.
     */
    private static ConcurrentHashMap<String, String> cipherValueMap;

    /**
     * Decisive configurations coming from carbon.xml that affect the MB configs. e.g port Offset
     * These are injected as custom logic when reading the configurations.
     */
    private static int carbonPortOffset;

    /**
     * initialize the configuration manager. this MUST be called at application startup.
     * (QpidServiceComponent bundle -> activate event)
     *
     * @throws AndesException
     */
    public static void initialize(int portOffset) throws AndesException {

        String ROOT_CONFIG_FILE_PATH;
        // If system property is available set conf path to that value
        if (componentsPath != null) {
            ROOT_CONFIG_FILE_PATH = Paths.get(componentsPath).toString();
        } else {
            ROOT_CONFIG_FILE_PATH = Paths.get(System.getProperty(CARBON_HOME), "repository", "conf").toString();
        }

        String brokerConfigFilePath = ROOT_CONFIG_FILE_PATH + File.separator + ROOT_CONFIG_FILE_NAME;

        log.info("Main andes configuration located at : " + brokerConfigFilePath);

        try {

            compositeConfiguration = new CompositeConfiguration();
            compositeConfiguration.setDelimiterParsingDisabled(true);

            XMLConfiguration rootConfiguration = new XMLConfiguration();
            rootConfiguration.setDelimiterParsingDisabled(true);
            rootConfiguration.setFileName(brokerConfigFilePath);
            rootConfiguration.setExpressionEngine(new XPathExpressionEngine());
            rootConfiguration.load();
            compositeConfiguration.addConfiguration(rootConfiguration);

            //Decrypt and maintain secure vault property values in a map for cross-reference.
            decryptConfigurationFromFile(brokerConfigFilePath);

            // Derive certain special properties that are not simply specified in the configuration files.
            addDerivedProperties();

            // set carbonPortOffset coming from carbon
            AndesConfigurationManager.carbonPortOffset = portOffset;

            //set delivery timeout for a message
            int deliveryTimeoutForMessage = AndesConfigurationManager
                    .readValue(AndesConfiguration.PERFORMANCE_TUNING_TOPIC_MESSAGE_DELIVERY_TIMEOUT);
            AndesContext.getInstance().setDeliveryTimeoutForMessage(deliveryTimeoutForMessage);

        } catch (ConfigurationException e) {
            String error = "Error occurred when trying to construct configurations from file at path : "
                    + brokerConfigFilePath;
            log.error(error, e);
            throw new AndesException(error, e);

        } catch (UnknownHostException e) {
            String error = "Error occurred when trying to derive the bind address for messaging from configurations.";
            log.error(error, e);
            throw new AndesException(error, e);
        } catch (FileNotFoundException e) {
            String error = "Error occurred when trying to read the configuration file : " + brokerConfigFilePath;
            log.error(error, e);
            throw new AndesException(error, e);
        } catch (JaxenException e) {
            String error = "Error occurred when trying to process cipher text in file : " + brokerConfigFilePath;
            log.error(error, e);
            throw new AndesException(error, e);
        } catch (XMLStreamException e) {
            String error = "Error occurred when trying to process cipher text in file : " + brokerConfigFilePath;
            log.error(error, e);
            throw new AndesException(error, e);
        }
    }

    /**
     * The sole method exposed to everyone accessing configurations. We can use the relevant
     * enums (e.g.- config.enums.BrokerConfiguration) to pass the required property and
     * its meta information.
     *
     * @param <T>                   Expected data type of the property
     * @param configurationProperty relevant enum value (e.g.- config.enums.AndesConfiguration)
     * @return Value of config in the expected data type.
     */
    public static <T> T readValue(ConfigurationProperty configurationProperty) {

        // If the property requests a port value, we need to apply the carbon offset to it.
        if (configurationProperty.get().getName().endsWith(PORT_TYPE)) {
            return (T) readPortValue(configurationProperty);
        }

        try {
            // The cast to T is unavoidable. Even though the function returns the same data type,
            // compiler doesn't know about it. We could add the data type as a parameter,
            // but that only complicates the method call.
            return (T) deriveValidConfigurationValue(configurationProperty);
        } catch (ConfigurationException e) {

            log.error(e); // Since the descriptive message is wrapped in exception itself

            // Return the parsed default value. This path will be met if a user adds an invalid value to a property.
            // Assuming we always have proper default values defined, this will rescue us from exploding due to a
            // small mistake.
            try {
                return (T) deriveValidConfigurationValue(configurationProperty);
            } catch (ConfigurationException e1) {
                // It is highly unlikely that this will throw an exception (if defined default values are also invalid).
                // But if it does, the method will return null.
                // Exception is not propagated to avoid unnecessary clutter of config related exception handling.

                log.error(e); // Since the descriptive message is wrapped in exception itself
                return null;
            }
        }
    }

    /**
     * Using this method, you can access a singular property of a child.
     * <p/>
     * example,
     * <p/>
     * <users>
     * <user userName="testuser1">password1</user>
     * <user userName="testuser2">password2</user>
     * </users> scenario.
     *
     * @param configurationProperty relevant enum value (e.g.- above scenario -> config
     *                              .enums.AndesConfiguration.TRANSPORTS_MQTT_PASSWORD)
     * @param key                   key of the child of whom you seek the value (e.g. above scenario -> "testuser2")
     */
    public static <T> T readValueOfChildByKey(AndesConfiguration configurationProperty, String key) {

        String constructedKey = configurationProperty.get().getKeyInFile().replace("{key}", key);

        // The cast to T is unavoidable. Even though the function returns the same data type,
        // compiler doesn't know about it. We could add the data type as a parameter,
        // but that only complicates the method call.
        try {
            return (T) deriveValidConfigurationValue(constructedKey, configurationProperty.get().getDataType(),
                    configurationProperty.get().getDefaultValue());
        } catch (ConfigurationException e) {
            // This means that there is no child by the given key for the parent property.
            log.error(MessageFormat.format(NO_CHILD_FOR_KEY_IN_PROPERTY, key, configurationProperty), e);
            return null;
        }
    }

    /**
     * Use this method when you need to acquire a list of properties of same group.
     *
     * @param configurationProperty relevant enum value (e.g.- config.enums.AndesConfiguration
     *                              .LIST_TRANSPORTS_MQTT_USERNAMES)
     * @return String list of required property values
     */
    public static List<String> readValueList(AndesConfiguration configurationProperty) {

        if (configurationProperty.toString().startsWith(LIST_TYPE)) {
            return Arrays.asList(compositeConfiguration.getStringArray(configurationProperty.get().getKeyInFile()));
        } else {
            log.error(MessageFormat.format(PROPERTY_NOT_A_LIST, configurationProperty));
            return new ArrayList<String>();
        }
    }

    /**
     * Retrieves value from configuration file (i.e. broker.xml) for the
     * specified config-definition.
     * <p>
     * This method has the support for deprecated properties
     * </p>
     * Behavior is: <br/>
     * <ol>
     * <li>Check if the configuration is defined in file.</li>
     * <li>If configuration exists then return the value provided in file</li>
     * <li>if configuration is non-existent then check whether this
     * configuration is
     * associated with a another deprecated config parameter</li>
     * <li>If user has specified deprecated configuration it will be read.
     * </ol>
     *
     * @param <T> Expected data type of the property
     * @return Value of config in the expected data type.
     * @throws ConfigurationException an error
     */
    public static <T> T deriveValidConfigurationValue(ConfigurationProperty configurationProperty)
            throws ConfigurationException {

        if (compositeConfiguration.containsKey(configurationProperty.get().getKeyInFile())) {
            return (T) deriveValidConfigurationValue(configurationProperty.get().getKeyInFile(),
                    configurationProperty.get().getDataType(), configurationProperty.get().getDefaultValue());
        } else {

            // Check whether a deprecated configuration associated with current
            // config definition.
            // If that's true; check whether the deprecated configuration is
            // defined by the user.
            if (configurationProperty.hasDeprecatedProperty() && compositeConfiguration
                    .containsKey(configurationProperty.getDeprecated().get().getKeyInFile())) {
                // User is still using the old configuration parameter instead
                // of new one. therefore a warning will be logged.
                log.warn("configuration [" + configurationProperty.getDeprecated().get().getKeyInFile()
                        + "] is deprecated. please use: [" + configurationProperty.get().getKeyInFile() + "]");

                return (T) deriveValidConfigurationValue(configurationProperty.getDeprecated().get().getKeyInFile(),
                        configurationProperty.getDeprecated().get().getDataType(),
                        configurationProperty.getDeprecated().get().getDefaultValue());
            } else {
                // go with the default value of original configuration parameter
                return (T) deriveValidConfigurationValue(configurationProperty.get().getKeyInFile(),
                        configurationProperty.get().getDataType(), configurationProperty.get().getDefaultValue());
            }

        }

    }

    /**
     * Given the data type and the value read from a config, this returns the parsed value
     * of the property.
     *
     * @param key          The Key to the property being read (n xpath format as contained in file.)
     * @param dataType     Expected data type of the property
     * @param defaultValue This parameter should NEVER be null since we assign a default value to
     *                     every config property.
     * @param <T>          Expected data type of the property
     * @return Value of config in the expected data type.
     * @throws ConfigurationException
     */
    public static <T> T deriveValidConfigurationValue(String key, Class<T> dataType, String defaultValue)
            throws ConfigurationException {

        if (log.isDebugEnabled()) {
            log.debug("Reading andes configuration value " + key);
        }

        String readValue = compositeConfiguration.getString(key);

        String validValue = defaultValue;

        // If the dataType is a Custom Config Module class, the readValue will be null (since the child properties
        // are the ones with values.). Therefore the warning is printed only in other situations.
        if (StringUtils.isBlank(readValue) && !CONFIG_MODULE_PACKAGE.equals(dataType.getPackage().getName())) {
            log.warn("Error when trying to read property : " + key + ". Switching to " + "default value : "
                    + defaultValue);
        } else {
            validValue = overrideWithDecryptedValue(key, readValue);
        }

        if (log.isDebugEnabled()) {
            log.debug("Valid value read for andes configuration property " + key + " is : " + validValue);
        }

        try {

            if (Boolean.class.equals(dataType)) {
                return dataType.cast(Boolean.parseBoolean(validValue));

            } else if (Date.class.equals(dataType)) {
                // Sample date : "Sep 28 20:29:30 JST 2000"
                DateFormat df = new SimpleDateFormat("MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
                return dataType.cast(df.parse(validValue));

            } else if (dataType.isEnum()) {
                // this will indirectly forces programmer to define enum values in upper case
                return (T) Enum.valueOf((Class<? extends Enum>) dataType, validValue.toUpperCase(Locale.ENGLISH));

            } else if (CONFIG_MODULE_PACKAGE.equals(dataType.getPackage().getName())) {
                // Custom data structures defined within this package only need the root Xpath to extract the other
                // required child properties to construct the config object.
                return dataType.getConstructor(String.class).newInstance(key);

            } else {
                return dataType.getConstructor(String.class).newInstance(validValue);
            }

        } catch (NoSuchMethodException e) {
            throw new ConfigurationException(MessageFormat.format(GENERIC_CONFIGURATION_PARSE_ERROR, key), e);
        } catch (ParseException e) {
            throw new ConfigurationException(MessageFormat.format(GENERIC_CONFIGURATION_PARSE_ERROR, key), e);
        } catch (IllegalAccessException e) {
            throw new ConfigurationException(MessageFormat.format(GENERIC_CONFIGURATION_PARSE_ERROR, key), e);
        } catch (InvocationTargetException e) {
            throw new ConfigurationException(MessageFormat.format(GENERIC_CONFIGURATION_PARSE_ERROR, key), e);
        } catch (InstantiationException e) {
            throw new ConfigurationException(MessageFormat.format(GENERIC_CONFIGURATION_PARSE_ERROR, key), e);
        }
    }

    /**
     * This method is used to derive certain special properties that are not simply specified in
     * the configuration files.
     */
    private static void addDerivedProperties() throws AndesException, UnknownHostException {

        // For AndesConfiguration.TRANSPORTS_MQTT_BIND_ADDRESS
        if ("*".equals(readValue(AndesConfiguration.TRANSPORTS_MQTT_BIND_ADDRESS))) {

            InetAddress host = InetAddress.getLocalHost();
            compositeConfiguration.setProperty(AndesConfiguration.TRANSPORTS_MQTT_BIND_ADDRESS.get().getKeyInFile(),
                    host.getHostAddress());
        }

        // For AndesConfiguration.TRANSPORTS_AMQP_BIND_ADDRESS
        if ("*".equals(readValue(AndesConfiguration.TRANSPORTS_AMQP_BIND_ADDRESS))) {

            InetAddress host = InetAddress.getLocalHost();
            compositeConfiguration.setProperty(AndesConfiguration.TRANSPORTS_AMQP_BIND_ADDRESS.get().getKeyInFile(),
                    host.getHostAddress());
        }
    }

    /**
     * This method is used when reading a port value from configuration. It is intended to abstract the port offset
     * logic.If the enum contains keyword "_PORT", this will be called
     *
     * @param configurationProperty relevant enum value (e.g.- above scenario -> config.enums.AndesConfiguration
     *                              .TRANSPORTS_MQTT_PORT)
     * @return port with carbon port offset
     */
    private static Integer readPortValue(ConfigurationProperty configurationProperty) {

        if (!Integer.class.equals(configurationProperty.get().getDataType())) {
            log.error(MessageFormat.format(AndesConfigurationManager.PROPERTY_NOT_A_PORT, configurationProperty));
            return 0; // 0 can never be a valid port. therefore, returning 0 in the error path will keep code
            // predictable.
        }

        try {
            Integer portFromConfiguration = (Integer) deriveValidConfigurationValue(
                    configurationProperty.get().getKeyInFile(), configurationProperty.get().getDataType(),
                    configurationProperty.get().getDefaultValue());

            return portFromConfiguration + carbonPortOffset;

        } catch (ConfigurationException e) {
            log.error(MessageFormat.format(GENERIC_CONFIGURATION_PARSE_ERROR, configurationProperty), e);

            //recover and return default port with offset value.
            return Integer.parseInt(configurationProperty.get().getDefaultValue()) + carbonPortOffset;
        }
    }

    /**
     * Decrypt properties with secure vault and maintain on a separate hashmap for cross-reference.
     *
     * @param filePath File path to the configuration file in question
     * @throws FileNotFoundException
     * @throws JaxenException
     * @throws XMLStreamException
     */
    private static void decryptConfigurationFromFile(String filePath)
            throws FileNotFoundException, JaxenException, XMLStreamException {

        cipherValueMap = new ConcurrentHashMap<String, String>();

        StAXOMBuilder stAXOMBuilder = new StAXOMBuilder(new FileInputStream(new File(filePath)));
        OMElement dom = stAXOMBuilder.getDocumentElement();

        //Initialize the SecretResolver providing the configuration element.
        SecretResolver secretResolver = SecretResolverFactory.create(dom, false);
        Stack<String> nameStack = new Stack<String>();
        readChildElements(dom, nameStack, secretResolver);
    }

    private static void readChildElements(OMElement serverConfig, Stack<String> nameStack,
            SecretResolver secretResolver) {

        for (Iterator childElements = serverConfig.getChildElements(); childElements.hasNext();) {
            OMElement element = (OMElement) childElements.next();
            nameStack.push(element.getLocalName());
            if (elementHasText(element)) {
                String key = getKey(nameStack);
                String value;
                String resolvedValue = MiscellaneousUtil.resolve(element, secretResolver);

                if (resolvedValue != null && !resolvedValue.isEmpty()) {
                    value = resolvedValue;
                } else {
                    value = element.getText();
                }
                value = replaceSystemProperty(value);
                cipherValueMap.put(key, value);
            }
            readChildElements(element, nameStack, secretResolver);
            nameStack.pop();
        }
    }

    private static String getKey(Stack<String> nameStack) {
        StringBuffer key = new StringBuffer();
        for (int i = 0; i < nameStack.size(); i++) {
            String name = nameStack.elementAt(i);
            key.append(name).append(".");
        }
        key.deleteCharAt(key.lastIndexOf("."));

        return key.toString();
    }

    private static boolean elementHasText(OMElement element) {
        String text = element.getText();
        return text != null && text.trim().length() != 0;
    }

    /**
     * If the property is contained in the cipherValueMap, replace the raw value with that value.
     *
     * @param keyInFile xpath expression used to extract the value from file.
     * @param rawValue  The value read from the file without any processing.
     * @return the decrypted value if the property was encrypted with ciphertool.
     */
    private static String overrideWithDecryptedValue(String keyInFile, String rawValue) {

        if (keyInFile.contains("@")) {
            if (log.isDebugEnabled()) {
                log.debug("Ciphertool does not operate on xml attributes or lists. ( input key : " + keyInFile
                        + " )");
            }
        }

        if (!StringUtils.isBlank(keyInFile)) {

            // The alias is inferred from the xpath of the property.
            // If the xpath = "transports/amqp/sslConnection/keyStore/password",
            // secretAlias should be "transports.amqp.sslConnection.keyStore.password"
            String secretAlias = keyInFile.replaceAll("/", ".");

            if (cipherValueMap.containsKey(secretAlias)) {
                return cipherValueMap.get(secretAlias);
            }
        }

        return rawValue;
    }

}