org.fuin.kickstart4j.Kickstart4J.java Source code

Java tutorial

Introduction

Here is the source code for org.fuin.kickstart4j.Kickstart4J.java

Source

/**
 * Copyright (C) 2009 Future Invent Informationsmanagement GmbH. All rights
 * reserved. <http://www.fuin.org/>
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option) any
 * later version.
 *
 * This library 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 Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library. If not, see <http://www.gnu.org/licenses/>.
 */
package org.fuin.kickstart4j;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

import org.apache.commons.exec.CommandLine;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.fuin.utils4j.Cancelable;
import org.fuin.utils4j.Utils4J;
import org.fuin.utils4swing.common.ScreenCenterPositioner;
import org.fuin.utils4swing.common.Utils4Swing;
import org.fuin.utils4swing.dialogs.CanceledException;
import org.fuin.utils4swing.dialogs.DirectorySelector;
import org.fuin.utils4swing.progress.FileCopyProgressListener;
import org.fuin.utils4swing.progress.FileCopyProgressMonitor;
import org.fuin.utils4swing.threadsafe.ThreadSafeJOptionPane;

/**
 * Main application.
 */
public final class Kickstart4J {

    private static final Logger LOG = Logger.getLogger(Kickstart4J.class);

    private static final String PROGRAM_TERMINATED_WITH_ERROR = "Program terminated with error!";

    private static final String INCOMPLETE_FILE = ".incomplete";

    private static final String PROGRAM_DIRECTORY_KEY = "program-directory";

    /** Configuration used for the application. */
    private final Config config;

    /** Listens to life cycle events. */
    private Kickstart4JListener listener;

    /**
     * Constructor with configuration.
     * 
     * @param config
     *            Configuration to use.
     */
    public Kickstart4J(final Config config) {
        super();
        if (config == null) {
            throw new IllegalArgumentException("The argument 'config' cannot be null!");
        }
        this.config = config;
        this.listener = new DefaultListener();
    }

    /**
     * Returns the life cycle listener.
     * 
     * @return Listener - Always non-<code>null</code>.
     */
    public final Kickstart4JListener getListener() {
        return listener;
    }

    /**
     * Sets the life cycle listener.
     * 
     * @param listener
     *            Listener - Will be set to a default listener if
     *            <code>null</code>.
     */
    public final void setListener(final Kickstart4JListener listener) {
        if (listener == null) {
            this.listener = new DefaultListener();
        } else {
            this.listener = listener;
        }
    }

    /**
     * Initialize file logging with configuration values.
     */
    private void initLogging() {

        try {
            final File logFile = new File(config.getLogFilename()).getCanonicalFile();

            boolean ok = true;
            if (!logFile.getParentFile().exists()) {
                ok = logFile.getParentFile().mkdirs();
            }

            if (ok) {
                final Properties props = new Properties();
                props.put("log4j.rootLogger", "INFO, FILE");
                props.put("log4j.appender.FILE", "org.apache.log4j.RollingFileAppender");
                props.put("log4j.appender.FILE.File", logFile.toString());
                props.put("log4j.appender.FILE.MaxFileSize", "1MB");
                props.put("log4j.appender.FILE.MaxBackupIndex", "1");
                props.put("log4j.appender.FILE.layout", "org.apache.log4j.PatternLayout");
                props.put("log4j.appender.FILE.layout.ConversionPattern", "%d [%t] %-5p %c - %m%n");
                PropertyConfigurator.configure(props);
            } else {
                LOG.error("Cannot create log directory: " + logFile.getParentFile());
            }
        } catch (final IOException ex) {
            LOG.error("Cannot create log!", ex);
        }

    }

