com.redhat.rcm.version.Cli.java Source code

Java tutorial

Introduction

Here is the source code for com.redhat.rcm.version.Cli.java

Source

/*
 * Copyright (c) 2012 Red Hat, Inc.
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see
 * <http://www.gnu.org/licenses>.
 */

package com.redhat.rcm.version;

import static com.redhat.rcm.version.stats.VersionInfo.APP_BUILDER;
import static com.redhat.rcm.version.stats.VersionInfo.APP_COMMIT_ID;
import static com.redhat.rcm.version.stats.VersionInfo.APP_DESCRIPTION;
import static com.redhat.rcm.version.stats.VersionInfo.APP_NAME;
import static com.redhat.rcm.version.stats.VersionInfo.APP_TIMESTAMP;
import static com.redhat.rcm.version.stats.VersionInfo.APP_VERSION;
import static com.redhat.rcm.version.util.InputUtils.getClasspathResource;
import static com.redhat.rcm.version.util.InputUtils.getFile;
import static com.redhat.rcm.version.util.InputUtils.readClasspathProperties;
import static com.redhat.rcm.version.util.InputUtils.readListProperty;
import static com.redhat.rcm.version.util.InputUtils.readProperties;
import static com.redhat.rcm.version.util.InputUtils.readPropertiesList;
import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.apache.commons.lang.StringUtils.join;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;

import org.apache.maven.mae.MAEException;
import org.apache.maven.mae.project.key.FullProjectKey;
import org.codehaus.plexus.util.StringUtils;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.MapOptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.redhat.rcm.version.mgr.VersionManager;
import com.redhat.rcm.version.mgr.mod.ProjectModder;
import com.redhat.rcm.version.mgr.session.SessionBuilder;
import com.redhat.rcm.version.mgr.session.VersionManagerSession;
import com.redhat.rcm.version.report.Report;
import com.redhat.rcm.version.util.InputUtils;
import com.redhat.rcm.version.util.VManFormatter;
import com.redhat.rcm.version.util.http.SSLUtils;

public class Cli {
    @Argument(index = 0, metaVar = "target", usage = "POM file (or directory containing POM files) to modify.")
    private File target = new File(System.getProperty("user.dir"), "pom.xml");

    @Option(name = "-b", aliases = "--boms", usage = "File containing a list of BOM URLs to use for standardizing dependencies.\nProperty file equivalent: boms")
    private File bomList;

    @Option(name = "-B", aliases = {
            "--bootstrap" }, usage = "Bootstrap properties to read for location of VMan configuration.")
    private File bootstrapConfig;

    @Option(name = "-C", aliases = "--config", usage = "Load default configuration for BOMs, toolchain, removedPluginsList, etc. from this file/url.")
    private String configuration;

    @Option(name = "-e", usage = "POM exclude path pattern (glob)\nProperty file equivalent: pom-file-excludes")
    private String pomExcludePattern;

    @Option(name = "-E", usage = "POM exclude module list (groupId:artifactId,groupId:artifactId...)\nProperty file equivalent: pom-module-excludes")
    private String pomExcludeModules;

    @Option(name = "-h", aliases = { "--help" }, usage = "Print this message and quit")
    private boolean help;

    @Option(name = "-H", aliases = { "-hm",
            "--help-modifications" }, usage = "Print the list of available modifications and quit")
    private boolean helpModders;

    @Option(name = "-hr", aliases = {
            "--help-reports" }, usage = "Print the list of available reports, plus their configuration options, and quit")
    private boolean helpReporters;

    @Option(name = "--no-console", usage = "DON'T log information to console in addition to <workspace>/vman.log.\n")
    private boolean noConsole;

    @Option(name = "--no-log-file", usage = "DON'T log information to <workspace>/vman.log.\n")
    private boolean noLogFile;

    @Option(name = "-L", aliases = { "--local-repo",
            "--local-repository" }, usage = "Local repository directory.\nDefault: <workspace>/local-repository\nProperty file equivalent: local-repository")
    private File localRepository;

    @Option(name = "-m", aliases = "--remote-repositories", usage = "Maven remote repositories from which load missing parent POMs.\nProperty file equivalent: remote-repositories.")
    private String remoteRepositories;

    @Option(name = "-M", aliases = {
            "--enable-modifications" }, usage = "List of modifications to enable for this execution (see --help-modifications for more information).")
    private String modifications;

    @Option(name = "-O", aliases = { "--capture-output",
            "--capture-pom" }, usage = "Write captured (missing) definitions to this POM location.\nProperty file equivalent: capture-pom")
    private File capturePom;

    @Option(name = "-p", usage = "POM path pattern (glob).\nDefault: **/*.pom,**/pom.xml")
    private String pomPattern = "**/*.pom,**/pom.xml";

    @Option(name = "-P", aliases = {
            "--preserve" }, usage = "Write changed POMs back to original input files.\nDefault: false")
    private boolean preserveFiles = false;

    @Option(name = "-r", aliases = { "--rm-plugins",
            "--removed-plugins" }, usage = "List of plugins (format: <groupId:artifactId>[,<groupId:artifactId>]) to REMOVE if found.\nProperty file equivalent: removed-plugins")
    private String removedPluginsList;

    @Option(name = "--removed-tests", usage = "List of test modules (format: <groupId:artifactId>[,<groupId:artifactId>]) to remove (via maven.test.skip) if found.\nProperty file equivalent: removed-tests")
    private String removedTestsList;

