com.buildml.main.BMLAdminMain.java Source code

Java tutorial

Introduction

Here is the source code for com.buildml.main.BMLAdminMain.java

Source

/*******************************************************************************
 * Copyright (c) 2012 Arapiki Solutions Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    "Peter Smith <psmith@arapiki.com>" - initial API and 
 *        implementation and/or initial documentation
 *******************************************************************************/

package com.buildml.main;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import org.apache.commons.cli.*;

import com.buildml.main.commands.*;
import com.buildml.model.BuildStoreFactory;
import com.buildml.model.BuildStoreVersionException;
import com.buildml.model.IBuildStore;
import com.buildml.utils.print.PrintUtils;
import com.buildml.utils.string.StringArray;
import com.buildml.utils.version.Version;

/**
 * The main entry point for the "bml" command line program. All other parts of the code 
 * (with the exception of the Eclipse plug-in) are invoked by this code.
 * 
 * @author "Peter Smith <psmith@arapiki.com>"
 */
public final class BMLAdminMain {

    /*=====================================================================================*
     * TYPES/FIELDS
     *=====================================================================================*/

    /** The file name to use for the BuildStore database (defaults to "build.bml"). */
    private String buildStoreFileName = "build.bml";

    /** Set if the user selected the -h option. */
    private boolean optionHelp = false;

    /** Set if the user selected the -v option. */
    private boolean optionVersion = false;

    /** The global command line options, as managed by the Apache Commons CLI library. */
    private Options globalOpts = null;

    /**
     * All CLI command are "plugged into" the CliMain class, from where they can then
     * be invoked. The CommandGroup class is used to encapsulate logical groups of commands.
     * For example, one group could be all the commands that display FileSet listings.
     */
    private class CommandGroup {

        /** The title of the command group (e.g. "Commands for displaying action information") */
        String groupHeading;

        /** The commands that fall within this group */
        ICliCommand commands[];
    }

    /** The list of command groups that are registered with CliMain. */
    private ArrayList<CommandGroup> commandGroups = null;

    /*=====================================================================================*
     * PRIVATE METHODS
     *=====================================================================================*/

    /**
     * Create a new CliMain instance. This should only be done once, from the standard
     * Java main() function.
     */
    private BMLAdminMain() {
        /* empty */
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Process the global command line arguments, using the Apache Commons CLI library.
     * Global options are defined as being those arguments that appear before the sub-command
     * name. They are distinct from "command options" that are specific to each sub-command.
     * 
     * @param args The standard command line array from the main() method.
     * @return The remaining command line arguments (with global options excluded), with the first
     * being the command name.
     */
    private String[] processGlobalOptions(String[] args) {

        /* create a new Apache Commons CLI parser, using Posix style arguments */
        CommandLineParser parser = new PosixParser();

        /* define the bml command's arguments */
        globalOpts = new Options();

        /* add the -f / --file option */
        Option fOpt = new Option("f", "file", true, "Name of build database to query/edit");
        fOpt.setArgName("file-name");
        globalOpts.addOption(fOpt);

        /* add the -h / --help option */
        Option hOpt = new Option("h", "help", false, "Show this help information");
        globalOpts.addOption(hOpt);

        /* add the -v / --version option */
        Option vOpt = new Option("v", "version", false, "Show version information");
        globalOpts.addOption(vOpt);

        /* how many columns of output should we show (default is 80) */
        Option widthOpt = new Option("w", "width", true,
                "Number of output columns in reports (default is " + CliUtils.getColumnWidth() + ")");
        globalOpts.addOption(widthOpt);

        /*
         * Initiate the parsing process - also, report on any options that require
         * an argument but didn't receive one. We only want to parse arguments
         * up until the first non-argument (that doesn't start with -).
         */
        CommandLine line = null;
        try {
            line = parser.parse(globalOpts, args, true);

        } catch (ParseException e) {
            displayHelpAndExit(e.getMessage());
        }

        /*
         * Validate all the options and their argument values.
         */
        if (line.hasOption('f')) {
            buildStoreFileName = line.getOptionValue('f');
        }

        if (line.hasOption('h')) {
            optionHelp = true;
        }

        if (line.hasOption('v')) {
            optionVersion = true;
        }

        String argWidth = line.getOptionValue("width");
        if (argWidth != null) {
            try {
                int newWidth = Integer.valueOf(argWidth);
                CliUtils.setColumnWidth(newWidth);
            } catch (NumberFormatException ex) {
                CliUtils.reportErrorAndExit("Invalid argument to --width: " + argWidth);
            }
        }

        /* 
         * Return the array of arguments that come after the global option. This
         * includes the sub-command name, any sub-command options, and the sub-command's
         * arguments.
         */
        return line.getArgs();
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Display a set of options (as defined by the Options class). This methods is used
     * in displaying the help text.
     * @param opts A set of command line options, as defined by the Options class.
     */
    @SuppressWarnings("unchecked")
    private void displayOptions(Options opts) {

        /* obtain a list of all the options */
        Collection<Option> optList = opts.getOptions();

        /* if there are no options for this command ... */
        if (optList.size() == 0) {
            System.err.println("    No options available.");
        }

        /* 
         * Else, we have options to display. Show them in a nicely tabulated
         * format, with the short option name (e.g. -p) and the long option name
         * (--show-pkgs) first, followed by a text description of the option.
         */
        else {
            for (Iterator<Option> iterator = optList.iterator(); iterator.hasNext();) {
                Option thisOpt = iterator.next();
                String shortOpt = thisOpt.getOpt();
                String longOpt = thisOpt.getLongOpt();
                String line = "    ";
                if (shortOpt != null) {
                    line += "-" + shortOpt;
                } else {
                    line += "  ";
                }
                if (shortOpt != null && longOpt != null) {
                    line += " | ";
                } else {
                    line += "   ";
                }
                if (longOpt != null) {
                    line += "--" + thisOpt.getLongOpt();
                }
                if (thisOpt.hasArg()) {
                    line += " <" + thisOpt.getArgName() + ">";
                }
                formattedDisplayLine(line, thisOpt.getDescription());
            }
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Display a formatted line in the help output. This is a helper method used for lining 
     * up the columns in the help text.
     * @param leftCol The text in the left column.
     * @param rightCol The text in the right column.
     */
    private void formattedDisplayLine(String leftCol, String rightCol) {
        System.err.print(leftCol);
        PrintUtils.indent(System.err, 40 - leftCol.length());
        System.err.println(rightCol);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Display the main help text for the "bml" command. The includes the global command line
     * options and the list of sub-commands. Note: this method never returns, instead the whole
     * program is aborted. Optionally, an additional text message will be displayed to clarify
     * the error.
     * @param message A special purpose string error message.
     */
    private void displayHelpAndExit(String message) {
        System.err.println("\nUsage: bml [ global-options ] command [ command-options ] arg, ...");
        System.err.println("\nOptions for all commands:");

        /* display the global command options */
        displayOptions(globalOpts);

        /* display a summary of all the sub-commands (in their respective groups) */
        for (CommandGroup group : commandGroups) {
            System.err.println("\n" + group.groupHeading + ":");
            for (int i = 0; i < group.commands.length; i++) {
                ICliCommand cmd = group.commands[i];
                formattedDisplayLine("    " + cmd.getName(), cmd.getShortDescription());
            }
        }

        System.err.println("\nFor more help, use bml -h <command-name>\n");

        /* if the caller supplied a message, display it */
        CliUtils.reportErrorAndExit(message);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Display detailed help text about a specific CLI command. The output from this
     * command may be hundreds of lines long, depending on the length of the command-specific
     * help message. Note that this method aborts the whole program, and never returns.
     * 
     * @param cmd The CLI command to display detailed information about.
     */
    private void displayDetailedHelpAndExit(ICliCommand cmd) {

        System.err.println("\nSynopsis:\n\n    " + cmd.getName() + " - " + cmd.getShortDescription());
        System.err.println("\nSyntax:\n\n    " + cmd.getName() + " " + cmd.getParameterDescription());

        System.err.println("\nOptions:\n");
        displayOptions(cmd.getOptions());

        System.err.println("\nDescription:\n");
        String longHelp = cmd.getLongDescription();
        if (longHelp != null) {
            /* indent by 4, we'll manage the wrapping ourselves */
            PrintUtils.indentAndWrap(System.err, longHelp, 4, 1000);
        } else {
            System.err.println("    No detailed help available for this command.");
        }
        System.err.println();

        /* exit, with no particular error message */
        CliUtils.reportErrorAndExit(null);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Register a group of CLI commands. This is a helper method for registerCommands().
     * @param heading The title to be printed at the start of this group of commands.
     * @param commandList An array of commands to be added into this group.
     */
    private void registerCommandGroup(String heading, ICliCommand[] commandList) {

        /* 
         * A command group is essentially a structure with a title/heading an array of
         * ICliCommand entries.
         */
        CommandGroup newGrp = new CommandGroup();
        newGrp.groupHeading = heading;
        newGrp.commands = commandList;

        commandGroups.add(newGrp);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Register all the CliCommand* classes so that their commands can be executed. We want
     * our help page to be meaningful, so we add the commands in groups. Any new CLI commands
     * should be added here (and only here).
     */
    private void registerCommands() {

        /* we'll record all the command groups in a list */
        commandGroups = new ArrayList<CommandGroup>();

        registerCommandGroup("Commands for managing the build.bml database file",
                new ICliCommand[] { new CliCommandCreate(), new CliCommandUpgrade() });

        registerCommandGroup("Commands for scanning builds and build trees", new ICliCommand[] {
                new CliCommandScanTree(), new CliCommandScanEaAnno(), new CliCommandScanBuild() });

        registerCommandGroup("Commands for displaying file/path information",
                new ICliCommand[] { new CliCommandShowFiles(), new CliCommandShowUnusedFiles(),
                        new CliCommandShowWriteOnlyFiles(), new CliCommandShowPopularFiles(),
                        new CliCommandShowDerivedFiles(), new CliCommandShowInputFiles(),
                        new CliCommandShowFilesUsedBy() });

        registerCommandGroup("Commands for displaying action information",
                new ICliCommand[] { new CliCommandShowActions(), new CliCommandShowActionsThatUse() });

        registerCommandGroup("Commands for managing file system roots",
                new ICliCommand[] { new CliCommandShowRoot(), new CliCommandSetPackageRoot(),
                        new CliCommandSetWorkspaceRoot(), new CliCommandSetBuildMLFileDepth() });

        registerCommandGroup("Commands for managing packages",
                new ICliCommand[] { new CliCommandShowPkg(), new CliCommandAddPkg(), new CliCommandRemovePkg(),
                        new CliCommandMovePkg(), new CliCommandRenamePkg(), new CliCommandSetFilePkg(),
                        new CliCommandSetActionPkg() });

        registerCommandGroup("Commands for modifying the build system", new ICliCommand[] { new CliCommandRmFile(),
                new CliCommandMakeAtomic(), new CliCommandMergeActions(), new CliCommandRmAction() });
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Given a CLI command name, find the associated ICliCommand object that describes the
     * command.
     * @param cmdName The name of the CLI command, as entered by the user on the command line.
     * @return The associated ICliCommand object, or null if the command wasn't recognized.
     */
    private ICliCommand findCommand(String cmdName) {

        /* 
         * Given the small number of commands, we can do a linear search
         * through the command groups and commands.
         */
        for (CommandGroup group : commandGroups) {
            for (ICliCommand cmd : group.commands) {
                if (cmdName.equals(cmd.getName())) {
                    return cmd;
                }
            }
        }

        /* command not found, return null */
        return null;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * This is the main entry point for the bml command. This method parses the global
     * command line arguments, determines which sub-command is being invoked, parses that
     * command's options, then invokes the command. 
     * 
     * @param args Standard Java command line arguments - passed to us by the 
     * "bml" shell script.
     */
    private void invokeCommand(String[] args) {

        /* register all the sub-command classes */
        registerCommands();

        /* Process global command line options */
        String cmdArgs[] = processGlobalOptions(args);

        /* 
         * If the user types "bml -h" with no other arguments, show the general help page.
         * Also, if the user doesn't provide a sub-command name, show the same help page,
         * but also with an error message.
         */
        if (cmdArgs.length == 0) {
            if (optionVersion) {
                System.out.println(Version.getVersion());
                CliUtils.reportErrorAndExit(null);
            }
            if (optionHelp) {
                displayHelpAndExit(null);
            } else {
                displayHelpAndExit("Missing command - please specify an operation to perform.");
            }
        }

        /* what's the command's name? If it begins with '-', this means we have unparsed options! */
        String cmdName = cmdArgs[0];
        if (cmdName.startsWith("-")) {
            CliUtils.reportErrorAndExit("Unrecognized global option " + cmdName);
        }
        cmdArgs = StringArray.shiftLeft(cmdArgs);

        /* find the appropriate command object (if it exists) */
        ICliCommand cmd = findCommand(cmdName);
        if (cmd == null) {
            CliUtils.reportErrorAndExit("Unrecognized command - \"" + cmdName + "\"");
        }

        /* Does the user want help with this command? */
        if (optionHelp) {
            displayDetailedHelpAndExit(cmd);
        }

        /*
         * Open the build store file, or for the "create" command, we create it. For
         * "upgrade" we do neither.
         */
        IBuildStore buildStore = null;
        try {
            if (cmd.getName().equals("create")) {
                buildStore = BuildStoreFactory.createBuildStore(buildStoreFileName);
            } else if (!cmd.getName().equals("upgrade")) {
                buildStore = BuildStoreFactory.openBuildStore(buildStoreFileName);
            }
        } catch (FileNotFoundException ex) {
            CliUtils.reportErrorAndExit(ex.getMessage());
        } catch (IOException ex) {
            CliUtils.reportErrorAndExit(ex.getMessage());
        } catch (BuildStoreVersionException ex) {
            CliUtils.reportErrorAndExit(ex.getMessage());
        }

        /*
         * Fetch the command's command line options (Options object) which
         * is then used to parse the user-provided arguments.
         */
        CommandLineParser parser = new PosixParser();
        CommandLine cmdLine = null;
        Options cmdOpts = cmd.getOptions();
        try {
            cmdLine = parser.parse(cmdOpts, cmdArgs, true);
        } catch (ParseException e) {
            CliUtils.reportErrorAndExit(e.getMessage());
        }
        cmd.processOptions(buildStore, cmdLine);

        /*
         * Check for unprocessed command options. That is, if the first
         * non-option argument starts with '-', then it's actually an
         * unprocessed option.
         */
        String remainingArgs[] = cmdLine.getArgs();
        if (remainingArgs.length > 0) {
            String firstArg = remainingArgs[0];
            if (firstArg.startsWith("-")) {
                CliUtils.reportErrorAndExit("Unrecognized option " + firstArg);
            }
        }

        /*
         * Now, invoke the command. If the invoke() method wants to, it may completely
         * exit from the program. This is the typical case when an error is reported
         * via the CliUtils.reportErrorAndExit() method.
         */
        cmd.invoke(buildStore, buildStoreFileName, remainingArgs);

        /* release resources */
        if (buildStore != null) {
            buildStore.close();
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * The standard Java main() function, which does its work by delegating to invokeCommand().
     * We also use this opportunity to catch and report any stray Exceptions/Errors.
     * 
     * @param args The standard command line argument array.
     */
    public static void main(String[] args) {

        /*
         * We wrap everything in a global "try", to catch any uncaught Exception
         * exceptions that might be thrown. This is a catch all for all errors and
         * exceptions that don't get caught anywhere else. We'll do our best to 
         * display a meaningful error message.
         */
        try {
            new BMLAdminMain().invokeCommand(args);

        } catch (Throwable e) {
            System.err.println("\n============================================================\n");
            System.err.println("Error: Unexpected software problem, which was probably an internal");
            System.err.println("programming error, rather than something you did wrong. Please cut");
            System.err.println("and paste the following error and email it to bugs@buildml.com,");
            System.err.println("along with a description of what you were doing at the time.");
            System.err.println("\n============================================================\n");
            System.err.println(Version.getVersion());
            System.err.println("Java version is " + System.getProperty("java.vendor") + " "
                    + System.getProperty("java.version"));
            System.err.println("Operating system version is " + System.getProperty("os.name") + " "
                    + System.getProperty("os.version"));
            e.printStackTrace(System.err);
            System.err.println("\n============================================================");
            System.exit(1);
        }
    }

    /*-------------------------------------------------------------------------------------*/
}