    /**
     * Executes the installer/updater.
     * 
     * @throws CanceledException
     *             The user canceled the installation.
     * @throws InvalidConfigException
     *             The configuration is invalid.
     */
    public final void execute() throws CanceledException, InvalidConfigException {

        Locale.setDefault(config.getLocale());
        final File destDir = getDestDir();
        config.getCmdLineOptions().put("destDir", destDir.toString());

        // Check configuration AFTER destination directory is set
        // and logging is initialized
        config.check();
        initLogging();
        listener.initComplete();

        // Start the update
        final UpdateSet updateSet = new UpdateSet(config.getSrcFiles(), config.getMkDirs(), destDir,
                config.isLazyLoading());
        if (updateSet.isUpdateNecessary()) {
            if (LOG.isInfoEnabled()) {
                LOG.info("An update is available: New=" + updateSet.getNewFiles().size() + ", Changed="
                        + updateSet.getChangedFiles().size() + ", Deleted=" + updateSet.getDeletedFiles().size()
                        + ", SilentInstall=" + config.isSilentInstall() + ", SilentUpdate="
                        + config.isSilentUpdate() + ", FirstInstallation=" + config.isFirstInstallation());
            }
            if (config.isSilentUpdate() || config.isFirstInstallation()
                    || isAnswerYes(config.getMessages().getUpdateAvailable())) {
                execute(updateSet);
                final File installationIncompleteFile = new File(destDir, INCOMPLETE_FILE);
                if (installationIncompleteFile.exists()) {
                    installationIncompleteFile.delete();
                }
            }
        } else {
            LOG.info("Files are up to date");
        }

        final JFrame startFrame = showStartFrame();

        config.getCmdLineOptions().put("classpath", updateSet.createClasspath());

        // Write the config to the target directory
        saveConfigToTargetDir(destDir);

        // Run the target application
        final CommandLine commandLine = new CommandLine(config.getJavaExe());
        commandLine.addArguments(config.getJavaArgs(), false);
        logStart(destDir, commandLine.toString());
        new ApplicationStarter(destDir, commandLine, startFrame, listener, config).execute();

    }

    private JFrame showStartFrame() {
        if (config.isShowStartFrame()) {
            final JFrame startFrame = Utils4Swing.createShowAndPosition(config.getMessages().getStartDialogTitle(),
                    new StartPanel(), false, new ScreenCenterPositioner());
            startFrame.setResizable(false);
            return startFrame;
        } else {
            return null;
        }
    }

    private void saveConfigToTargetDir(final File destDir) {
        final File appXmlFile = new File(destDir, "application.xml");
        try {
            final String localConfigFileUrl = appXmlFile.toURI().toURL().toString();
            config.setConfigFileUrl(localConfigFileUrl);
            config.getCmdLineOptions().put("configFileUrl", localConfigFileUrl);
            config.writeToStaticXML(appXmlFile, true);
        } catch (final IOException ex) {
            throw new RuntimeException("Error writing " + appXmlFile + "!", ex);
        }
    }

    private File getIdFile(final Config config) {
        return new File(Utils4J.getUserHomeDir(), config.getIdFilename());
    }

    private void execute(final UpdateSet updateSet) throws CanceledException {
        executeMkdirs(updateSet.getDestDir(), updateSet.getMkDirs());

        final List orderList = updateSet.getOrderList();
        for (int i = 0; i < orderList.size(); i++) {
            final int order = ((Integer) orderList.get(i)).intValue();

            final List newFiles = updateSet.getNewFiles(order);
            final List changedFiles = updateSet.getChangedFiles(order);
            final List deletedFiles = updateSet.getDeletedFiles(order);
            executeCopy(updateSet.getDestDir(), newFiles, changedFiles, deletedFiles);

            final List decompressFiles = updateSet.getDecompressFiles(order);
            executeDecompress(updateSet.getDestDir(), decompressFiles);

        }

    }

    private void executeMkdirs(final File destDir, final List mkdirs) {

        for (int i = 0; i < mkdirs.size(); i++) {
            final MkDir mkDir = (MkDir) mkdirs.get(i);
            final File dirToCreate = mkDir.getDestDir(destDir);
            if (dirToCreate.exists()) {
                LOG.info("MKDIR: " + dirToCreate + " (Already exists)");
            } else {
                final boolean ok = dirToCreate.mkdirs();
                if (LOG.isInfoEnabled()) {
                    if (ok) {
                        LOG.info("MKDIR: " + dirToCreate);
                    } else {
                        LOG.info("MKDIR FAILED: " + dirToCreate);
                    }
                }
            }
        }

    }

    private void executeCopy(final File destDir, final List newFiles, final List changedFiles,
            final List deletedFiles) throws CanceledException {

        final int max = newFiles.size() + changedFiles.size();
        if (max > 0) {

            final Cancelable cancelable = new Cancelable() {

                private volatile boolean canceled = false;

                public void cancel() {
                    canceled = true;
                }

                public boolean isCanceled() {
                    return canceled;
                }
            };

            final FileCopyProgressMonitor monitor = new FileCopyProgressMonitor(cancelable, config.getTitle(),
                    config.getMessages().getProgressMonitorTransferText(),
                    config.getMessages().getProgressMonitorSrcLabelText(),
                    config.getMessages().getProgressMonitorDestLabelText(), max);

            int count = 0;

            monitor.open();
            try {

                // New files
                if (cancelable.isCanceled()) {
                    throw new CanceledException();
                }
                count = copyFiles(destDir, cancelable, monitor, newFiles, count, "NEW");

                // Changed files
                if (cancelable.isCanceled()) {
                    throw new CanceledException();
                }
                count = copyFiles(destDir, cancelable, monitor, changedFiles, count, "CHANGED");

                // No longer existent (deleted) files
                if (cancelable.isCanceled()) {
                    throw new CanceledException();
                }
                for (int i = 0; i < deletedFiles.size(); i++) {
                    if (cancelable.isCanceled()) {
                        break;
                    }
                    count = count + 1;
                    final String file = (String) deletedFiles.get(i);
                    final File destFile = new File(destDir, file);
                    final boolean ok = destFile.delete();
                    if (LOG.isInfoEnabled()) {
                        if (ok) {
                            LOG.info("DELETED: " + destFile);
                        } else {
                            LOG.info("DELETE FAILED: " + destFile);
                        }
                    }
                    monitor.updateFile("", destFile.toString(), count, 0);
                }

            } finally {
                monitor.close();
            }

        }
    }

