DeploymentConfig.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;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.polago.deployconf.group.ConfigGroupManager;
import org.polago.deployconf.task.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Describes a DeploymentConfiguration loaded from an InputStream.
 */
public class DeploymentConfig {

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

    private static final int BUF_SIZE = 1024;

    private final List<Task> tasks;

    private String name;

    private ConfigGroupManager groupManager;

    /**
     * Public Constructor.
     */
    public DeploymentConfig() {
        tasks = new ArrayList<Task>();
    }

    /**
     * Gets the name property value.
     *
     * @return the current value of the name property
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name property.
     *
     * @param name the new property value
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Gets the groupManager property value.
     *
     * @return the current value of the groupManager property
     */
    public ConfigGroupManager getGroupManager() {
        return groupManager;
    }

    /**
     * Sets the groupManager property.
     *
     * @param groupManager the new property value
     */
    public void setGroupManager(ConfigGroupManager groupManager) {
        this.groupManager = groupManager;
    }

    /**
     * Perform an interactive post merge operation.
     * <p>
     * For each non-configured Task, ask the user to configure the task.
     *
     * @param configurer the IntercativeConfigurer to use
     * @param forceInteractive if true, all tasks will be considered not configured
     * @return true if the user successfully configured all non-configured Tasks
     * @throws Exception indicating processing failure
     */
    public boolean interactiveMerge(InteractiveConfigurer configurer, boolean forceInteractive) throws Exception {

        if (forceInteractive) {
            logger.debug("Configure all Tasks interactively");
        } else {
            logger.debug("Configure Tasks interactively");
        }

        boolean result = true;

        printIntercativePreamble(configurer.getWriter());

        for (Task t : tasks) {
            if (forceInteractive || !t.isConfigured()) {
                boolean tr = t.configureInteractively(configurer, forceInteractive);
                if (tr == false) {
                    result = false;
                }
            }
        }

        return result;
    }

    /**
     * Print a preamble to prepare the user to configure interactively.
     *
     * @param writer the PrintWriter to use
     */
    private void printIntercativePreamble(PrintWriter writer) {
        writer.println();
        writer.println("One or more configuration properties needs a value");
        writer.println();
    }

