PropertiesTask.java

/**
 * Copyright (c) 2013-2017 Polago AB
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.polago.deployconf.task.properties;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.LinkedHashSet;
import java.util.Set;

import org.jdom2.Element;
import org.polago.deployconf.InteractiveConfigurer;
import org.polago.deployconf.group.ConfigGroup;
import org.polago.deployconf.group.ConfigGroupManager;
import org.polago.deployconf.task.AbstractTask;
import org.polago.deployconf.task.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Deployment Task for creating properties in a file.
 */
public class PropertiesTask extends AbstractTask {

    private static Logger logger = LoggerFactory.getLogger(PropertiesTask.class);

    /**
     * Task element name in config file.
     */
    public static final String DOM_ELEMENT_TASK = "properties";

    private static final String DOM_ELEMENT_PROPERTY = "property";

    protected static final String DOM_ELEMENT_NAME = "name";

    // Properties files are always using ISO-8859-1
    private static final String ENCODING = "ISO-8859-1";

    private static final String PATH_IGNORE = PropertiesTask.class.getCanonicalName() + "_NO_PATH";;

    private Set<Property> properties;

    /**
     * Public Constructor.
     *
     * @param groupManager the groupManager to use
     */
    public PropertiesTask(ConfigGroupManager groupManager) {
        super(groupManager);
        properties = new LinkedHashSet<Property>();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deserialize(Element node) throws IOException {
        String attribute = node.getAttributeValue(DOM_ATTRIBUTE_PATH);
        if (attribute == null) {
            attribute = PATH_IGNORE;
        }
        setPath(attribute);

        for (Element e : node.getChildren()) {
            String name = e.getChildTextTrim(DOM_ELEMENT_NAME);
            if (name.length() == 0) {
                throw new IllegalStateException("Property name element does not exists");
            }
            String description = e.getChildTextTrim(DOM_ELEMENT_DESCRIPTION);
            if (description.length() == 0) {
                throw new IllegalStateException("Property description element does not exists");
            }
            String defaultValue = e.getChildTextTrim(DOM_ELEMENT_DEFAULT);

            String group = e.getAttributeValue(DOM_ATTRIBUTE_GROUP);
            String value = null;

            if (group != null) {
                value = getGroupManager().lookupGroup(group).getProperty(name);
            }

            if (value == null) {
                value = e.getChildTextTrim(DOM_ELEMENT_VALUE);
                if (group != null && value != null) {
                    logger.debug("Populating group {} with value of name {}: {}", group, name, value);
                    getGroupManager().lookupGroup(group).setProperty(name, value);
                }
            }

            Property p = new Property(name, description, defaultValue, value);

            if (group != null) {
                p.setGroup(group);
            }

            p.setCondition(e.getChildTextTrim(DOM_ELEMENT_CONDITION));

            logger.debug("Deserializing property: {}", p);

            properties.add(p);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void serialize(Element node) throws IOException {
        if (!PATH_IGNORE.equals(getPath())) {
            node.setAttribute(DOM_ATTRIBUTE_PATH, getPath());
        }

        for (Property p : properties) {
            logger.debug("Serializing property: {}", p);
            Element e = createJDOMElement(DOM_ELEMENT_PROPERTY);
            e.addContent(createJDOMTextElement(DOM_ELEMENT_NAME, p.getName()));
            e.addContent(createJDOMCDATAElement(DOM_ELEMENT_DESCRIPTION, p.getDescription()));
            e.addContent(createJDOMTextElement(DOM_ELEMENT_DEFAULT, p.getDefaultValue()));
            e.addContent(createJDOMTextElement(DOM_ELEMENT_CONDITION, p.getCondition()));

            String group = p.getGroup();
            if (group != null) {
                e.setAttribute(DOM_ATTRIBUTE_GROUP, group);
            } else {
                e.addContent(createJDOMTextElement(DOM_ELEMENT_VALUE, p.getValue()));
            }

            node.addContent(e);
        }
    }

    /**
     * Gets the properties property value.
     *
     * @return the current value of the properties property
     */
    public Set<Property> getProperties() {
        return properties;
    }

    /**
     * Sets the properties property.
     *
     * @param properties the new property value
     */
    void setProperties(Set<Property> properties) {
        this.properties = properties;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void merge(Task other) {
        if (other instanceof PropertiesTask) {
            PropertiesTask opt = (PropertiesTask) other;
            properties.retainAll(opt.getProperties());

            for (Property op : opt.getProperties()) {
                boolean exists = false;
                for (Property p : properties) {
                    if (p.equals(op)) {
                        exists = true;
                        p.setDescription(op.getDescription());
                        p.setDefaultValue(op.getDefaultValue());
                        p.setGroup(op.getGroup());
                        p.setCondition(op.getCondition());
                        logger.debug("Merging existing Property: {}", p);
                        break;
                    }
                }
                if (!exists) {
                    logger.debug("Adding new Property: {}", op);
                    properties.add(op);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isConfigured() throws IOException {
        for (Property p : properties) {
            if (evaluateCondition(p.getCondition(), getGroupManager().lookupGroup(p.getGroup()))) {
                if (p.getValue() == null || p.getValue().length() == 0) {
                    logger.debug("Property is not configured: {}", p);
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean configureInteractively(InteractiveConfigurer configurer, boolean force) throws Exception {

        boolean configured = true;

        for (Property p : properties) {
            ConfigGroup group = getGroupManager().lookupGroup(p.getGroup());
            if (evaluateCondition(p.getCondition(), group)
                && (force || p.getValue() == null || p.getValue().length() == 0)) {
                configured = configurePropertyInteractively(p, configurer);
                if (configured) {
                    group.setProperty(p.getName(), p.getValue());
                } else {
                    return configured;
                }
            }
        }

        return configured;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getSerializedName() {
        return DOM_ELEMENT_TASK;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void apply(InputStream source, OutputStream destination) throws Exception {

        if (PATH_IGNORE.equals(getPath())) {
            // This task should never be applied
            return;
        }

        OutputStreamWriter out = new OutputStreamWriter(destination, ENCODING);
        BufferedWriter writer = new BufferedWriter(out);

        for (Property p : getProperties()) {
            ConfigGroup group = getGroupManager().lookupGroup(p.getGroup());
            if (evaluateCondition(p.getCondition(), group)) {
                writer.newLine();
                String description = p.getDescription();
                if (description != null) {
                    writer.append("#");
                    writer.newLine();
                    String[] lines = description.split("[\\r\\n]+");
                    for (String line : lines) {
                        writer.append("# ");
                        writer.append(line.trim());
                        writer.newLine();
                    }
                    writer.append("#");
                    writer.newLine();
                }
                writer.append(p.getName());
                writer.append("=");

                String value = expandPropertyExpression(p.getValue(), group);
                writer.append(value);
                writer.newLine();
            }
        }
        writer.flush();
    }

    /**
     * Configure a Property by asking the user.
     *
     * @param p the Property to configure
     * @param configurer the InteractiveConfigurer to use
     * @return true if the property was configured
     * @throws IOException indicating IO failure
     */
    private boolean configurePropertyInteractively(Property p, InteractiveConfigurer configurer) throws IOException {

        boolean result = false;

        logger.debug("Configure interactively: {}", p.getName());

        String defaultValue = p.getValue();
        if (defaultValue == null || defaultValue.length() == 0) {
            defaultValue = p.getDefaultValue();
        }

        String value = configurer.configure(p.getName(), p.getDescription(), defaultValue);

        logger.debug("Configure interactively result for '{}': {}", p.getName(), value);

        if (value != null) {
            p.setValue(value);
            result = true;
        }

        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "PropertiesTask [path=" + getPath() + ", properties=" + properties + "]";
    }


}