/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.shell.syntax;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.jnode.driver.console.CompletionInfo;
import org.jnode.shell.CommandLine;
import org.jnode.shell.SymbolSource;
/**
* An ArgumentBundle holds a collection of Argument objects and provides methods
* for looking them up (by label), and performing command line parsing and completion
* based on the Arguments and a separately managed Syntax. The ArgumentBundle and
* the Syntax tree are related by means of the label values on their respective nodes.
* <p>
* Each native JNode command implements a method for providing an ArgumentBundle to
* the command shell / interpreters. The (provisional) interactions for executing a
* shell command are as follows:
* <ol>
* <li>The shell / interpreter parse the raw command line text according to the current
* interpreter's meta-syntax. This process strips out redirections, control constructs
* and so on, and performs the relevant expansions. This yields a command alias name
* and sequence of argument strings, wrapped up as a CommandLine object.
* <li>The command alias is mapped to a Java class which is loaded.
* <li>If the command class <b>does not</b> extend AbstractCommand, the command is called via
* its "public static void main(String[])" entry point.
* <li>Otherwise:
* <ol>
* <li>An instance of the command class is created.
* <li>The command's 'getArgumentBundle' method is called to get the argument bundle.
* <li>The shell tries to find a Syntax for the alias. (These will be specified independently
* of the command class; e.g. in an XML file. Different Syntaxes may exist for a given command,
* depending on the interpreter, the user's preferences and so on.) If no Syntax is found, a
* default syntax can be generated from the ArgumentBundle.
* <li>The shell calls the 'parse' method on the ArgumentBundle, passing the CommandLine and
* the chosen (or default) Syntax. This traverses the Syntax, attempting to match the command
* argument tokens against the Syntax node's corresponding Arguments. In the process, command
* argument tokens are converted into values that are bound the the appropriate Arguments.
* <li>If the parse succeeds, the command is then run by calling the 'execute' method, passing
* the ArgumentBundle with the bound argument values. The command can use get the argument
* values, flags and so on by calling 'getArgument(..).getValue()', or by calling 'getValue()'
* on Argument references saved by the earlier 'getArgumentBundle()' call.
* </ol>
* </ol>
*
* @author crawley@jnode.org
*/
public class ArgumentBundle implements Iterable<Argument<?>> {
public static final int UNPARSED = 0;
public static final int PARSING = 1;
public static final int PARSE_SUCCEEDED = 2;
public static final int PARSE_FAILED = 3;
private Argument<?>[] arguments;
private final Map<String, Argument<?>> argumentMap;
private final String description;
private int status = UNPARSED;
public ArgumentBundle(String description, Argument<?>... arguments) {
this.description = description;
this.arguments = arguments;
this.argumentMap = new HashMap<String, Argument<?>>();
for (Argument<?> element : arguments) {
doAdd(element);
}
}
public ArgumentBundle(Argument<?> ...arguments) {
this(null, arguments);
}
private void doAdd(Argument<?> argument) {
String label = argument.getLabel();
if (label.isEmpty()) {
throw new IllegalArgumentException("argument label is empty");
}
if (this.argumentMap.containsKey(label)) {
throw new IllegalArgumentException(
"argument label '" + label + "' used more than once");
}
this.argumentMap.put(label, argument);
argument.setBundle(this);
}
public synchronized void parse(CommandLine commandLine, SyntaxBundle syntaxes)
throws CommandSyntaxException {
try {
doParse(commandLine, syntaxes, null);
for (Argument<?> element : arguments) {
if (!element.isSet() && element.isMandatory()) {
throw new CommandSyntaxException(
"Command syntax error: required argument '"
+ element.getLabel() + "' not supplied");
}
}
status = PARSE_SUCCEEDED;
} finally {
if (status != PARSE_SUCCEEDED) {
status = PARSE_FAILED;
}
}
}
public synchronized void complete(CommandLine partial, SyntaxBundle syntaxes,
CompletionInfo completion)
throws CommandSyntaxException {
try {
doParse(partial, syntaxes, completion);
status = PARSE_SUCCEEDED;
} finally {
if (status != PARSE_SUCCEEDED) {
status = PARSE_FAILED;
}
}
}
private void doParse(CommandLine commandLine, SyntaxBundle syntaxes,
CompletionInfo completion) throws CommandSyntaxException {
if (status != UNPARSED) {
clear();
}
status = PARSING;
if (syntaxes == null) {
syntaxes = new SyntaxBundle(commandLine.getCommandName(), createDefaultSyntax());
}
SymbolSource<CommandLine.Token> context = commandLine.tokenIterator();
MuSyntax muSyntax = syntaxes.prepare(this);
new MuParser().parse(muSyntax, completion, context, this);
}
/**
* Find the command Argument (as defined by the bundle) for an ArgumentSyntax node.
*
* @param syntax the ArgumentSyntax element
* @return the corresponding Argument
* @throws SyntaxFailureException if the label is not present in the argument
* bundle. This typically means that the Syntax includes elements that have
* no meaning to the command that created the bundle; i.e. the Syntax is broken.
*/
public Argument<?> getArgument(ArgumentSyntax syntax) throws SyntaxFailureException {
return getArgument(syntax.getArgName());
}
/**
* Find the command Argument (as defined by the bundle) for a given argument name.
*
* @param argName an argument name
* @return the corresponding Argument
* @throws SyntaxFailureException if the label is not present in the argument
* bundle. This typically means that the Syntax includes elements that have
* no meaning to the command that created the bundle; i.e. the Syntax is broken.
*/
public Argument<?> getArgument(String argName) throws SyntaxFailureException {
Argument<?> arg = argumentMap.get(argName);
if (arg == null) {
throw new SyntaxFailureException(
"No argument for syntax label '" + argName + "'");
}
return arg;
}
/**
* Generate a default command syntax to use when none has been defined.
* The syntax defines an option corresponding to each argument, with
* the argument labels as the long option names.
*
* @return the default syntax
*/
public Syntax createDefaultSyntax() {
if (arguments.length == 0) {
return new EmptySyntax("default", null);
} else if (arguments.length == 1) {
String label = arguments[0].getLabel();
return new OptionSyntax(label, label, null, null);
} else {
// A better default syntax would only allow one Option repetition
// for any Argument that accepts only one value, and would use mandatory
// Options for mandatory Arguments.
Syntax[] syntaxes = new OptionSyntax[arguments.length];
for (int i = 0; i < syntaxes.length; i++) {
String label = arguments[i].getLabel();
syntaxes[i] = new OptionSyntax(label, label, null, null);
}
return new PowersetSyntax("default", syntaxes);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ArgumentBundle{");
for (int i = 0; i < arguments.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(arguments[i]);
}
sb.append("}");
return sb.toString();
}
public synchronized void clear() {
for (Argument<?> element : arguments) {
element.clear();
}
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
/**
* Add an Argument to the bundle.
*
* @param argument
*/
public void addArgument(Argument<?> argument) {
doAdd(argument);
Argument<?>[] tmp = new Argument<?>[arguments.length + 1];
System.arraycopy(arguments, 0, tmp, 0, arguments.length);
tmp[arguments.length] = argument;
arguments = tmp;
}
/**
* Return the command's description string or <code>null</code>.
* @return the description string
*/
public String getDescription() {
return description;
}
public Iterator<Argument<?>> iterator() {
return Arrays.asList(arguments).iterator();
}
}