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

com.hp.autonomy.frontend.configuration.BaseConfigFileService Maven / Gradle / Ivy

/*
 * Copyright 2013-2015 Hewlett-Packard Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

package com.hp.autonomy.frontend.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jasypt.util.text.TextEncryptor;

/**
 * Reference implementation of {@link ConfigFileService}, which outputs configuration objects as JSON files.
 * An additional type bound is placed on the configuration object this class uses.
 *
 * Clients of this API should extend {@link AbstractAuthenticatingConfigFileService} or
 * {@link AbstractUnauthenticatingConfigFileService}, depending on their needs.
 *
 * This class requires that a default config file be available at runtime.
 *
 * Operations on the Config object are thread safe.
 *
 * @param  The type of the Configuration object. If it extends {@link PasswordsConfig}, passwords will be encrypted
 *           and decrypted when the file is written and read respectively.
 *
 */
@Slf4j
public abstract class BaseConfigFileService> implements ConfigFileService {

    private String configFileLocation;
    private String configFileName;

    private String defaultConfigFile;

    // Use AtomicReference for thread safety
    private final AtomicReference config = new AtomicReference<>(null);

    private ObjectMapper mapper;

    private TextEncryptor textEncryptor;

    private final Object updateLock = new Object();

    private FilterProvider filterProvider;

    private ValidationService validationService;

