package org.dcache.util.cli;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSortedMap;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import dmg.util.CommandException;
import dmg.util.CommandSyntaxException;
import dmg.util.command.Argument;
import dmg.util.command.Command;
import dmg.util.command.HelpFormat;
import dmg.util.command.Option;
import org.dcache.util.Args;
/**
* Support for commands, where a command is an Args-based request to some named
* entity that provides a Serializable response.
*
* Commands are found by scanning one or more command-listener objects. The
* process of discovering these commands is abstracted, and provided by one or
* more CommandScanner objects.
*
* Command-listener objects and CommandScanners are added after CommandInterpreter
* is created.
*/
public class CommandInterpreter
{
private final CommandEntry _rootEntry = new CommandEntry("");
private final List<CommandScanner> _scanners = new ArrayList<>();
private final List<Object> _commandListeners = new ArrayList<>();
protected synchronized void addCommandScanner(CommandScanner scanner)
{
_scanners.add(scanner);
for (Object commandListener : _commandListeners) {
addCommands(scanner.scan(commandListener));
}
}
/**
* Adds an interpreter too the current object.
* @params commandListener is the object which will be inspected.
*/
public final synchronized void addCommandListener(Object commandListener)
{
for (CommandScanner scanner : _scanners) {
addCommands(scanner.scan(commandListener));
}
_commandListeners.add(commandListener);
}
private void addCommands(Map<List<String>,? extends CommandExecutor> commands)
{
for (Map.Entry<List<String>,? extends CommandExecutor> entry: commands.entrySet()) {
CommandEntry currentEntry = _rootEntry.getOrCreate(entry.getKey());
if (currentEntry.hasCommand()) {
throw new IllegalArgumentException("Conflicting implementations of shell command '" +
Joiner.on(" ").join(entry.getKey()) + "': " +
currentEntry.getCommand() + " and " + entry.getValue());
}
currentEntry.setCommand(entry.getValue());
}
}
/**
* Interpreters the specified arguments and calles the
* corresponding method of the connected Object.
*
* @params args is the initialized Args Object containing
* the commands.
* @return the string returned by the corresponding
* method of the reflected object.
*
* @exception CommandSyntaxException if the used command syntax
* doesn't match any of the corresponding methods.
* The .getHelpText() method provides a short
* description of the correct syntax, if possible.
* @exception CommandExitException if the corresponding
* object doesn't want to be used any more.
* Usually shells send this Exception to 'exit'.
* @exception CommandThrowableException if the corresponding
* method throws any kind of throwable.
* The thrown throwable can be obtaines by calling
* .getTargetException of the CommandThrowableException.
* @exception CommandPanicException if the invocation of the
* corresponding method failed. .getTargetException
* provides the actual Exception of the failure.
*/
public Serializable command(Args args) throws CommandException {
//
// walk along the command tree as long as arguments are
// available and as long as those arguments match the
// tree.
//
CommandEntry entry = _rootEntry;
CommandEntry lastAcl = null;
StringBuilder path = new StringBuilder();
int i = 0;
while (i < args.argc()) {
CommandEntry ce = entry.get(args.argv(i));
if (ce == null) {
break;
}
if (ce.hasACLs()) {
lastAcl = ce;
}
path.append(ce.getName()).append(' ');
entry = ce;
i++;
}
if (!entry.hasCommand()) {
throw new CommandSyntaxException("Command not found: " + args);
}
args.shift(i);
String[] acls = lastAcl == null ? new String[0] : lastAcl.getACLs();
try {
return doExecute(entry, args, acls);
} catch (CommandSyntaxException e) {
if (e.getHelpText() == null) {
StringBuilder sb = new StringBuilder();
entry.dumpHelpHint(path.toString(), sb, HelpFormat.PLAIN);
e.setHelpText(sb.toString());
}
throw e;
}
}
protected Serializable doExecute(CommandEntry entry, Args args,
String[] acls) throws CommandException
{
return entry.execute(args);
}
/**
* A CommandEntry is a node in a tree representing command prefixes. Each node
* can be associated with a CommandExecutor.
*/
protected static class CommandEntry
{
private ImmutableSortedMap<String,CommandEntry> _suffixes =
ImmutableSortedMap.of();
private final String _name;
private CommandExecutor _commandExecutor;
CommandEntry(String name) {
_name = name;
}
public String getName() {
return _name;
}
public void put(String str, CommandEntry e) {
_suffixes = ImmutableSortedMap.<String,CommandEntry>naturalOrder()
.putAll(_suffixes)
.put(str,e)
.build();
}
public CommandEntry get(String str) {
return _suffixes.get(str);
}
public CommandEntry getOrCreate(String name) {
CommandEntry entry = _suffixes.get(name);
if (entry == null) {
entry = new CommandEntry(name);
put(name, entry);
}
return entry;
}
public CommandEntry getOrCreate(List<String> names) {
CommandEntry entry = this;
for (String name: names) {
entry = entry.getOrCreate(name);
}
return entry;
}
public void setCommand(CommandExecutor commandExecutor)
{
_commandExecutor = commandExecutor;
}
public CommandExecutor getCommand()
{
return _commandExecutor;
}
boolean hasCommand()
{
return _commandExecutor != null;
}
public boolean hasACLs()
{
return (_commandExecutor != null) && _commandExecutor.hasACLs();
}
public void dumpHelpHint(String top, StringBuilder sb, HelpFormat format)
{
if (_commandExecutor != null && !_commandExecutor.isDeprecated()) {
String hint = _commandExecutor.getHelpHint(format);
if (hint != null) {
sb.append(top).append(hint).append("\n");
}
}
for (CommandEntry ce: _suffixes.values()) {
ce.dumpHelpHint(top + ce.getName() + " ", sb, format);
}
}
public Serializable execute(Args arguments)
throws CommandException
{
return _commandExecutor.execute(arguments);
}
public String getFullHelp(HelpFormat format)
{
return (_commandExecutor == null) ? null : _commandExecutor.getFullHelp(format);
}
public String[] getACLs()
{
return (_commandExecutor == null) ? new String[0] : _commandExecutor.getACLs();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Entry : ").append(getName());
for (String key: _suffixes.keySet()) {
sb.append(" -> ").append(key).append("\n");
}
return sb.toString();
}
}
public String getHelp(HelpFormat format, String... command)
{
CommandEntry entry = _rootEntry;
StringBuilder path = new StringBuilder();
for (String word : command) {
CommandEntry ce = entry.get(word);
if (ce == null) {
break;
}
path.append(ce.getName()).append(' ');
entry = ce;
}
String help = entry.getFullHelp(format);
if (help == null) {
StringBuilder sb = new StringBuilder();
entry.dumpHelpHint(path.toString(), sb, format);
help = sb.toString();
}
return help;
}
public class HelpCommands
{
@Command(name = "help", hint = "display help pages")
public class HelpCommand implements Callable<String>
{
@Option(name = "format", usage = "Output format.")
HelpFormat format = HelpFormat.PLAIN;
@Argument(valueSpec = "COMMAND", required = false,
usage = "When invoked with a specific command, detailed help for that " +
"command is displayed. When invoked with a partial command or without " +
"an argument, a summary of all matching commands is shown.")
String[] command = {};
@Override
public String call()
{
return getHelp(format, command);
}
}
}
}