    private void executeDecompress(final File destDir, final List compressedFiles) throws CanceledException {

        final int max = compressedFiles.size();
        if (max > 0) {

            final Cancelable cancelable = new Cancelable() {

                private volatile boolean canceled = false;

                public void cancel() {
                    canceled = true;
                }

                public boolean isCanceled() {
                    return canceled;
                }
            };

            final FileCopyProgressMonitor monitor = new FileCopyProgressMonitor(cancelable, config.getTitle(),
                    config.getMessages().getProgressMonitorDecompressText(),
                    config.getMessages().getProgressMonitorSrcLabelText(),
                    config.getMessages().getProgressMonitorDestLabelText(), max);

            monitor.open();
            try {

                for (int i = 0; i < max; i++) {
                    final SrcFile file = (SrcFile) compressedFiles.get(i);
                    final File compressedFile = file.getDestFile(destDir);
                    if (LOG.isInfoEnabled()) {
                        LOG.info("Decompressing: " + compressedFile);
                    }
                    Utils.unzip(monitor, compressedFile, (i + 1), destDir, cancelable);
                }

            } finally {
                monitor.close();
            }

        }
    }

    private int copyFiles(final File destDir, final Cancelable cancelable, final FileCopyProgressListener listener,
            final List files, final int total, final String type) {

        int count = total;

        for (int i = 0; i < files.size(); i++) {
            if (cancelable.isCanceled()) {
                break;
            }
            count = count + 1;
            final SrcFile file = (SrcFile) files.get(i);
            final URL srcFileUrl = file.getSrcFileURL();
            final File destFile = file.getDestFile(destDir);
            try {
                Utils.copyURLToFile(listener, srcFileUrl, destFile, count, file.getSizeAsInt());
            } catch (final FileNotFoundException ex) {
                throw new RuntimeException("Source file not found!", ex);
            }
            final String hash = Utils4J.createHashMD5(destFile);
            if (!hash.equals(file.getMd5Hash())) {
                LOG.error("Hash local file (" + hash + ") is different from configuration hash ("
                        + file.getMd5Hash() + ")! [" + srcFileUrl + "]");
            }
            if (LOG.isInfoEnabled()) {
                LOG.info(type + ": " + srcFileUrl + " => " + destFile);
            }
        }

        return count;

    }