    @Option(name = "--extensions-whitelist", usage = "List of extensions (format: <groupId:artifactId>[,<groupId:artifactId>]) to preserve.\nProperty file equivalent: extensions-whitelist")
    private String extensionsWhitelistList;

    @Option(name = "-R", aliases = { "--report-dir" }, usage = "Write reports here.\nDefault: <workspace>/reports")
    private File reports = new File("vman-workspace/reports");

    @Option(name = "-s", aliases = "--version-suffix", usage = "A suffix to append to each POM's version.\nProperty file equivalent: version-suffix")
    private String versionSuffix;

    @Option(name = "--version-modifier", usage = "Change each POM's version using pattern:replacement format.\nProperty file equivalent: version-modifier")
    private String versionModifier;

    @Option(name = "--strict", usage = "Change ONLY the dependencies, plugins, and parents that are listed in BOMs and toolchain POM\nDefault: false\nProperty file equivalent: strict")
    private boolean strict = false;

    @Option(name = "-S", aliases = {
            "--settings" }, usage = "Maven settings.xml file/URL.\nProperty file equivalent: settings")
    private String settings;

    @Option(name = "-t", aliases = "--toolchain", usage = "Toolchain POM URL, containing standard plugin versions in the build/pluginManagement section, and plugin injections in the regular build/plugins section.\nProperty file equivalent: toolchain")
    private String toolchain;

    @Option(name = "-T", aliases = "--test-config", usage = "Test-load the configuration given, and print diagnostic information")
    private boolean testConfig;

    @Option(name = "-XR", handler = MapOptionHandler.class, aliases = "--user-prop", usage = "Add user-defined property of the form x=y that reports and other components can pick up on")
    private Map<String, String> reportProperties;

    @Option(name = "-v", aliases = "--version", usage = "Show version information and quit.")
    private boolean showVersion;

    @Option(name = "-W", aliases = {
            "--workspace" }, usage = "Backup original files here up before modifying.\nDefault: vman-workspace")
    private File workspace = new File("vman-workspace");

    @Option(name = "-Z", aliases = {
            "--no-system-exit" }, usage = "Don't call System.exit(..) with the return value (for embedding/testing).")
    private boolean noSystemExit;

    @Option(name = "--trustpath", usage = "Directory containing .pem files with certificates of servers to trust. (Use 'classpath:' prefix for a directory embedded in the jar.)")
    private String truststorePath = DEFAULT_TRUSTSTORE_PATH;

    @Option(name = "--use-effective-poms", usage = "Enable resolution of effective POMs for projects being modified.")
    private boolean useEffectivePoms = false;

    private static final String DEFAULT_TRUSTSTORE_PATH = "classpath:ssl/trust";

    private static final File DEFAULT_CONFIG_FILE = new File(System.getProperty("user.home"), ".vman.properties");

    static final String BOOTSTRAP_PROPERTIES = "vman.boot.properties";

    public static final String REMOTE_REPOSITORIES_PROPERTY = "remote-repositories";

    @Deprecated
    public static final String REMOTE_REPOSITORY_PROPERTY = "remote-repository";

    public static final String REPORT_PROPERTY_PREFIX = "report.";

    public static final String VERSION_SUFFIX_PROPERTY = "version-suffix";

    public static final String VERSION_MODIFIER_PROPERTY = "version-modifier";

    public static final String TOOLCHAIN_PROPERTY = "toolchain";

    public static final String BOMS_LIST_PROPERTY = "boms";

    public static final String REMOVED_PLUGINS_PROPERTY = "removed-plugins";

    public static final String EXTENSIONS_WHITELIST_PROPERTY = "extensions-whitelist";

    public static final String POM_EXCLUDE_MODULE_PROPERTY = "pom-module-excludes";

    public static final String POM_EXCLUDE_FILE_PROPERTY = "pom-file-excludes";

    public static final String REMOVED_TESTS_PROPERTY = "removed-tests";

    public static final String LOCAL_REPOSITORY_PROPERTY = "local-repository";

    public static final String SETTINGS_PROPERTY = "settings";

    public static final String CAPTURE_POM_PROPERTY = "capture-pom";

    public static final String STRICT_MODE_PROPERTY = "strict";

    public static final String MODIFICATIONS = "modifications";

    public static final String RELOCATIONS_PROPERTY = "relocated-coordinates";

    public static final String PROPERTY_MAPPINGS_PROPERTY = "property-mappings";

    public static final String BOOT_CONFIG_PROPERTY = "configuration";

    public static final String TRUSTSTORE_PATH_PROPERTY = "truststore-path";

    public static final String USE_EFFECTIVE_POMS_PROPERTY = "use-effective-poms";

    private static final File DEFAULT_BOOTSTRAP_CONFIG = new File(System.getProperty("user.home"),
            ".vman.boot.properties");

    private static VersionManager vman;

    private List<String> boms;

    private List<String> removedPlugins;

    private List<String> extensionsWhitelist;

    private List<String> removedTests;

    private List<String> modders;

    private Map<String, String> relocatedCoords;

    private Map<String, String> propertyMappings;

    private String bootstrapLocation;

    private String configLocation;

    private boolean bootstrapRead;

    private File logFile = new File(workspace, "vman.log");

    private static int exitValue = Integer.MIN_VALUE;

    /**
     * The global logger for vman. Set here to prevent it being gc'd.
     */
    private static final java.util.logging.Logger root = java.util.logging.Logger
            .getLogger("com.redhat.rcm.version");

    public static void main(final String[] args) {
        final Cli cli = new Cli();
        final CmdLineParser parser = new CmdLineParser(cli);
        try {
            parser.parseArgument(args);

            final boolean useLog = !(cli.noLogFile || cli.testConfig || /*cli.help ||*/cli.helpModders
                    || cli.showVersion || cli.helpReporters);

            //            System.out.printf( "--no-console: %s \n--no-log-file: %s \n--test-config: %s\n--help: %s\n--help-modifications: %s\n--version: %s\n\nUse logfile? %s\n\n",
            //                               cli.noConsole, cli.noLogFile, cli.testConfig, cli.help, cli.helpModders,
            //                               cli.showVersion, useLog );

            configureLogging(!cli.noConsole, useLog, cli.logFile);
            LoggerFactory.getLogger(Cli.class).info("Testing log appenders...");

            vman = VersionManager.getInstance();

            exitValue = 0;
            if (cli.help) {
                printUsage(parser, null);
            } else if (cli.helpModders) {
                printModders();
            } else if (cli.helpReporters) {
                printReporters();
            } else if (cli.showVersion) {
                printVersionInfo();
            } else if (cli.testConfig) {
                cli.testConfigAndPrintDiags();
            } else {
                exitValue = cli.run();
            }

            if (!cli.noSystemExit) {
                System.exit(exitValue);
            }
        } catch (final CmdLineException error) {
            printUsage(parser, error);
        } catch (final MAEException e) {
            printUsage(parser, e);
        } catch (final MalformedURLException e) {
            printUsage(parser, e);
        }
    }

    public Cli(final File target, final File bomList) {
        this.target = target;
        this.bomList = bomList;
    }

    public Cli() {
    }

    private void testConfigAndPrintDiags() {
        VersionManagerSession session = null;
        final List<VManException> errors = new ArrayList<VManException>();
        try {
            session = initSession();
        } catch (final VManException e) {
            errors.add(e);
        }

        if (session != null) {
            try {
                vman.configureSession(boms, toolchain, session);
            } catch (final VManException e) {
                errors.add(e);
            }
        }

        final FullProjectKey toolchainKey = session == null ? null : session.getToolchainKey();
        final List<FullProjectKey> bomCoords = session == null ? null : session.getBomCoords();

        final LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
        map.put("Bootstrap location:", bootstrapLocation);
        map.put("Bootstrap read?", bootstrapRead);
        map.put("Config location:", configLocation);
        map.put("", "");
        map.put(" ", "");
        map.put("Toolchain location:", toolchain);
        map.put("Toolchain:", toolchainKey);
        map.put("    ", "");
        map.put("BOM locations:", boms);
        map.put("BOMs:", bomCoords);
        map.put("  ", "");
        map.put("   ", "");
        map.put("Settings.xml:", settings);
        map.put("Remote repo:", remoteRepositories);

        System.out.println("Version information:\n-------------------------------------------------\n\n");
        printVersionInfo();
        System.out.printf("Diagnostics:\n-------------------------------------------------\n\n");
        int max = 0;
        for (final String key : map.keySet()) {
            max = Math.max(max, key.length());
        }

        final StringBuilder indent = new StringBuilder();
        for (int i = 0; i < max + 4; i++) {
            indent.append(' ');
        }

        final int descMax = 75 - max;
        final String fmt = "%-" + max + "s    %-" + descMax + "s\n";
        for (final Map.Entry<String, Object> entry : map.entrySet()) {
            final Object value = entry.getValue();

            String val = value == null ? "-NONE-" : String.valueOf(value);
            if (value instanceof Collection<?>) {
                final Collection<?> coll = ((Collection<?>) value);
                if (coll.isEmpty()) {
                    val = "-NONE-";
                } else {
                    val = join(coll, "\n" + indent) + "\n";
                }
            }

            System.out.printf(fmt, entry.getKey(), val);
        }

        System.out.println();
        System.out.printf("Errors:\n-------------------------------------------------\n%s\n\n",
                errors.isEmpty() ? "-NONE" : join(errors, "\n\n"));
        System.out.println();
    }

    private static void configureLogging(boolean useConsole, final boolean useLogFile, final File logFile) {
        System.out.println("Log file is: " + logFile.getAbsolutePath());

        final List<Handler> handlers = new ArrayList<Handler>();

        if (!useConsole && !useLogFile) {
            if (!useLogFile) {
                System.out.println(
                        "\n\nNOTE: --no-console option has been OVERRIDDEN since --no-log-file option was also provided.\nOutputting to console ONLY.\n");
                useConsole = true;
            }
        }

        if (useConsole) {
            final Handler chandler = new ConsoleHandler();
            chandler.setFormatter(new VManFormatter());
            chandler.setLevel(Level.ALL);
            handlers.add(chandler);
        }

        if (useLogFile) {
            try {
                final File dir = logFile.getParentFile();
                if (dir != null && !dir.isDirectory() && !dir.mkdirs()) {
                    throw new RuntimeException(
                            "Failed to create parent directory for logfile: " + dir.getAbsolutePath());
                }
                final Handler fhandler = new FileHandler(logFile.getPath(), false);
                fhandler.setFormatter(new VManFormatter());
                fhandler.setLevel(Level.ALL);
                handlers.add(fhandler);
            } catch (final IOException e) {
                final StringWriter sw = new StringWriter();
                final PrintWriter pw = new PrintWriter(sw);
                e.printStackTrace(pw);
                System.out.printf("ERROR: Failed to initialize log file: %s. Reason: %s\n\n%s\n\n", logFile,
                        e.getMessage(), sw.toString());

                throw new RuntimeException("Failed to initialize logfile.");
            }
        }

        root.setUseParentHandlers(false);
        final Handler[] currenthandlers = root.getHandlers();
        for (final Handler h : currenthandlers) {
            h.close();
            root.removeHandler(h);
        }
        for (final Handler h : handlers) {
            root.addHandler(h);
        }
    }