    /**
     * Initialises the service
     *
     * @throws IllegalStateException If an error occurs which prevent service initialization
     * @throws Exception If an unspecified error occurs
     */
    // Exception thrown by method in library
    @SuppressWarnings("ProhibitedExceptionDeclared")
    @PostConstruct
    public void init() throws Exception {
        final String configFileLocation = getConfigFileLocation();
        T fileConfig = getEmptyConfig();

        boolean fileExists = false;
        if(StringUtils.isNotBlank(configFileLocation)) {
            log.debug("Using {} as config file location", configFileLocation);

            try(Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(configFileLocation), "UTF-8"))){
                fileConfig = mapper.readValue(reader, getConfigClass());

                if(fileConfig instanceof PasswordsConfig) {
                    fileConfig = ((PasswordsConfig) fileConfig).withDecryptedPasswords(textEncryptor);
                }

                fileExists = true;
            }
            catch(final FileNotFoundException e) {
                log.warn("Config file not found, using empty configuration object");
            }
            catch(final IOException e) {
                log.error("Error reading config file at {}", configFileLocation);
                log.error("Recording stack trace", e);
                // throw this so we don't continue to start the webapp
                throw new IllegalStateException("Could not initialize configuration", e);
            }
        }

        if (StringUtils.isNotBlank(defaultConfigFile)) {
            try (InputStream defaultConfigInputStream = getClass().getResourceAsStream(defaultConfigFile)) {
                if (defaultConfigInputStream != null) {
                    final T defaultConfig = mapper.readValue(defaultConfigInputStream, getConfigClass());

                    fileConfig = fileConfig.merge(defaultConfig);

                    if (!fileExists) {
                        fileConfig = generateDefaultLogin(fileConfig);

                        try {
                            writeOutConfigFile(fileConfig, configFileLocation);
                        } catch (final IOException e) {
                            throw new IllegalStateException("Could not initialize configuration", e);
                        }
                    }
                }
            } catch (final IOException e) {
                log.error("Error reading default config file", e);
                // throw this so we don't continue to start the webapp
                throw new IllegalStateException("Could not initialize configuration", e);
            }
        }

        try {
            fileConfig.basicValidate();
        } catch (final ConfigException e) {
            log.error("Config validation failed in " + e);
            throw new IllegalStateException("Could not initialize configuration", e);
        }

        if (fileConfig.equals(getEmptyConfig())) {
            log.info("Cannot read value from {}", configFileLocation);
            log.debug("Environment is {}", System.getenv());
        } else {
            config.set(fileConfig);
            postInitialise(getConfig());
        }
    }

    protected String getConfigFileLocation() {
        if (configFileLocation != null) {
            final String propertyValue = System.getProperty(configFileLocation);
            if (propertyValue != null) {
                return propertyValue + File.separator + configFileName;
            }
        }

        throw new IllegalStateException("No configuration file defined. System property key: " + configFileLocation);
    }

    /**
     * @inheritDoc
     */
    @Override
    public T getConfig() {
        return config.get();
    }

    /**
     * Returns the Config in a format suitable for revealing to users.
     *
     * @return A ConfigResponse of the current config. Passwords will be encrypted and the default login wil be removed.
     */
	@Override
	public ConfigResponse getConfigResponse() {
        T config = this.config.get();

        config = withoutDefaultLogin(config);

        if(config instanceof PasswordsConfig) {
            config = ((PasswordsConfig)config).withoutPasswords();
        }

        return new ConfigResponse<>(config, getConfigFileLocation(), configFileLocation);
	}

    /**
     * Validate the config with the supplied {@link ValidationService}, calls {@link #withoutDefaultLogin(Config)}
     * and {@link #withHashedPasswords(Config)} before setting the config, and calls {@link #postUpdate}
     * before writing the config to a file.
     *
     * @param config The new config
     * @throws IOException If an error occurs writing out the config file
     * @throws ConfigValidationException If the supplied config is invalid.
     * @throws Exception if an unspecified error occurs in postUpdate
     */
    @Override
    public void updateConfig(final T config) throws Exception {
        final ValidationResults validationResults = validationService.validateEnabledConfig(config);

        if(!validationResults.isValid()) {
            throw new ConfigValidationException(validationResults);
        }

        final T initialConfig = getConfig();
        setConfig(config);

        try{
            postUpdate(config);
        } catch (final ConfigException ce) {
            setConfig(initialConfig);
            throw ce;
        }

        writeOutConfigFile(this.config.get(), getConfigFileLocation());
    }

    private void setConfig(final T config) {
        final T preMergeConfig = preUpdate(config);

        synchronized(updateLock) {
            T mergedConfig = preMergeConfig.merge(this.config.get());

            mergedConfig = withoutDefaultLogin(mergedConfig);
            mergedConfig = withHashedPasswords(mergedConfig);

            this.config.set(mergedConfig);
        }
    }

	private void writeOutConfigFile(final T config, final String configFileLocation) throws IOException {
        try(Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(configFileLocation), "UTF-8"))){
            log.debug("Writing out config file to {}", configFileLocation);

            T configToWrite = config;

            if(configToWrite instanceof PasswordsConfig) {
                configToWrite = ((PasswordsConfig) configToWrite).withEncryptedPasswords(textEncryptor);
            }

            // TODO: This scales badly and needs rethinking
            if(filterProvider != null) {
                mapper.writer(filterProvider).writeValue(writer, configToWrite);
            }
            else {
                mapper.writeValue(writer, configToWrite);
            }
        }
        catch(final IOException e) {
            log.error("Error writing out config file", e);
            throw e;
        }
    }

    /**
     * @param systemProperty The name of the system property which stores the location
     *                            of the config file.
     */
    public void setConfigFileLocation(final String systemProperty) {
        configFileLocation = systemProperty;
    }

    /**
     * @param configFileName The name of the config file
     */
    public void setConfigFileName(final String configFileName) {
        this.configFileName = configFileName;
    }

    /**
     * @param defaultConfigFile The path to the default config file. This should be a resource on the classpath e.g
     *                          com/example/foo/defaultConfigFile.json
     *
     */
    public void setDefaultConfigFile(final String defaultConfigFile) {
        this.defaultConfigFile = defaultConfigFile;
    }

    /**
     * @param mapper The {@link ObjectMapper} used to perform JSON conversion.
     */
    public void setMapper(final ObjectMapper mapper) {
        this.mapper = mapper;
    }

    /**
     * If {@link T} is not a {@link PasswordsConfig}, this property need not be set.
     * @param textEncryptor The {@link TextEncryptor} used to encrypt passwords.
     */
    public void setTextEncryptor(final TextEncryptor textEncryptor) {
        this.textEncryptor = textEncryptor;
    }

    /**
     * This property is optional if no filtering is required.
     * @param filterProvider The {@link FilterProvider} used to filter the config before writing. This must provide a
     *                       filter called "configurationFilter".
     */
    public void setFilterProvider(final FilterProvider filterProvider) {
        this.filterProvider = filterProvider;
    }

    /**
     * @param validationService The {@link ValidationService} used to perform validation
     */
    public void setValidationService(final ValidationService validationService) {
        this.validationService = validationService;
    }

    /**
     * Called after the Config is initialised
     * @param config The newly initialised config
     * @throws Exception
     */
    public abstract void postInitialise(final T config) throws Exception;

    /**
     * @return The class object representing T.
     */
    public abstract Class getConfigClass();

    /**
     * Returns a configuration object on which no properties have been set.
     *
     * @return An empty configuration object.
     */
    public abstract T getEmptyConfig();

    /**
     * Generates a default login for a new config file
     * @param config The initial config object
     * @return A copy of config with a default login, or the same config object if a default login is not required
     */
    public abstract T generateDefaultLogin(T config);

    /**
     * Removes the default login from the configuration object
     * @param config The initial config object
     * @return A copy of config without a default login, or the same config object if a default login is not required
     */
    public abstract T withoutDefaultLogin(T config);

    /**
     * Hashes any passwords in the configuration object
     * @param config The initial config object
     * @return A copy of config without any plaintext passwords, or the same config object if there are no passwords
     */
    public abstract T withHashedPasswords(T config);
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy