/*******************************************************************************
* 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);
}
}
/*-------------------------------------------------------------------------------------*/
}