    public int run() throws MAEException, VManException, MalformedURLException {
        final Logger logger = LoggerFactory.getLogger(getClass());

        final VersionManagerSession session = initSession();

        if (session.getModderKeys().contains("bom-realignment") && (boms == null || boms.isEmpty())) {
            logger.error("You must specify at least one BOM.");
            return -2;
        }

        if (session.getErrors().isEmpty()) {
            logger.info("Modifying POM(s).\n\nTarget:\n\t" + target + "\n\nBOMs:\n\t"
                    + StringUtils.join(boms.iterator(), "\n\t") + "\n\nWorkspace:\n\t" + workspace
                    + "\n\nReports:\n\t" + reports);

            if (target.isDirectory()) {
                vman.modifyVersions(target, pomPattern, pomExcludePattern, boms, toolchain, session);
            } else {
                vman.modifyVersions(target, boms, toolchain, session);
            }
        }

        reports.mkdirs();
        vman.generateReports(reports, session);

        if (capturePom != null && capturePom.exists()) {
            logger.warn("\n\n\n\n\nMissing dependency/plugin information has been captured in:\n\n\t"
                    + capturePom.getAbsolutePath() + "\n\n\n\n");
        }

        final List<Throwable> errors = session.getErrors();
        if (errors != null && !errors.isEmpty()) {
            logger.error(errors.size() + " errors detected!\n\n");

            int i = 1;
            for (final Throwable error : errors) {
                logger.error("\n\n" + i, error);
                i++;
            }

            return -1;
        }

        return 0;
    }

    private VersionManagerSession initSession() throws VManException {
        SSLUtils.initSSLContext(truststorePath);

        loadConfiguration();

        loadBomList();

        loadPlugins();

        loadAndNormalizeModifications();

        final Logger logger = LoggerFactory.getLogger(getClass());
        logger.info("modifications = " + join(modders, " "));

        final SessionBuilder builder = new SessionBuilder(workspace, reports).withVersionSuffix(versionSuffix)
                .withVersionModifier(versionModifier).withRemovedPlugins(removedPlugins)
                .withRemovedTests(removedTests).withExtensionsWhitelist(extensionsWhitelist).withModders(modders)
                .withPreserveFiles(preserveFiles).withStrict(strict).withCoordinateRelocations(relocatedCoords)
                .withPropertyMappings(propertyMappings).withExcludedModulePoms(pomExcludeModules)
                .withUseEffectivePoms(useEffectivePoms).withUserProperties(reportProperties);

        final VersionManagerSession session = builder.build();

        if (remoteRepositories != null) {
            try {
                session.setRemoteRepositories(remoteRepositories);
            } catch (final MalformedURLException e) {
                throw new VManException("Cannot initialize remote repositories: %s. Error: %s", e,
                        remoteRepositories, e.getMessage());
            }
        }

        if (settings != null) {
            session.setSettingsXml(settings);
        }

        if (localRepository == null) {
            localRepository = new File(workspace, "local-repository");
        }

        session.setLocalRepositoryDirectory(localRepository);

        if (capturePom != null) {
            session.setCapturePom(capturePom);
        }

        return session;
    }

    private static void printVersionInfo() {
        final StringBuilder sb = new StringBuilder();
        sb.append(APP_NAME).append("\n\n").append(APP_DESCRIPTION).append("\n\n");

        final LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
        map.put("Built By:", APP_BUILDER);
        map.put("Commit ID:", APP_COMMIT_ID);
        map.put("Built On:", APP_TIMESTAMP);
        map.put("Version:", APP_VERSION);

        sb.append(formatHelpMap(map, "\n"));
        sb.append("\n\n");

        System.out.println(sb.toString());
    }

    private static void printReporters() {
        final Map<String, Report> reports = vman.getReports();

        final List<String> keys = new ArrayList<String>(reports.keySet());
        Collections.sort(keys);

        final int max = maxKeyLength(reports.keySet());
        final int valMax = 75 - max;

        final String headerFmt = "%-" + max + "s    %-" + valMax + "s\n";

        //        final int propMax = valMax - 8;
        //        final String propFmt = "%-" + ( max + 8 ) + "s    %-" + propMax + "s\n";

        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw);

        pw.println("The following project reports are available: \n\n");
        for (final String key : keys) {
            final Report r = reports.get(key);
            printKVLine(key, r.getDescription(), headerFmt, valMax, pw);
            final Map<String, String> props = r.getPropertyDescriptions();

            if (props != null && !props.isEmpty()) {
                pw.println("\n  Properties:\n  ---------------");
                pw.println();

                for (final Entry<String, String> entry : props.entrySet()) {
                    final String pk = entry.getKey();
                    final String value = entry.getValue();

                    pw.println("  - " + REPORT_PROPERTY_PREFIX + key + "." + pk);
                    pw.println();
                    printTextLine(value, "        ", 69, pw);
                }
            }

            pw.println();
            pw.println();
        }