    /**
     * Merge the template configuration into this instance.
     *
     * @param template the template configuration to merge
     * @return true if the merge was successful, ie all configurations has a value.
     * @throws Exception indicating processing failure
     */
    public boolean merge(DeploymentConfig template) throws Exception {

        setName(template.getName());

        // Remove all tasks that isn't available in the template since they are
        // not used anymore
        tasks.retainAll(template.tasks);

        // Add any new task from the template
        for (Task t : template.tasks) {
            if (!tasks.contains(t)) {
                tasks.add(t);
            } else {
                mergeTask(t);
            }
        }

        // Determine if any task lacks a configuration value
        for (Task t : tasks) {
            if (!t.isConfigured()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Merge a single Task with corresponding Task in this instance.
     *
     * @param task the Task to merge
     */
    private void mergeTask(Task task) {
        for (Task t : tasks) {
            if (t.equals(task)) {
                t.merge(task);
                return;
            }
        }
    }

    /**
     * Gets the tasks property value.
     *
     * @return the current value of the tasks property
     */
    public List<Task> getTasks() {
        return tasks;
    }

    /**
     * Add a task to this DeploymentConfig.
     *
     * @param task the Task to add
     */
    public void addTask(Task task) {
        tasks.add(task);
    }

    /**
     * Determine if this DeploymentConfig has no Tasks.
     *
     * @return true if this DeploymentConfig doent'y have any Tasks
     */
    public boolean isEmpty() {
        return tasks.isEmpty();
    }

    /**
     * Save this DeploymentConfig to persistent storage.
     *
     * @param outputStream the stream to save this DeploymentConfig into
     * @throws IOException indicating failure
     */
    public void save(OutputStream outputStream) throws IOException {
        DeploymentWriter writer = new DeploymentWriter(outputStream);
        writer.persist(this);
    }

    /**
     * Apply this DeploymentConfig to the destination using source as input.
     *
     * @param srcStream the InputStream file to use
     * @param destStream the OutputStream file to use
     * @param ignorePath a zip path to ignore
     * @throws Exception indicating IO error
     */
    public void apply(InputStream srcStream, OutputStream destStream, String ignorePath) throws Exception {

        ZipInputStream srcZipStream = new ZipInputStream(srcStream);
        ZipOutputStream destZipStream = new ZipOutputStream(destStream);

        ZipEntry e = srcZipStream.getNextEntry();

        Map<String, List<Task>> taskMap = getTaskMap();
        logger.debug("Using TaskMap: {}", taskMap);

        if (e == null) {
            logger.warn("Source input stream has no entries");
        }

        boolean entryWritten = false;
        while (e != null) {
            if (e.getName().equals(ignorePath)) {
                logger.debug("Ignoring Zip Entry: " + e);
                e = srcZipStream.getNextEntry();
                continue;
            }
            destZipStream.putNextEntry(createZipEntry(e));
            List<Task> taskList = taskMap.get(e.getName());
            if (taskList != null) {
                applyZipEntry(e, taskList, srcZipStream, destZipStream);
            } else {
                copyZipEntry(e, srcZipStream, destZipStream);
            }
            e = srcZipStream.getNextEntry();
            entryWritten = true;
        }
        if (entryWritten) {
            destZipStream.closeEntry();
        }
        destZipStream.finish();
    }

    /**
     * Create a new ZipEntry preserving relevant fields.
     * <p>
     * Just reusing the original entry seems to produce a corrupt zip file
     *
     * @param e the ZipEntry to use
     * @return a new ZipEntry based on e
     */
    private ZipEntry createZipEntry(ZipEntry e) {
        ZipEntry result = new ZipEntry(e.getName());
        result.setComment(e.getComment());
        byte[] extra = e.getExtra();
        if (extra != null) {
            result.setExtra(extra.clone());
        }
        result.setTime(e.getTime());

        return result;
    }

    /**
     * Copy the given ZipEntry from src to dest.
     *
     * @param e the ZipEnrty to copy
     * @param src the source ZipInputStream
     * @param dest the destination ZipOutputStream
     * @throws IOException indicating IO error
     */
    private void copyZipEntry(ZipEntry e, ZipInputStream src, ZipOutputStream dest) throws IOException {

        logger.debug("Copying Zip Entry: " + e);

        byte[] buf = new byte[BUF_SIZE];
        int i = src.read(buf);
        while (i != -1) {
            dest.write(buf, 0, i);
            i = src.read(buf);
        }
    }

    /**
     * Apply given Tasks to a ZipEntry.
     *
     * @param e the ZipEntry to use
     * @param taskList the list of task to apply to the ZipEntry
     * @param zipSrc the ZipInputStream file to use
     * @param zipDest the ZipOutputStream file to use
     * @throws Exception indicating processing error
     */
    private void applyZipEntry(ZipEntry e, List<Task> taskList, ZipInputStream zipSrc, ZipOutputStream zipDest)
        throws Exception {

        logger.info("Applying deployment config to Zip Entry: " + e);
        for (Task t : taskList) {
            t.apply(zipSrc, zipDest);
        }
    }

    /**
     * Create a Map with the path as key and a list of Tasks as value from the list of Tasks.
     *
     * @return a Map of available tasks for each path
     */
    private Map<String, List<Task>> getTaskMap() {
        Map<String, List<Task>> result = new HashMap<String, List<Task>>();

        for (Task t : getTasks()) {
            String path = t.getPath();
            List<Task> taskList = result.get(path);
            if (taskList == null) {
                taskList = new ArrayList<Task>();
                result.put(path, taskList);
            }
            taskList.add(t);
        }

        return result;
    }

}