uk.co.modularaudio.service.configuration.impl.ConfigurationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for uk.co.modularaudio.service.configuration.impl.ConfigurationServiceImpl.java

Source

/**
 *
 * Copyright (C) 2015 - Daniel Hams, Modular Audio Limited
 *                      daniel.hams@gmail.com
 *
 * Mad is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Mad 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Mad.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package uk.co.modularaudio.service.configuration.impl;

import java.awt.Color;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import uk.co.modularaudio.service.configuration.ConfigurationService;
import uk.co.modularaudio.util.component.ComponentWithLifecycle;
import uk.co.modularaudio.util.date.DateConverter;
import uk.co.modularaudio.util.exception.ComponentConfigurationException;
import uk.co.modularaudio.util.exception.RecordNotFoundException;

/**
 * <p>An implementation of the configuration service interface that
 * reads the configuration key values from a filesystem file or
 * from a classloader resource path.</p>
 *
 * <p>Clients are advised to use the helper class ConfigurationServiceHelper
 * which provides methods for bulk error handling.</p>
 *
 * @see uk.co.modularaudio.service.configuration.ConfigurationServiceHelper
 *
 * @author dan
 *
 */
public class ConfigurationServiceImpl implements ConfigurationService, ComponentWithLifecycle {
    private static Log log = LogFactory.getLog(ConfigurationServiceImpl.class.getName());

    private final static String ALGO_TYPE = "Blowfish";

    /**
     * The classloader resource path that should be used to find
     * a properties file containing key value pairs
     */
    private String configResourcePath;
    /**
     * A list of additional classloader resource paths that are used
     * to add additional key value pairs to the config information
     */
    private String[] additionalResourcePaths;
    /**
     * The filesystem path that should be used to find
     * a properties file containing key value pairs
     */
    private String configFilePath;
    /**
     * A list of additional filesystem paths that are used
     * to add additional key value pairs to the config information
     */
    private String[] additionalFilePaths;

    /**
     * Whether the configuration service should attempt to initialise
     * an encryption cipher. Requries setting the encryptionKey.
     */
    private boolean useEncryption = false;
    /**
     * A base64 encoded string used as encryption pepper.
     */
    private String encryptionPepper;

    private SecretKeySpec keySpec;
    private Cipher cipher;

    private final HashMap<String, String> keyToValueMap = new HashMap<String, String>();
    private final HashSet<String> usedKeys = new HashSet<String>();

    /**
     * If set to true the service will log where it is reading key value pairs from
     */
    private final boolean logWhereConfigComesFrom;

    /**
     * Emtpy constructor will initialise the service without it logging
     * where it is retrieving the key values from.
     */
    public ConfigurationServiceImpl() {
        this(false);
    }

    /**
     * Constructor that allows you to specify if you want the
     * sources of key value pairs logged.
     * @param logWhereConfigComesFrom set to yes for logging of config sources
     */
    public ConfigurationServiceImpl(final boolean logWhereConfigComesFrom) {
        this.logWhereConfigComesFrom = logWhereConfigComesFrom;
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.util.component.ComponentWithLifecycle#init()
     */
    @Override
    public void init() throws ComponentConfigurationException {

        if (configFilePath != null) {
            if (logWhereConfigComesFrom) {
                if (log.isInfoEnabled()) {
                    log.info("ConfigurationServiceImpl beginning. Will use '" + configFilePath + "'"); // NOPMD by dan on 02/02/15 11:51
                }
            }
            parseOneFilePath(configFilePath);

            if (additionalFilePaths != null && additionalFilePaths.length > 0) {
                for (final String additionalFilePath : additionalFilePaths) {
                    parseOneFilePath(additionalFilePath);
                }
            }
        } else if (configResourcePath != null) {
            if (logWhereConfigComesFrom) {
                if (log.isInfoEnabled()) {
                    log.info("ConfigurationServiceImpl beginning. Will use '" + configResourcePath + "'"); // NOPMD by dan on 02/02/15 11:51
                }
            }
            parseOneResourcePath(configResourcePath);

            if (additionalResourcePaths != null && additionalResourcePaths.length > 0) {
                for (final String additionalResourcePath : additionalResourcePaths) {
                    parseOneResourcePath(additionalResourcePath);
                }
            }
        } else {
        }

        if (useEncryption && encryptionPepper != null) {
            try {
                // Initialise the secret key with our pepper (its not salt,
                // since salt should be added to taste i.e. random).
                final byte keyAsBytes[] = Base64.decodeBase64(encryptionPepper);

                keySpec = new SecretKeySpec(keyAsBytes, ALGO_TYPE);
                cipher = Cipher.getInstance(ALGO_TYPE);
            } catch (final NoSuchAlgorithmException nsae) {
                final String msg = "Unable to initialise config key decryption: " + nsae.toString();
                log.error(msg, nsae);
                throw new ComponentConfigurationException(msg, nsae);
            } catch (final NoSuchPaddingException nspe) {
                final String msg = "Unable to initialise config key decryption: " + nspe.toString();
                log.error(msg, nspe);
                throw new ComponentConfigurationException(msg, nspe);
            }
        } else if (useEncryption) {
            throw new ComponentConfigurationException("No encryption key specified yet component expected one");
        }
    }

    /**
     * Internal method that opens the supplied file and reads
     * it as though it is a properties file.
     * @param fpToProcess path of the file to read
     * @throws ComponentConfigurationException
     */
    private void parseOneFilePath(final String fpToProcess) throws ComponentConfigurationException {
        InputStream fis = null;
        final BufferedReader br = null;
        try {
            fis = new FileInputStream(new File(fpToProcess));
            parseOneInputStream(fis);
        } catch (final IOException ioe) {
            final String msg = "IOException caught reading config file: " + ioe.toString();
            log.error(msg);
            throw new ComponentConfigurationException(msg);
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
                if (fis != null) {
                    fis.close();
                }
            } catch (final IOException e) {
                throw new ComponentConfigurationException("Failed cleaning up config file stream: " + e.toString(),
                        e);
            }
        }
    }