        System.out.println(sw.toString());
    }

    private static void printModders() {
        final Map<String, ProjectModder> modders = vman.getModders();

        final List<String> keys = new ArrayList<String>(modders.keySet());
        Collections.sort(keys);

        final LinkedHashMap<String, Object> props = new LinkedHashMap<String, Object>();
        for (final String key : keys) {
            props.put(key, modders.get(key).getDescription());
        }

        final StringBuilder sb = new StringBuilder();
        sb.append("The following project modifications are available: ");
        sb.append(formatHelpMap(props, "\n\n"));

        sb.append(
                "\n\nNOTE: To ADD any of these modifiers to the standard list, use the notation '--modifications=+<modifier-id>' (prefixed with '+') or for the properties file use 'modifications=+...'.\n\nThe standard modifiers are: ");

        for (final String key : ProjectModder.STANDARD_MODIFICATIONS) {
            sb.append(String.format("\n  - %s", key));
        }

        sb.append("\n\n");

        System.out.println(sb);
    }

    private static String formatHelpMap(final LinkedHashMap<String, Object> map, final String itemSeparator) {
        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw);

        final int max = maxKeyLength(map.keySet());

        final int descMax = 75 - max;
        final String fmt = "%-" + max + "s    %-" + descMax + "s" + itemSeparator;

        for (final Map.Entry<String, Object> entry : map.entrySet()) {
            final String key = entry.getKey();
            final String description = entry.getValue() == null ? "-NONE-" : String.valueOf(entry.getValue());
            printKVLine(key, description, fmt, descMax, pw);
        }

        return sw.toString();
    }

    private static void printTextLine(final String line, final String indent, final int max, final PrintWriter pw) {
        final String fmt = "%s%-" + max + "s\n";

        final List<String> lines = new ArrayList<String>();

        final BreakIterator iter = BreakIterator.getLineInstance();
        iter.setText(line);

        int start = iter.first();
        int end = BreakIterator.DONE;
        final StringBuilder currentLine = new StringBuilder();
        String seg;
        while (start != BreakIterator.DONE && (end = iter.next()) != BreakIterator.DONE) {
            seg = line.substring(start, end);
            if (currentLine.length() + seg.length() > max) {
                lines.add(currentLine.toString());
                currentLine.setLength(0);
            }

            currentLine.append(seg);
            start = end;
        }

        if (currentLine.length() > 0) {
            lines.add(currentLine.toString());
        }

        for (final String ln : lines) {
            pw.printf(fmt, indent, ln);
        }
    }

    private static void printKVLine(final String key, final String value, final String fmt, final int valMax,
            final PrintWriter pw) {
        final List<String> lines = new ArrayList<String>();

        final BreakIterator iter = BreakIterator.getLineInstance();
        iter.setText(value);

        int start = iter.first();
        int end = BreakIterator.DONE;
        final StringBuilder currentLine = new StringBuilder();
        String seg;
        while (start != BreakIterator.DONE && (end = iter.next()) != BreakIterator.DONE) {
            seg = value.substring(start, end);
            if (currentLine.length() + seg.length() > valMax) {
                lines.add(currentLine.toString());
                currentLine.setLength(0);
            }

            currentLine.append(seg);
            start = end;
        }

        if (currentLine.length() > 0) {
            lines.add(currentLine.toString());
        }

        pw.printf(fmt, key, lines.isEmpty() ? "" : lines.get(0));
        if (lines.size() > 1) {
            for (int i = 1; i < lines.size(); i++) {
                // blank string to serve for indentation in format with two fields.
                pw.printf(fmt, "", lines.get(i));
            }
        }
    }

    private static int maxKeyLength(final Set<String> keys) {
        int max = 0;
        for (final String key : keys) {
            max = Math.max(max, key.length());
        }

        return max;
    }

    private void loadPlugins() {
        if (extensionsWhitelistList == null && extensionsWhitelistList != null) {
            final String[] ls = extensionsWhitelistList.split("\\s*,\\s*");
            extensionsWhitelist = Arrays.asList(ls);
        }
        if (removedPlugins == null && removedPluginsList != null) {
            final String[] ls = removedPluginsList.split("\\s*,\\s*");
            removedPlugins = Arrays.asList(ls);
        }
        if (removedTests == null && removedTestsList != null) {
            final String[] ls = removedTestsList.split("\\s*,\\s*");
            removedTests = Arrays.asList(ls);
        }
    }

    private void loadAndNormalizeModifications() {
        if (modifications != null) {
            final String[] ls = modifications.split("\\s*,\\s*");
            modders = new ArrayList<String>();
            for (final String modder : ls) {
                if (!modders.contains(modder)) {
                    modders.add(modder);
                }
            }
        }

        final List<String> mods = new ArrayList<String>();
        boolean loadStandards = modders == null;
        if (modders != null) {
            if (!modders.isEmpty() && modders.iterator().next().startsWith("+")) {
                loadStandards = true;
            }

            for (final String key : modders) {
                if (ProjectModder.STANDARD_MODS_ALIAS.equals(key)) {
                    loadStandards = true;
                } else if (key.startsWith("+")) {
                    if (key.length() > 1) {
                        mods.add(key.substring(1).trim());
                    }
                } else {
                    mods.add(key);
                }
            }
        }

        if (loadStandards) {
            mods.addAll(Arrays.asList(ProjectModder.STANDARD_MODIFICATIONS));
        }

        modders = mods;
    }

    private void loadConfiguration() throws VManException {
        final Logger logger = LoggerFactory.getLogger(getClass());

        File config = null;

        if (configuration != null) {
            config = InputUtils.getFile(configuration, workspace);
        }

        if (config == null) {
            config = loadBootstrapConfig();
        }

        if (config == null) {
            configLocation = DEFAULT_CONFIG_FILE.getAbsolutePath();
            config = DEFAULT_CONFIG_FILE;
        }

        if (config != null && config.canRead()) {
            InputStream is = null;
            try {
                is = new FileInputStream(config);
                final Properties props = new Properties();
                props.load(is);

                final StringWriter sWriter = new StringWriter();
                for (final Enumeration<?> e = props.propertyNames(); e.hasMoreElements();) {
                    final String key = (String) e.nextElement();
                    sWriter.write("  ");
                    sWriter.write(key);
                    sWriter.write(" = ");
                    sWriter.write(props.getProperty(key));
                    sWriter.write("\n");
                }

                props.list(new PrintWriter(sWriter));

                logger.info("Loading configuration from: " + config + ":\n\n" + sWriter);

                final File downloadsDir = VersionManagerSession.getDownloadsDir(workspace);
                final List<String> relocations = readListProperty(props, RELOCATIONS_PROPERTY);
                if (relocations != null) {
                    relocatedCoords = readPropertiesList(relocations, downloadsDir, true);
                } else {
                    relocatedCoords = new HashMap<String, String>();
                }

                final List<String> mappingsLocations = readListProperty(props, PROPERTY_MAPPINGS_PROPERTY);
                if (mappingsLocations != null) {
                    this.propertyMappings = readPropertiesList(mappingsLocations, downloadsDir, true);
                } else {
                    this.propertyMappings = new HashMap<String, String>();
                }

                if (removedPluginsList == null) {
                    removedPlugins = readListProperty(props, REMOVED_PLUGINS_PROPERTY);
                }

                if (removedTestsList == null) {
                    removedTests = readListProperty(props, REMOVED_TESTS_PROPERTY);
                }

                if (extensionsWhitelistList == null) {
                    extensionsWhitelist = readListProperty(props, EXTENSIONS_WHITELIST_PROPERTY);
                }

                if (pomExcludeModules == null) {
                    pomExcludeModules = props.getProperty(POM_EXCLUDE_MODULE_PROPERTY);
                }

                if (pomExcludePattern == null) {
                    pomExcludePattern = props.getProperty(POM_EXCLUDE_FILE_PROPERTY);
                }

                if (modifications == null) {
                    final List<String> lst = readListProperty(props, MODIFICATIONS);
                    logger.info("modifications from properties: '" + join(lst, " ") + "'");
                    if (lst != null) {
                        modders = modders == null ? new ArrayList<String>() : new ArrayList<String>(modders);
                        modders.addAll(lst);
                    }
                }

                if (bomList == null) {
                    if (boms == null) {
                        boms = new ArrayList<String>();
                    }

                    final List<String> pBoms = readListProperty(props, BOMS_LIST_PROPERTY);
                    if (pBoms != null) {
                        boms.addAll(pBoms);
                    }
                }

                if (toolchain == null) {
                    toolchain = props.getProperty(TOOLCHAIN_PROPERTY);
                    if (toolchain != null) {
                        toolchain = toolchain.trim();
                    }
                }

                if (versionSuffix == null) {
                    versionSuffix = props.getProperty(VERSION_SUFFIX_PROPERTY);
                    if (versionSuffix != null) {
                        versionSuffix = versionSuffix.trim();
                    }
                }

                if (versionModifier == null) {
                    versionModifier = props.getProperty(VERSION_MODIFIER_PROPERTY);
                    if (versionModifier != null) {
                        versionModifier = versionModifier.trim();
                    }
                }

                if (remoteRepositories == null) {
                    remoteRepositories = props.getProperty(REMOTE_REPOSITORIES_PROPERTY);
                    if (remoteRepositories != null) {
                        remoteRepositories = remoteRepositories.trim();
                    } else {
                        remoteRepositories = props.getProperty(REMOTE_REPOSITORY_PROPERTY);

                        if (remoteRepositories != null) {
                            logger.warn("Using deprecated " + REMOTE_REPOSITORY_PROPERTY);
                            remoteRepositories = remoteRepositories.trim();
                        }
                    }
                }

                if (settings == null) {
                    final String s = props.getProperty(SETTINGS_PROPERTY);
                    if (s != null) {
                        settings = s;
                    }
                }

                if (localRepository == null) {
                    final String l = props.getProperty(LOCAL_REPOSITORY_PROPERTY);
                    if (l != null) {
                        localRepository = new File(l);
                    }
                }

                if (capturePom == null) {
                    final String p = props.getProperty(CAPTURE_POM_PROPERTY);
                    if (p != null) {
                        capturePom = new File(p);
                    }
                }

                if (!strict) {
                    strict = Boolean
                            .valueOf(props.getProperty(STRICT_MODE_PROPERTY, Boolean.toString(Boolean.FALSE)));
                }

                if (!useEffectivePoms) {
                    useEffectivePoms = Boolean.valueOf(
                            props.getProperty(USE_EFFECTIVE_POMS_PROPERTY, Boolean.toString(Boolean.FALSE)));
                }

                if (truststorePath == null) {
                    truststorePath = props.getProperty(TRUSTSTORE_PATH_PROPERTY);
                }

                final Map<String, String> userProps = new HashMap<String, String>();
                for (final Enumeration<?> keys = props.keys(); keys.hasMoreElements();) {
                    final String key = (String) keys.nextElement();
                    if (key.startsWith(REPORT_PROPERTY_PREFIX)) {
                        userProps.put(key.substring(REPORT_PROPERTY_PREFIX.length()), props.getProperty(key));
                    }
                }

                if (!userProps.isEmpty()) {
                    if (reportProperties == null) {
                        reportProperties = userProps;
                    } else {
                        userProps.putAll(reportProperties);
                        reportProperties = userProps;
                    }
                }
            } catch (final IOException e) {
                throw new VManException("Failed to load configuration from: " + config, e);
            } finally {
                closeQuietly(is);
            }
        } else {
            configLocation = "command-line";
        }
    }

    /**
     * Try to load bootstrap configuration using the following order or preference:
     * 1. configured file (using -B option)
     * 2. embedded resource (classpath:bootstrap.properties)
     * 3. default file ($HOME/.vman.boot.properties)
     *
     * @return The configuration file referenced by the bootstrap properties, or null if no bootstrap properties is
     *         found.
     *
     * @throws VManException In cases where the specified bootstrap properties file is unreadable.
     */
    private File loadBootstrapConfig() throws VManException {
        final Logger logger = LoggerFactory.getLogger(getClass());

        Map<String, String> bootProps = null;
        if (bootstrapConfig == null) {
            logger.info("Reading bootstrap info from classpath resource: " + BOOTSTRAP_PROPERTIES);
            final URL resource = getClasspathResource(BOOTSTRAP_PROPERTIES);
            if (resource != null) {
                bootstrapLocation = "classpath:" + resource;

                bootProps = readClasspathProperties(BOOTSTRAP_PROPERTIES);
            } else if (DEFAULT_BOOTSTRAP_CONFIG.exists() && DEFAULT_BOOTSTRAP_CONFIG.canRead()) {
                logger.info("Reading bootstrap info from: " + DEFAULT_BOOTSTRAP_CONFIG);
                bootstrapLocation = "file:" + DEFAULT_BOOTSTRAP_CONFIG.getAbsolutePath();

                bootProps = readProperties(DEFAULT_BOOTSTRAP_CONFIG);
            }
        } else {
            if (!bootstrapConfig.exists() || !bootstrapConfig.canRead()) {
                throw new VManException("Cannot read bootstrap from: " + bootstrapConfig);
            } else {
                logger.info("Reading bootstrap info from: " + bootstrapConfig);
                bootstrapLocation = "file:" + bootstrapConfig.getAbsolutePath();

                bootProps = readProperties(bootstrapConfig);
            }
        }

        bootstrapRead = bootProps != null;

        if (bootProps != null) {
            configLocation = bootProps.get(BOOT_CONFIG_PROPERTY);
            if (configLocation != null) {
                logger.info("Reading configuration from: " + configLocation);
                try {
                    final File file = getFile(configLocation, new File(System.getProperty("java.io.tmpdir")), true);

                    logger.info("...downloaded to file: " + file);
                    return file;
                } catch (final VManException e) {
                    logger.error("Failed to download configuration from: " + configLocation + ". Reason: "
                            + e.getMessage(), e);
                    throw e;
                }
            }
        }

        return null;
    }

    private void loadBomList() throws VManException {
        if (boms == null) {
            boms = new ArrayList<String>();
        }

        if (bomList != null && bomList.canRead()) {
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new FileReader(bomList));
                String line = null;
                while ((line = reader.readLine()) != null) {
                    boms.add(line.trim());
                }
            } catch (final IOException e) {
                throw new VManException("Failed to read bom list from: " + bomList, e);
            } finally {
                closeQuietly(reader);
            }
        }
    }

    private static void printUsage(final CmdLineParser parser, final Exception error) {
        if (error != null) {
            System.err.println("Invalid option(s): " + error.getMessage());
            System.err.println();
        }

        System.err.println("Usage: $0 [OPTIONS] [<target-path>]");
        System.err.println();
        System.err.println();
        // If we are running under a Linux shell COLUMNS might be available for the width
        // of the terminal.
        parser.setUsageWidth((System.getenv("COLUMNS") == null ? 100 : Integer.valueOf(System.getenv("COLUMNS"))));
        parser.printUsage(System.err);
        System.err.println();
    }

    public File getTarget() {
        return target;
    }

    public File getBomList() {
        return bomList;
    }

    public File getBootstrapConfig() {
        return bootstrapConfig;
    }

    public String getConfiguration() {
        return configuration;
    }

    public String getPomExcludePattern() {
        return pomExcludePattern;
    }

    public String getPomExcludeModules() {
        return pomExcludeModules;
    }

    public boolean isHelp() {
        return help;
    }

    public boolean isHelpModders() {
        return helpModders;
    }

    public boolean isNoConsole() {
        return noConsole;
    }

    public boolean isNoLogFile() {
        return noLogFile;
    }

    public File getLocalRepository() {
        return localRepository;
    }

    public String getRemoteRepositories() {
        return remoteRepositories;
    }

    public String getModifications() {
        return modifications;
    }

    public File getCapturePom() {
        return capturePom;
    }

    public String getPomPattern() {
        return pomPattern;
    }

    public boolean isPreserveFiles() {
        return preserveFiles;
    }

    public String getRemovedPluginsList() {
        return removedPluginsList;
    }

    public String getRemovedTestsList() {
        return removedTestsList;
    }

    public String getExtensionsWhitelistList() {
        return extensionsWhitelistList;
    }

    public File getReports() {
        return reports;
    }

    public String getVersionSuffix() {
        return versionSuffix;
    }

    public String getVersionModifier() {
        return versionModifier;
    }

    public boolean isStrict() {
        return strict;
    }

    public String getSettings() {
        return settings;
    }

    public String getToolchain() {
        return toolchain;
    }

    public boolean isTestConfig() {
        return testConfig;
    }

    public Map<String, String> getUserProperties() {
        return reportProperties;
    }

    public boolean isShowVersion() {
        return showVersion;
    }

    public File getWorkspace() {
        return workspace;
    }

    public boolean isNoSystemExit() {
        return noSystemExit;
    }

    public String getTruststorePath() {
        return truststorePath;
    }

    public boolean isUseEffectivePoms() {
        return useEffectivePoms;
    }

    public List<String> getBoms() {
        return boms;
    }

    public List<String> getRemovedPlugins() {
        return removedPlugins;
    }

    public List<String> getExtensionsWhitelist() {
        return extensionsWhitelist;
    }

    public List<String> getRemovedTests() {
        return removedTests;
    }

    public List<String> getModders() {
        return modders;
    }

    public Map<String, String> getRelocatedCoords() {
        return relocatedCoords;
    }

    public Map<String, String> getPropertyMappings() {
        return propertyMappings;
    }

    public String getBootstrapLocation() {
        return bootstrapLocation;
    }

    public String getConfigLocation() {
        return configLocation;
    }

    public boolean isBootstrapRead() {
        return bootstrapRead;
    }

    public File getLogFile() {
        return logFile;
    }

    public void setTarget(final File target) {
        this.target = target;
    }

    public void setBomList(final File bomList) {
        this.bomList = bomList;
    }

    public void setBootstrapConfig(final File bootstrapConfig) {
        this.bootstrapConfig = bootstrapConfig;
    }

    public void setConfiguration(final String configuration) {
        this.configuration = configuration;
    }

    public void setPomExcludePattern(final String pomExcludePattern) {
        this.pomExcludePattern = pomExcludePattern;
    }

    public void setPomExcludeModules(final String pomExcludeModules) {
        this.pomExcludeModules = pomExcludeModules;
    }

    public void setHelp(final boolean help) {
        this.help = help;
    }

    public void setHelpModders(final boolean helpModders) {
        this.helpModders = helpModders;
    }

    public void setNoConsole(final boolean noConsole) {
        this.noConsole = noConsole;
    }

    public void setNoLogFile(final boolean noLogFile) {
        this.noLogFile = noLogFile;
    }

    public void setLocalRepository(final File localRepository) {
        this.localRepository = localRepository;
    }

    public void setRemoteRepositories(final String remoteRepositories) {
        this.remoteRepositories = remoteRepositories;
    }

    public void setModifications(final String modifications) {
        this.modifications = modifications;
    }

    public void setCapturePom(final File capturePom) {
        this.capturePom = capturePom;
    }

    public void setPomPattern(final String pomPattern) {
        this.pomPattern = pomPattern;
    }

    public void setPreserveFiles(final boolean preserveFiles) {
        this.preserveFiles = preserveFiles;
    }

    public void setRemovedPluginsList(final String removedPluginsList) {
        this.removedPluginsList = removedPluginsList;
    }

    public void setRemovedTestsList(final String removedTestsList) {
        this.removedTestsList = removedTestsList;
    }

    public void setExtensionsWhitelistList(final String extensionsWhitelistList) {
        this.extensionsWhitelistList = extensionsWhitelistList;
    }

    public void setReports(final File reports) {
        this.reports = reports;
    }

    public void setVersionSuffix(final String versionSuffix) {
        this.versionSuffix = versionSuffix;
    }

    public void setVersionModifier(final String versionModifier) {
        this.versionModifier = versionModifier;
    }

    public void setStrict(final boolean strict) {
        this.strict = strict;
    }

    public void setSettings(final String settings) {
        this.settings = settings;
    }

    public void setToolchain(final String toolchain) {
        this.toolchain = toolchain;
    }

    public void setTestConfig(final boolean testConfig) {
        this.testConfig = testConfig;
    }

    public void setUserProperties(final Map<String, String> userProperties) {
        this.reportProperties = userProperties;
    }

    public void setShowVersion(final boolean showVersion) {
        this.showVersion = showVersion;
    }

    public void setWorkspace(final File workspace) {
        this.workspace = workspace;
    }

    public void setNoSystemExit(final boolean noSystemExit) {
        this.noSystemExit = noSystemExit;
    }

    public void setTruststorePath(final String truststorePath) {
        this.truststorePath = truststorePath;
    }

    public void setUseEffectivePoms(final boolean useEffectivePoms) {
        this.useEffectivePoms = useEffectivePoms;
    }

    public void setBoms(final List<String> boms) {
        this.boms = boms;
    }

    public void setRemovedPlugins(final List<String> removedPlugins) {
        this.removedPlugins = removedPlugins;
    }

    public void setExtensionsWhitelist(final List<String> extensionsWhitelist) {
        this.extensionsWhitelist = extensionsWhitelist;
    }

    public void setRemovedTests(final List<String> removedTests) {
        this.removedTests = removedTests;
    }

    public void setModders(final List<String> modders) {
        this.modders = modders;
    }

    public void setRelocatedCoords(final Map<String, String> relocatedCoords) {
        this.relocatedCoords = relocatedCoords;
    }

    public void setPropertyMappings(final Map<String, String> propertyMappings) {
        this.propertyMappings = propertyMappings;
    }

    public void setBootstrapLocation(final String bootstrapLocation) {
        this.bootstrapLocation = bootstrapLocation;
    }

    public void setConfigLocation(final String configLocation) {
        this.configLocation = configLocation;
    }

    public void setBootstrapRead(final boolean bootstrapRead) {
        this.bootstrapRead = bootstrapRead;
    }

    public void setLogFile(final File logFile) {
        this.logFile = logFile;
    }
}