    private void logStart(final File dir, final String commandLine) {
        try {
            final File file = new File(dir, "start.log");
            final FileWriter writer = new FileWriter(file);
            try {
                writer.write(commandLine);
            } finally {
                writer.close();
            }
        } catch (final IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private File getDestDir() throws CanceledException {
        final File installationIncompleteFile;
        final File dir;
        final File idFile = getIdFile(config);
        if (idFile.exists()) {
            // Update
            final Properties props = Utils4J.loadProperties(idFile);
            final String dirStr = props.getProperty(PROGRAM_DIRECTORY_KEY);
            if (dirStr == null) {
                throw new IllegalStateException(
                        "The property '" + PROGRAM_DIRECTORY_KEY + "' was not found inside '" + idFile + "'!");
            }
            dir = new File(dirStr);
            Utils4J.checkValidDir(dir);
            installationIncompleteFile = new File(dir, INCOMPLETE_FILE);
        } else {
            // First installation or file removed
            final String dirStr;
            final Properties props = new Properties();
            if (config.isSilentInstall()) {
                dirStr = config.getDestDir().toString();
            } else {
                // Ask User for destination directory
                dirStr = DirectorySelector.selectDirectory(config.getMessages().getSelectDestinationDirectory(),
                        config.getDestDir().toString()).getDirectory();
            }
            props.setProperty(PROGRAM_DIRECTORY_KEY, dirStr);
            Utils4J.saveProperties(idFile, props, "# --- DO NOT EDIT OR DELETE --- Generated by Kickstart4J ---");
            dir = new File(dirStr);
            installationIncompleteFile = new File(dir, INCOMPLETE_FILE);
            if (!dir.exists()) {
                try {
                    dir.mkdirs();
                    installationIncompleteFile.createNewFile();
                } catch (final IOException ex) {
                    throw new RuntimeException("Cannot create file '" + installationIncompleteFile + "'!");
                }
            }
        }
        config.setFirstInstallation(installationIncompleteFile.exists());
        return dir;
    }

    private static boolean isAnswerYes(final String message) {
        final int result = ThreadSafeJOptionPane.showConfirmDialog(null, message, "TITLE",
                JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
        return result == JOptionPane.YES_OPTION;
    }

    /**
     * Shows a message and exits the program with <code>System.exit(..)</code>.
     * 
     * @param message
     *            Message to display.
     * @param exitCode
     *            System exit code.
     */
    public static void showMessageAndExit(final String message, final int exitCode) {
        ThreadSafeJOptionPane.showMessageDialog(null, message, "Hint", JOptionPane.INFORMATION_MESSAGE);
        System.exit(exitCode);
    }

    /**
     * Shows an error message and exits the program with
     * <code>System.exit(..)</code>.
     * 
     * @param parser
     *            Parser for usage display.
     * @param ex
     *            Exception to display.
     * @param exitCode
     *            System exit code.
     */
    public static void displayCmdLineExceptionAndExit(final CmdLineParser parser, final CmdLineException ex,
            final int exitCode) {
        final StringBuffer sb = new StringBuffer();
        sb.append(ex.getMessage());
        sb.append("\n");
        sb.append("\n");
        sb.append("java Kickstart4J [options]");
        sb.append("\n");
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        parser.printUsage(out);
        sb.append(out.toString());
        sb.append("\n");
        System.out.println(sb);
        ErrorDialog.showAndExit(sb.toString(), exitCode);
    }

    /**
     * Shows an error message and exits the program with
     * <code>System.exit(..)</code>.
     * 
     * @param ex
     *            Exception to display.
     * @param exitCode
     *            System exit code.
     */
    public static void displayExceptionAndExit(final Exception ex, final int exitCode) {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        ex.printStackTrace(new PrintStream(out));
        final String msg = out.toString();
        System.err.println(msg);
        ErrorDialog.showAndExit(msg, exitCode);
    }

    /**
     * Empty implementation.
     */
    private static final class DefaultListener implements Kickstart4JListener {

        /**
         * {@inheritDoc}
         */
        public final void initComplete() {
            // Do nothing
        }

        /**
         * {@inheritDoc}
         */
        public void startupComplete() {
            // Do nothing
        }

    }

    /**
     * Main method used to start the installer/updater. If you want to start it
     * from another Java application you can simply use
     * <code>new Kickstart4J(kickstart4JConfig).execute()</code> instead of
     * calling this method.
     * 
     * @param args
     *            Command line arguments.
     */
    public static void main(final String[] args) {

        final Logger log = Logger.getLogger(Kickstart4J.class);

        final Config config = new Config();
        try {

            // Parse command line
            final CmdLineParser cmdLineParser = new CmdLineParser(Locale.getDefault());
            try {
                cmdLineParser.parse(args);
                if (log.isDebugEnabled()) {
                    log.info("Command line arguments: " + cmdLineParser);
                }

                // Set user defined options from command line
                cmdLineParser.copyToConfig(config);

                // Load the configuration and start update
                try {
                    ConfigParser.parse(config, config.getConfigFileURL());
                    if (log.isInfoEnabled()) {
                        log.info("Configuration: " + config);
                    }

                    Utils4Swing.initLookAndFeel(config.getLookAndFeelClassName());
                    (new Kickstart4J(config)).execute();

                } catch (final CanceledException ex) {
                    log.info("Operation canceled by user!");
                    showMessageAndExit(config.getMessages().getOperationCanceled(), 1);
                } catch (final InvalidConfigException ex) {
                    log.error(PROGRAM_TERMINATED_WITH_ERROR, ex);
                    displayExceptionAndExit(ex, 1);
                }

            } catch (final CmdLineException ex) {
                log.error(PROGRAM_TERMINATED_WITH_ERROR, ex);
                displayCmdLineExceptionAndExit(cmdLineParser, ex, 1);
            }

        } catch (final RuntimeException ex) {
            log.error(PROGRAM_TERMINATED_WITH_ERROR, ex);
            displayExceptionAndExit(ex, 1);
        }

    }

}