    /**
     * Internal method that find the supplied resource using
     * the current class classloader and reads
     * it as though it is a properties file.
     * @param rpToProcess resource path to open
     * @throws ComponentConfigurationException
     */
    private void parseOneResourcePath(final String rpToProcess) throws ComponentConfigurationException {
        InputStream fis = null;
        final BufferedReader br = null;
        try {
            fis = getClass().getResourceAsStream(rpToProcess);
            if (fis == null) {
                final String msg = "Unable to find config file '" + rpToProcess + "'";
                log.error(msg);
                throw new ComponentConfigurationException(msg);
            }
            parseOneInputStream(fis);
        } catch (final IOException ioe) {
            final String msg = "IOException caught reading config file: " + ioe.toString();
            log.error(msg);
            throw new ComponentConfigurationException(msg);
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
                if (fis != null) {
                    fis.close();
                }
            } catch (final IOException e) {
                throw new ComponentConfigurationException("Failed cleaning up config file stream: " + e.toString(),
                        e);
            }
        }
    }

    /**
     * Read the contents of a properties file as an input stream
     * and populate the internal key value pairs with the contents.
     * @param is input stream to parse
     * @throws ComponentConfigurationException on malformed content
     * @throws IOException on file IO errors
     */
    private void parseOneInputStream(final InputStream is) throws ComponentConfigurationException, IOException {
        final BufferedReader br = new BufferedReader(new InputStreamReader(is));

        String line = null;
        while ((line = br.readLine()) != null) {
            if (line.length() == 0 || line.charAt(0) == '#' || line.charAt(0) == '/') {
                continue;
            } else {
                final int equalsIndex = line.indexOf('=');
                if (equalsIndex != -1) {
                    final String key = line.substring(0, equalsIndex);
                    final String value = line.substring(equalsIndex + 1);
                    keyToValueMap.put(key, value);
                } else {
                    throw new ComponentConfigurationException("Unparsable line: " + line);
                }
            }
        }
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.util.component.ComponentWithLifecycle#destroy()
     */
    @Override
    public void destroy() {
        for (final String key : keyToValueMap.keySet()) {
            if (!usedKeys.contains(key)) {
                if (log.isWarnEnabled()) {
                    log.warn("Configuration key: " + key + " never read!");
                }
            }
        }
    }

    /**
     * <p>Specify a classloader resource path as the source of configuration
     * information.</p>
     * <p>If both a filesystem path and classloader resource path are specified,
     * the filesystem path wins.</p>
     * @param configResource the resource path
     */
    public void setConfigResourcePath(final String configResource) {
        this.configResourcePath = configResource;
    }

    /**
     * Additional classloader resource paths from which to pull key value
     * pairs.
     * @param additionalResourcePaths an array of classloader resource paths
     */
    public void setAdditionalResourcePaths(final String[] additionalResourcePaths) {
        this.additionalResourcePaths = additionalResourcePaths;
    }

    /**
     * If encryption of values inside the properties file is required
     * this field must be set to true
     * @param useEncryption set to true for use of encryption
     */
    public void setUseEncryption(final boolean useEncryption) {
        this.useEncryption = useEncryption;
    }

    /**
     * <p>The encryption seed passed during initialisation of the cipher.</p>
     * <p>This field must be set if useEncryption is true.</p>
     * <p>Use of the method {@link ConfigurationServiceImpl#encryptStringWithKey(String)}
     * is a good way to generate the necessary base64 encoded string.</p>
     * @param encryptionPepper base64 encoded string of the required encryption pepper
     */
    public void setEncryptionPepper(final String encryptionPepper) {
        this.encryptionPepper = encryptionPepper;
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleStringValue(java.lang.String)
     */
    @Override
    public String getSingleStringValue(final String key) throws RecordNotFoundException {
        final String retVal = keyToValueMap.get(key);
        usedKeys.add(key);

        if (retVal == null) {
            throw new RecordNotFoundException("Unknown configuration key: " + key);
        } else {
            return (retVal);
        }
    }

    /**
     * <p>Specify a filesystem path as the source of configuration
     * information.</p>
     * <p>If both a filesystem path and classloader resource path are specified,
     * the filesystem path wins.</p>
     * @param configFile path to the properties file on the filesystem
     */
    public void setConfigFilePath(final String configFile) {
        this.configFilePath = configFile;
    }

    /**
     * Additional filesystem paths from which to pull key value
     * pairs.
     * @param additionalFilePaths array of filesystem paths
     */
    public void setAdditionalFilePaths(final String[] additionalFilePaths) {
        this.additionalFilePaths = additionalFilePaths;
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getCommaSeparatedStringValues(java.lang.String)
     */
    @Override
    public String[] getCommaSeparatedStringValues(final String key) throws RecordNotFoundException {
        final String values = getSingleStringValue(key);
        if (values.length() > 0) {
            return values.split(",");
        } else {
            return new String[0];
        }
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleEncryptedStringValue(java.lang.String)
     */
    @Override
    public String getSingleEncryptedStringValue(final String key) throws RecordNotFoundException {
        if (!useEncryption) {
            throw new RecordNotFoundException("No encryption configured");
        }
        String retVal = keyToValueMap.get(key);
        usedKeys.add(key);

        if (retVal == null) {
            throw new RecordNotFoundException("Unknown configuration key: " + key);
        } else {
            try {
                // Decrypt it

                // Convert the text back into bytes for decrpytion
                retVal = decryptStringWithKey(retVal);
            } catch (final Exception e) {
                final String msg = "Exception thrown decrypting " + key + ": " + e.toString();
                log.error(msg, e);
                throw new RecordNotFoundException(msg);
            }
            return (retVal);
        }
    }

    /**
     * A method to decrypt a base64 encoded string ciphertext previous encrypted
     * with the corresponding encryptStringWithKey method.
     * @param cipherTextString base64 encoded cipher text
     * @return decrypted plain text
     * @throws InvalidKeyException on failure during cipher initialisation
     * @throws IllegalBlockSizeException on failure during cipher initialisation
     * @throws BadPaddingException on failure during cipher initialisation
     * @throws UnsupportedEncodingException on failure during cipher initialisation
     */
    public String decryptStringWithKey(final String cipherTextString) throws InvalidKeyException,
            IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
        if (!useEncryption) {
            throw new IllegalStateException("No encryption configured");
        }
        final byte cipherTextBytes[] = Base64.decodeBase64(cipherTextString);

        cipher.init(Cipher.DECRYPT_MODE, keySpec);

        final byte plainTextBytes[] = cipher.doFinal(cipherTextBytes);

        return new String(plainTextBytes, "UTF-8");
    }

    /**
     * A method to encrypt a given plaintext string using the configuration service internal
     * cipher mechanism.
     * @param plainText plaintext string to encrypt
     * @return cipher text encrypted contents as a base64 encoded string
     * @throws InvalidKeyException on failure during cipher initialisation
     * @throws IllegalBlockSizeException on failure during cipher initialisation
     * @throws BadPaddingException on failure during cipher initialisation
     * @throws UnsupportedEncodingException on failure during cipher initialisation
     */
    public String encryptStringWithKey(final String plainText) throws InvalidKeyException,
            IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
        if (!useEncryption) {
            throw new IllegalStateException("No encryption configured");
        }
        final byte plainTextBytes[] = plainText.getBytes("UTF-8");

        cipher.init(Cipher.ENCRYPT_MODE, keySpec);

        final byte cipherTextBytes[] = cipher.doFinal(plainTextBytes);

        final String b64CipherText = Base64.encodeBase64String(cipherTextBytes);

        return b64CipherText;
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleIntValue(java.lang.String)
     */
    @Override
    public int getSingleIntValue(final String key) throws RecordNotFoundException {
        int retVal = 0;
        boolean badValue = false;

        final String tmpStr = keyToValueMap.get(key);
        usedKeys.add(key);

        if (tmpStr == null) {
            badValue = true;
        } else {
            try {
                retVal = Integer.parseInt(keyToValueMap.get(key));
            } catch (final NumberFormatException nfe) {
                badValue = true;
            }
        }

        if (badValue) {
            throw new RecordNotFoundException("Unknown or bad number for configuration key: " + key);
        }

        return (retVal);
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleLongValue(java.lang.String)
     */
    @Override
    public long getSingleLongValue(final String key) throws RecordNotFoundException {
        long retVal = 0;
        boolean badValue = false;

        final String tmpStr = keyToValueMap.get(key);
        usedKeys.add(key);

        if (tmpStr == null) {
            badValue = true;
        } else {
            try {
                retVal = Long.parseLong(keyToValueMap.get(key));
            } catch (final NumberFormatException nfe) {
                badValue = true;
            }
        }

        if (badValue) {
            throw new RecordNotFoundException("Unknown or bad number for configuration key: " + key);
        }

        return (retVal);
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleDoubleValue(java.lang.String)
     */
    @Override
    public double getSingleDoubleValue(final String key) throws RecordNotFoundException {
        double retVal = 0;
        boolean badValue = false;

        final String tmpStr = keyToValueMap.get(key);
        usedKeys.add(key);

        if (tmpStr == null) {
            badValue = true;
        } else {
            try {
                retVal = Double.parseDouble(keyToValueMap.get(key));
            } catch (final NumberFormatException nfe) {
                badValue = true;
            }
        }

        if (badValue) {
            throw new RecordNotFoundException("Unknown or bad number for configuration key: " + key);
        }

        return (retVal);
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleFloatValue(java.lang.String)
     */
    @Override
    public float getSingleFloatValue(final String key) throws RecordNotFoundException {
        float retVal = 0;
        boolean badValue = false;

        final String tmpStr = keyToValueMap.get(key);
        usedKeys.add(key);

        if (tmpStr == null) {
            badValue = true;
        } else {
            try {
                retVal = Float.parseFloat(keyToValueMap.get(key));
            } catch (final NumberFormatException nfe) {
                badValue = true;
            }
        }

        if (badValue) {
            throw new RecordNotFoundException("Unknown or bad number for configuration key: " + key);
        }

        return (retVal);
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleBooleanValue(java.lang.String)
     */
    @Override
    public boolean getSingleBooleanValue(final String key) throws RecordNotFoundException {
        boolean retVal = false;
        boolean badValue = false;

        final String tmpStr = keyToValueMap.get(key);
        usedKeys.add(key);

        if (tmpStr == null) {
            badValue = true;
        } else {
            if (tmpStr.equals("true")) {
                retVal = true;
            } else if (tmpStr.equals("false")) {
                retVal = false;
            } else {
                badValue = true;
            }
        }

        if (badValue) {
            throw new RecordNotFoundException("Bad boolean specified for configuration key: " + key);
        }

        return (retVal);
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleBooleanValue(java.lang.String, boolean)
     */
    @Override
    public boolean getSingleBooleanValue(final String key, final boolean defaultValue) {
        boolean retVal = false;

        final String tmpStr = keyToValueMap.get(key);
        usedKeys.add(key);

        if (tmpStr == null) {
            return defaultValue;
        } else {
            if (tmpStr.trim().equals("true")) {
                retVal = true;
            } else if (tmpStr.trim().equals("false")) {
                retVal = false;
            } else {
                retVal = defaultValue;
            }
        }
        return retVal;
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getKeysBeginningWith(java.lang.String)
     */
    @Override
    public String[] getKeysBeginningWith(final String keyStart) {
        final ArrayList<String> retVal = new ArrayList<String>();

        final Set<String> keys = keyToValueMap.keySet();
        for (final String curKey : keys) {
            if (curKey.indexOf(keyStart) == 0) {
                usedKeys.add(curKey);
                retVal.add(curKey);
            }
        }

        return (retVal.toArray(new String[0]));
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleStringValue(java.lang.String, java.lang.String)
     */
    @Override
    public String getSingleStringValue(final String key, final String defaultValue) {
        final String retVal = keyToValueMap.get(key);
        usedKeys.add(key);

        if (retVal == null) {
            return defaultValue;
        } else {
            return retVal;
        }
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleDateValue(java.lang.String)
     */
    @Override
    public Date getSingleDateValue(final String key) throws RecordNotFoundException {
        final String strVal = this.getSingleStringValue(key);

        return DateConverter.customDateTimeStrToJavaDate(strVal, DateConverter.MA_USER_DATE_TIME_FORMAT);
    }

    /* (non-Javadoc)
     * @see uk.co.modularaudio.service.configuration.ConfigurationService#getSingleColorValue(java.lang.String)
     */
    @Override
    public Color getSingleColorValue(final String key) throws RecordNotFoundException {
        final String strVal = this.getSingleStringValue(key);
        return Color.decode(strVal);
    }
}