org.protempa.cli.CLI Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of protempa-framework Show documentation
Show all versions of protempa-framework Show documentation
Protempa Framework is the core of Protempa.
/*
* #%L
* Protempa Framework
* %%
* Copyright (C) 2012 - 2013 Emory University
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package org.protempa.cli;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.lang3.StringUtils;
import org.protempa.Protempa;
import org.protempa.CloseException;
import org.protempa.ProtempaStartupException;
/**
* Convenience class for creating command line applications using PROTEMPA.
*
* @author Andrew Post
*/
public abstract class CLI {
/**
* For defining command line arguments that are not prefixed by the option
* syntax.
*/
public static final class Argument {
private final String name;
private final boolean required;
/**
* Instantiates an instance with the name of the argument and
* whether or not it is required.
*
* @param name the argument's name, a {@link String}.
* @param required true
if the argument is required,
* false
if not.
*/
public Argument(String name, boolean required) {
if (name == null || name.trim().length() == 0) {
throw new IllegalArgumentException(
"name cannot be null or empty");
}
this.name = name;
this.required = required;
}
/**
* Returns the argument's name.
*
* @return a {@link String}.
*/
public String getName() {
return this.name;
}
/**
* Returns whether the argument is required.
*
* @return true
if the argument is required,
* false
if not.
*/
public boolean isRequired() {
return this.required;
}
/**
* Gets the argument formatted as a string suitable for a usage
* statement.
*
* @return a {@link String}.
*/
public String getFormatted() {
if (this.required) {
return this.name;
} else {
return '[' + this.name + ']';
}
}
}
private Protempa protempa;
private CommandLine commandLine;
private final String shellCommand;
private final Argument[] arguments;
private final boolean configurationIdEnabled;
/**
* Instantiates this application. The system property app.name
* must be set to the shell command name.
*/
protected CLI() {
this(null, null);
}
/**
* Instantiates the application with a required shell command name.
*
* @param shellCommand a {@link String}, cannot be null
or
* empty.
*/
protected CLI(String shellCommand) {
this(shellCommand, null);
}
/**
* Instantiates the application with optional arguments. The system
* property app.name
must be set to the shell command name.
*
* @param arguments the command line arguments for this application. May
* be null
or empty.
*/
protected CLI(Argument[] arguments) {
this(null, arguments, true);
}
/**
* Instantiates the application with a required shell command name and
* optional arguments that are not prefixed with option syntax.
*
* To add options, override
* {@link #addCustomCliOptions(org.apache.commons.cli.Options)}.
*
* @param shellCommand a {@link String}, cannot be null
or
* empty.
* @param arguments an {@link Argument[]}.
*/
protected CLI(String shellCommand, Argument[] arguments) {
this(shellCommand, arguments, true);
}
/**
* Instantiates the application with a required shell command name and
* whether to create the configuration id option. The latter is
* true
by default, and normally would be unless PROTEMPA
* would not be initialized by the program for some reason.
*
* To add options, override
* {@link #addCustomCliOptions(org.apache.commons.cli.Options)}.
*
* @param arguments an {@link Argument[]}.
* @param configurationIdEnabled whether or not the configuration id option
* should be created.
*/
protected CLI(Argument[] arguments, boolean configurationIdEnabled) {
this(null, arguments, configurationIdEnabled);
}
/**
* Instantiates the application with a required shell command name,
* optional arguments that are not prefixed with option syntax, and
* whether to create the configuration id option. The latter is
* true
by default, and normally would be unless PROTEMPA
* would not be initialized by the program for some reason.
*
* To add options, override
* {@link #addCustomCliOptions(org.apache.commons.cli.Options)}.
*
* @param shellCommand a {@link String}, cannot be null
or
* empty.
* @param arguments an {@link Argument[]}.
* @param configurationIdEnabled whether or not the configuration id option
* should be created.
*/
protected CLI(String shellCommand, Argument[] arguments,
boolean configurationIdEnabled) {
if (shellCommand == null || shellCommand.trim().length() == 0) {
shellCommand = System.getProperty("app.name");
if (shellCommand == null || shellCommand.trim().length() == 0) {
throw new IllegalArgumentException(
"shellCommand cannot be null or empty (tried system property app.name too)");
}
}
this.shellCommand = shellCommand;
if (arguments == null) {
arguments = new Argument[0];
}
this.arguments = arguments;
this.configurationIdEnabled = configurationIdEnabled;
}
/**
* Gets the shell command.
*
* @return a {@link String}.
*/
public final String getShellCommand() {
return this.shellCommand;
}
/**
* Gets optional arguments that are not prefixed with option syntax.
*
* @return a {@link Argument[]}. Guaranteed not null
.
*/
public final Argument[] getArguments() {
return this.arguments.clone();
}
/**
* Whether the configuration option will be created. This is
* true
unless otherwise specified, and must be
* true
if {@link #initialize()} will be called.
*
* @return true
or false
.
*/
public final boolean isConfigurationIdEnabled() {
return this.configurationIdEnabled;
}
/**
* Initializes PROTEMPA with the specified configuration. Must be called
* after {@link #processArgs(java.lang.String[], int, int) }.
*
* @param configurationId the id {@link String} of a configuration.
* @throws ProtempaStartupException if an error occurred starting up
* PROTEMPA.
*/
public final void initialize()
throws ProtempaStartupException {
if (!this.configurationIdEnabled) {
throw new IllegalStateException(
"Cannot initialize without a configuration id");
}
this.protempa = Protempa.newInstance(commandLine.getOptionValue("c"));
}
/**
* Implemented with whatever is done with the instance of PROTEMPA
* created with {@link #initialize(java.lang.String)}, and processing of
* any arguments and command line options specified in
* {@link #addCustomCliOptions(org.apache.commons.cli.Options)}. The
* default implementation is a no-op.
*
* @param protempa an instance of {@link Protempa}.
* @param commandLine the {@link CommandLine} options passed in by the
* caller.
* @throws CLIException if some exception was thrown by the implementation
* of this method. The convention is for any such exceptions to be nested
* in an instance of {@link CLIException}.
*/
public void execute(Protempa protempa, CommandLine commandLine)
throws CLIException {
}
/**
* Prints to the console an exception's message and its nested exception's
* message (if any).
*
* @param cliException the {@link Exception} to print.
*/
public final void printException(Exception cliException) {
System.err.print(cliException.getMessage());
printCause(cliException);
}
private void printCause(Throwable cliException) {
Throwable cause = cliException.getCause();
if (cause != null && cause.getMessage() != null
&& cause.getMessage().length() > 0) {
System.err.print(": ");
System.err.println();
System.err.print(cause.getMessage());
printCause(cause);
} else {
System.err.println();
}
}
/**
* Calls the code in
* {@link #execute(org.protempa.Protempa, org.apache.commons.cli.CommandLine)}.
*
* @throws CLIException if
* {@link #execute(org.protempa.Protempa, org.apache.commons.cli.CommandLine)}
* throws an exception.
*/
public final void execute() throws CLIException {
if (this.protempa == null) {
throw new IllegalStateException("PROTEMPA not initialized.");
}
execute(this.protempa, this.commandLine);
}
/**
* Closes the instance of {@link Protempa} created by
* {@link #initialize(java.lang.String)}.
*/
public final void close() throws CloseException {
if (this.protempa == null) {
throw new IllegalStateException("PROTEMPA not initialized");
}
this.protempa.close();
this.protempa = null;
}
/**
* Processes command line options. The default command line options are -h
* or --help, which prints usage and the list of options, and -c or
* --configuration, which is followed by the id of a PROTEMPA
* configuration. The latter is required unless -h is specified. Any
* additional options that are specified in
* {@link #addCustomCliOptions(org.apache.commons.cli.Options)} should be
* handled by
* {@link #execute(org.protempa.Protempa, org.apache.commons.cli.CommandLine)}.
* There always must be at least one argument for the configuration id.
*
* @param args the argument {@link String[]} passed into main
.
* @return a {@link CommandLine} instance.
*/
public final CommandLine processOptionsAndArgs(String[] args) {
if (args == null) {
throw new IllegalArgumentException("args cannot be null");
}
Options cliOptions = constructCliOptions();
CommandLineParser parser = new PosixParser();
try {
this.commandLine = parser.parse(cliOptions, args);
} catch (ParseException ex) {
System.err.println(ex.getMessage());
System.exit(1);
}
String commandLineSyntax = commandLineSyntax(this.arguments);
if (this.commandLine.hasOption("h")) {
new HelpFormatter().printHelp(commandLineSyntax, cliOptions);
System.exit(0);
}
if (this.configurationIdEnabled && !this.commandLine.hasOption("c")) {
System.err.println("missing option: c");
System.exit(1);
}
checkInvalidArguments(this.arguments);
return this.commandLine;
}
/**
* Like calling {@link #initialize(java.lang.String)}, {@link #execute()}
* and {@link #close()} in succession. Must be called only after
* {@link #processArgs(java.lang.String[], int, int) }.
*
*/
public final void initializeExecuteAndClose() {
try {
initialize();
} catch (ProtempaStartupException ex) {
printException(ex);
System.exit(1);
}
boolean error = false;
try {
execute();
close();
} catch (CloseException | CLIException ex) {
printException(ex);
error = true;
} finally {
if (error) {
try {
close();
} catch (CloseException ignore) {
}
}
}
if (error) {
System.exit(1);
}
}
/**
* Override to specify any custom command line options.
*
* @param options the command line {@link Options}.
*/
protected void addCustomCliOptions(Options options) {
}
private String commandLineSyntax(Argument[] arguments) {
StringBuilder argumentStringBuilder = new StringBuilder();
if (arguments.length > 0) {
argumentStringBuilder.append(' ');
}
for (int i = 0; i < arguments.length; i++) {
argumentStringBuilder.append(arguments[i].getFormatted());
if (i < arguments.length - 1) {
argumentStringBuilder.append(' ');
}
}
String commandLineSyntax = this.shellCommand + " [options]"
+ argumentStringBuilder.toString();
return commandLineSyntax;
}
private Options constructCliOptions() {
Options options = new Options();
options.addOption("h", "help", false, "print this message");
if (configurationIdEnabled) {
options.addOption("c", "configuration", true,
"PROTEMPA configuration id");
}
addCustomCliOptions(options);
return options;
}
private void checkInvalidArguments(Argument[] arguments) {
String[] leftOverArgs = commandLine.getArgs();
List requiredArguments = new ArrayList<>();
for (Argument argument : arguments) {
if (argument.isRequired()) {
requiredArguments.add(argument);
}
}
List missingRequiredArgs = new ArrayList<>();
if (leftOverArgs.length < requiredArguments.size()) {
for (int i = leftOverArgs.length, n = requiredArguments.size();
i < n; i++) {
missingRequiredArgs.add(requiredArguments.get(i).getName());
}
}
if (!missingRequiredArgs.isEmpty()) {
System.err.println("Missing argument(s): "
+ StringUtils.join(missingRequiredArgs, ", "));
System.exit(1);
}
if (leftOverArgs.length > arguments.length) {
List extraArgs = new ArrayList<>();
for (int i = arguments.length; i < leftOverArgs.length; i++) {
extraArgs.add(leftOverArgs[i]);
}
System.err.println("Invalid extra argument(s): "
+ StringUtils.join(extraArgs, ","));
System.exit(1);
}
}
}