package org.springframework.roo.shell;
import static org.apache.commons.io.IOUtils.LINE_SEPARATOR;
import static org.springframework.roo.shell.CliOption.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.Transformer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.util.CollectionUtils;
import org.springframework.roo.support.util.XmlElementBuilder;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Default implementation of {@link Parser}.
*
* @author Ben Alex
* @since 1.0
*/
public class SimpleParser implements Parser {
private static final Comparator<Object> COMPARATOR = new NaturalOrderComparator<Object>();
private static final Logger LOGGER = HandlerUtils
.getLogger(SimpleParser.class);
static String isMatch(final String buffer, final String command,
final boolean strictMatching) {
if ("".equals(buffer.trim())) {
return "";
}
final String[] commandWords = StringUtils.split(command, " ");
int lastCommandWordUsed = 0;
Validate.notEmpty(commandWords, "Command required");
String bufferToReturn = null;
String lastWord = null;
next_buffer_loop: for (int bufferIndex = 0; bufferIndex < buffer
.length(); bufferIndex++) {
final String bufferSoFarIncludingThis = buffer.substring(0,
bufferIndex + 1);
final String bufferRemaining = buffer.substring(bufferIndex + 1);
final int bufferLastIndexOfWord = bufferSoFarIncludingThis
.lastIndexOf(" ");
String wordSoFarIncludingThis = bufferSoFarIncludingThis;
if (bufferLastIndexOfWord != -1) {
wordSoFarIncludingThis = bufferSoFarIncludingThis
.substring(bufferLastIndexOfWord);
}
if (wordSoFarIncludingThis.equals(" ")
|| bufferIndex == buffer.length() - 1) {
if (bufferIndex == buffer.length() - 1
&& !"".equals(wordSoFarIncludingThis.trim())) {
lastWord = wordSoFarIncludingThis.trim();
}
// At end of word or buffer. Let's see if a word matched or not
for (int candidate = lastCommandWordUsed; candidate < commandWords.length; candidate++) {
if (lastWord != null && lastWord.length() > 0
&& commandWords[candidate].startsWith(lastWord)) {
if (bufferToReturn == null) {
// This is the first match, so ensure the intended
// match really represents the start of a command
// and not a later word within it
if (lastCommandWordUsed == 0 && candidate > 0) {
// This is not a valid match
break next_buffer_loop;
}
}
if (bufferToReturn != null) {
// We already matched something earlier, so ensure
// we didn't skip any word
if (candidate != lastCommandWordUsed + 1) {
// User has skipped a word
bufferToReturn = null;
break next_buffer_loop;
}
}
bufferToReturn = bufferRemaining;
lastCommandWordUsed = candidate;
if (candidate + 1 == commandWords.length) {
// This was a match for the final word in the
// command, so abort
break next_buffer_loop;
}
// There are more words left to potentially match, so
// continue
continue next_buffer_loop;
}
}
// This word is unrecognised as part of a command, so abort
bufferToReturn = null;
break next_buffer_loop;
}
lastWord = wordSoFarIncludingThis.trim();
}
// We only consider it a match if ALL words were actually used
if (bufferToReturn != null) {
if (!strictMatching
|| lastCommandWordUsed + 1 == commandWords.length) {
return bufferToReturn;
}
}
return null; // Not a match
}
private final Map<String, MethodTarget> availabilityIndicators = new HashMap<String, MethodTarget>();
private final Set<CommandMarker> commands = new HashSet<CommandMarker>();
private final Set<Converter<?>> converters = new HashSet<Converter<?>>();
private final Object mutex = new Object();
public final void add(final CommandMarker command) {
synchronized (mutex) {
commands.add(command);
for (final Method method : command.getClass().getMethods()) {
final CliAvailabilityIndicator availability = method
.getAnnotation(CliAvailabilityIndicator.class);
if (availability != null) {
Validate.isTrue(
method.getParameterTypes().length == 0,
"CliAvailabilityIndicator is only legal for 0 parameter methods ('%s')",
method.toGenericString());
Validate.isTrue(
method.getReturnType().equals(Boolean.TYPE),
"CliAvailabilityIndicator is only legal for primitive boolean return types (%s)",
method.toGenericString());
for (final String cmd : availability.value()) {
Validate.isTrue(
!availabilityIndicators.containsKey(cmd),
"Cannot specify an availability indicator for '%s' more than once",
cmd);
availabilityIndicators.put(cmd, new MethodTarget(
method, command));
}
}
}
}
}
public final void add(final Converter<?> converter) {
synchronized (mutex) {
converters.add(converter);
}
}
protected void commandNotFound(final Logger logger, final String buffer) {
logger.warning("Command '" + buffer
+ "' not found (for assistance press "
+ AbstractShell.completionKeys
+ " or type \"hint\" then hit ENTER)");
}
public int complete(final String buffer, final int cursor,
final List<String> candidates) {
final List<Completion> completions = new ArrayList<Completion>();
final int result = completeAdvanced(buffer, cursor, completions);
for (final Completion completion : completions) {
candidates.add(completion.getValue());
}
return result;
}
public int completeAdvanced(String buffer, int cursor,
final List<Completion> candidates) {
synchronized (mutex) {
Validate.notNull(buffer, "Buffer required");
Validate.notNull(candidates, "Candidates list required");
// Remove all spaces from beginning of command
while (buffer.startsWith(" ")) {
buffer = buffer.replaceFirst("^ ", "");
cursor--;
}
// Replace all multiple spaces with a single space
while (buffer.contains(" ")) {
buffer = StringUtils.replace(buffer, " ", " ", 1);
cursor--;
}
// Begin by only including the portion of the buffer represented to
// the present cursor position
final String translated = buffer.substring(0, cursor);
// Start by locating a method that matches
final Collection<MethodTarget> targets = locateTargets(translated,
false, true);
final SortedSet<Completion> results = new TreeSet<Completion>(
COMPARATOR);
if (targets.isEmpty()) {
// Nothing matches the buffer they've presented
return cursor;
}
if (targets.size() > 1) {
// Assist them locate a particular target
for (final MethodTarget target : targets) {
// Calculate the correct starting position
final int startAt = translated.length();
// Only add the first word of each target
int stopAt = target.getKey().indexOf(" ", startAt);
if (stopAt == -1) {
stopAt = target.getKey().length();
}
results.add(new Completion(target.getKey().substring(0,
stopAt)
+ " "));
}
candidates.addAll(results);
return 0;
}
// There is a single target of this method, so provide completion
// services for it
final MethodTarget methodTarget = targets.iterator().next();
// Identify the command we're working with
final CliCommand cmd = methodTarget.getMethod().getAnnotation(
CliCommand.class);
Validate.notNull(cmd, "CliCommand unavailable for '%s'",
methodTarget.getMethod().toGenericString());
// Make a reasonable attempt at parsing the remainingBuffer
Map<String, String> options;
try {
options = ParserUtils.tokenize(methodTarget
.getRemainingBuffer());
}
catch (final IllegalArgumentException ex) {
// Assume any IllegalArgumentException is due to a quotation
// mark mismatch
candidates.add(new Completion(translated + "\""));
return 0;
}
// Lookup arguments for this target
final Annotation[][] parameterAnnotations = methodTarget
.getMethod().getParameterAnnotations();
// If there aren't any parameters for the method, at least ensure
// they have typed the command properly
if (parameterAnnotations.length == 0) {
for (final String value : cmd.value()) {
if (buffer.startsWith(value) || value.startsWith(buffer)) {
// No space at the end, as there's no need to continue
// the command further
results.add(new Completion(value));
}
}
candidates.addAll(results);
return 0;
}
// If they haven't specified any parameters yet, at least verify the
// command name is fully completed
if (options.isEmpty()) {
for (final String value : cmd.value()) {
if (value.startsWith(buffer)) {
// They are potentially trying to type this command
// We only need provide completion, though, if they
// failed to specify it fully
if (!buffer.startsWith(value)) {
// They failed to specify the command fully
results.add(new Completion(value + " "));
}
}
}
// Only quit right now if they have to finish specifying the
// command name
if (results.size() > 0) {
candidates.addAll(results);
return 0;
}
}
// To get this far, we know there are arguments required for this
// CliCommand, and they specified a valid command name
// Record all the CliOptions applicable to this command
final List<CliOption> cliOptions = new ArrayList<CliOption>();
for (final Annotation[] annotations : parameterAnnotations) {
CliOption cliOption = null;
for (final Annotation a : annotations) {
if (a instanceof CliOption) {
cliOption = (CliOption) a;
}
}
Validate.notNull(cliOption,
"CliOption not found for parameter '%s'",
Arrays.toString(annotations));
cliOptions.add(cliOption);
}
// Make a list of all CliOptions they've already included or are
// system-provided
final List<CliOption> alreadySpecified = new ArrayList<CliOption>();
for (final CliOption option : cliOptions) {
for (final String value : option.key()) {
if (options.containsKey(value)) {
alreadySpecified.add(option);
break;
}
}
if (option.systemProvided()) {
alreadySpecified.add(option);
}
}
// Make a list of all CliOptions they have not provided
final List<CliOption> unspecified = new ArrayList<CliOption>(
cliOptions);
unspecified.removeAll(alreadySpecified);
// Determine whether they're presently editing an option key or an
// option value
// (and if possible, the full or partial name of the said option key
// being edited)
String lastOptionKey = null;
String lastOptionValue = null;
// The last item in the options map is *always* the option key
// they're editing (will never be null)
if (options.size() > 0) {
lastOptionKey = new ArrayList<String>(options.keySet())
.get(options.keySet().size() - 1);
lastOptionValue = options.get(lastOptionKey);
}
// Handle if they are trying to find out the available option keys;
// always present option keys in order
// of their declaration on the method signature, thus we can stop
// when mandatory options are filled in
if (methodTarget.getRemainingBuffer().endsWith("--")) {
boolean showAllRemaining = true;
for (final CliOption include : unspecified) {
if (include.mandatory()) {
showAllRemaining = false;
break;
}
}
for (final CliOption include : unspecified) {
for (final String value : include.key()) {
if (!"".equals(value)) {
results.add(new Completion(translated + value + " "));
}
}
if (!showAllRemaining) {
break;
}
}
candidates.addAll(results);
return 0;
}
// Handle suggesting an option key if they haven't got one presently
// specified (or they've completed a full option key/value pair)
if (lastOptionKey == null || !"".equals(lastOptionKey)
&& !"".equals(lastOptionValue) && translated.endsWith(" ")) {
// We have either NEVER specified an option key/value pair
// OR we have specified a full option key/value pair
// Let's list some other options the user might want to try
// (naturally skip the "" option, as that's the default)
for (final CliOption include : unspecified) {
for (final String value : include.key()) {
// Manually determine if this non-mandatory but
// unspecifiedDefaultValue=* requiring option is able to
// be bound
if (!include.mandatory()
&& "*".equals(include.unspecifiedDefaultValue())
&& !"".equals(value)) {
try {
for (final Converter<?> candidate : converters) {
// Find the target parameter
Class<?> paramType = null;
int index = -1;
for (final Annotation[] a : methodTarget
.getMethod()
.getParameterAnnotations()) {
index++;
for (final Annotation an : a) {
if (an instanceof CliOption) {
if (an.equals(include)) {
// Found the parameter, so
// store it
paramType = methodTarget
.getMethod()
.getParameterTypes()[index];
break;
}
}
}
}
if (paramType != null
&& candidate.supports(paramType,
include.optionContext())) {
// Try to invoke this usable converter
candidate.convertFromText("*",
paramType,
include.optionContext());
// If we got this far, the converter is
// happy with "*" so we need not bother
// the user with entering the data in
// themselves
break;
}
}
}
catch (final RuntimeException notYetReady) {
if (translated.endsWith(" ")) {
results.add(new Completion(translated
+ "--" + value + " "));
}
else {
results.add(new Completion(translated
+ " --" + value + " "));
}
continue;
}
}
// Handle normal mandatory options
if (!"".equals(value) && include.mandatory()) {
if (translated.endsWith(" ")) {
results.add(new Completion(translated + "--"
+ value + " "));
}
else {
results.add(new Completion(translated + " --"
+ value + " "));
}
}
}
}
// Only abort at this point if we have some suggestions;
// otherwise we might want to try to complete the "" option
if (results.size() > 0) {
candidates.addAll(results);
return 0;
}
}
// Handle completing the option key they're presently typing
if ((lastOptionValue == null || "".equals(lastOptionValue))
&& !translated.endsWith(" ")) {
// Given we haven't got an option value of any form, and there's
// no space at the buffer end, we must still be typing an option
// key
for (final CliOption option : cliOptions) {
for (final String value : option.key()) {
if (value != null
&& lastOptionKey != null
&& value.regionMatches(true, 0, lastOptionKey,
0, lastOptionKey.length())) {
final String completionValue = translated
.substring(0, translated.length()
- lastOptionKey.length())
+ value + " ";
results.add(new Completion(completionValue));
}
}
}
candidates.addAll(results);
return 0;
}
// To be here, we are NOT typing an option key (or we might be, and
// there are no further option keys left)
if (lastOptionKey != null && !"".equals(lastOptionKey)) {
// Lookup the relevant CliOption that applies to this
// lastOptionKey
// We do this via the parameter type
final Class<?>[] parameterTypes = methodTarget.getMethod()
.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
final CliOption option = cliOptions.get(i);
final Class<?> parameterType = parameterTypes[i];
for (final String key : option.key()) {
if (key.equals(lastOptionKey)) {
final List<Completion> allValues = new ArrayList<Completion>();
String suffix = " ";
// Let's use a Converter if one is available
for (final Converter<?> candidate : converters) {
if (candidate.supports(parameterType,
option.optionContext())) {
// Found a usable converter
final boolean addSpace = candidate
.getAllPossibleValues(allValues,
parameterType,
lastOptionValue,
option.optionContext(),
methodTarget);
if (!addSpace) {
suffix = "";
}
break;
}
}
if (allValues.isEmpty()) {
// Doesn't appear to be a custom Converter, so
// let's go and provide defaults for simple
// types
// Provide some simple options for common types
if (Boolean.class
.isAssignableFrom(parameterType)
|| Boolean.TYPE
.isAssignableFrom(parameterType)) {
allValues.add(new Completion("true"));
allValues.add(new Completion("false"));
}
if (Number.class
.isAssignableFrom(parameterType)) {
allValues.add(new Completion("0"));
allValues.add(new Completion("1"));
allValues.add(new Completion("2"));
allValues.add(new Completion("3"));
allValues.add(new Completion("4"));
allValues.add(new Completion("5"));
allValues.add(new Completion("6"));
allValues.add(new Completion("7"));
allValues.add(new Completion("8"));
allValues.add(new Completion("9"));
}
}
String prefix = "";
if (!translated.endsWith(" ")) {
prefix = " ";
}
// Only include in the candidates those results
// which are compatible with the present buffer
for (final Completion currentValue : allValues) {
// We only provide a suggestion if the
// lastOptionValue == ""
if (StringUtils.isBlank(lastOptionValue)) {
// We should add the result, as they haven't
// typed anything yet
results.add(new Completion(prefix
+ currentValue.getValue() + suffix,
currentValue.getFormattedValue(),
currentValue.getHeading(),
currentValue.getOrder()));
}
else {
// Only add the result **if** what they've
// typed is compatible *AND* they haven't
// already typed it in full
if (currentValue
.getValue()
.toLowerCase()
.startsWith(
lastOptionValue
.toLowerCase())
&& !lastOptionValue
.equalsIgnoreCase(currentValue
.getValue())
&& lastOptionValue.length() < currentValue
.getValue().length()) {
results.add(new Completion(prefix
+ currentValue.getValue()
+ suffix, currentValue
.getFormattedValue(),
currentValue.getHeading(),
currentValue.getOrder()));
}
}
}
// ROO-389: give inline options given there's
// multiple choices available and we want to help
// the user
final StringBuilder help = new StringBuilder();
help.append(LINE_SEPARATOR);
help.append(option.mandatory() ? "required --"
: "optional --");
if ("".equals(option.help())) {
help.append(lastOptionKey).append(": ")
.append("No help available");
}
else {
help.append(lastOptionKey).append(": ")
.append(option.help());
}
if (option.specifiedDefaultValue().equals(
option.unspecifiedDefaultValue())) {
if (option.specifiedDefaultValue().equals(
NULL)) {
help.append("; no default value");
}
else {
help.append("; default: '")
.append(option
.specifiedDefaultValue())
.append("'");
}
}
else {
if (!"".equals(option.specifiedDefaultValue())
&& !NULL.equals(option
.specifiedDefaultValue())) {
help.append(
"; default if option present: '")
.append(option
.specifiedDefaultValue())
.append("'");
}
if (!"".equals(option.unspecifiedDefaultValue())
&& !NULL.equals(option
.unspecifiedDefaultValue())) {
help.append(
"; default if option not present: '")
.append(option
.unspecifiedDefaultValue())
.append("'");
}
}
LOGGER.info(help.toString());
if (results.size() == 1) {
final String suggestion = results.iterator()
.next().getValue().trim();
if (suggestion.equals(lastOptionValue)) {
// They have pressed TAB in the default
// value, and the default value has already
// been provided as an explicit option
return 0;
}
}
if (results.size() > 0) {
candidates.addAll(results);
// Values presented from the last space onwards
if (translated.endsWith(" ")) {
return translated.lastIndexOf(" ") + 1;
}
return translated.trim().lastIndexOf(" ");
}
return 0;
}
}
}
}
return 0;
}
}
private MethodTarget getAvailabilityIndicator(final String command) {
return availabilityIndicators.get(command);
}
private Set<CliOption> getCliOptions(
final Annotation[][] parameterAnnotations) {
final Set<CliOption> cliOptions = new LinkedHashSet<CliOption>();
for (final Annotation[] annotations : parameterAnnotations) {
for (final Annotation annotation : annotations) {
if (annotation instanceof CliOption) {
final CliOption cliOption = (CliOption) annotation;
cliOptions.add(cliOption);
}
}
}
return cliOptions;
}
public Set<String> getEveryCommand() {
synchronized (mutex) {
final SortedSet<String> result = new TreeSet<String>(COMPARATOR);
for (final Object o : commands) {
final Method[] methods = o.getClass().getMethods();
for (final Method m : methods) {
final CliCommand cmd = m.getAnnotation(CliCommand.class);
if (cmd != null) {
result.addAll(Arrays.asList(cmd.value()));
}
}
}
return result;
}
}
private Set<String> getSpecifiedUnavailableOptions(
final Set<CliOption> cliOptions, final Map<String, String> options) {
final Set<String> cliOptionKeySet = new LinkedHashSet<String>();
for (final CliOption cliOption : cliOptions) {
for (final String key : cliOption.key()) {
cliOptionKeySet.add(key.toLowerCase());
}
}
final Set<String> unavailableOptions = new LinkedHashSet<String>();
for (final String suppliedOption : options.keySet()) {
if (!cliOptionKeySet.contains(suppliedOption.toLowerCase())) {
unavailableOptions.add(suppliedOption);
}
}
return unavailableOptions;
}
public void helpReferenceGuide() {
synchronized (mutex) {
final File f = new File(".");
final File[] existing = f.listFiles(new FileFilter() {
public boolean accept(final File pathname) {
return pathname.getName().startsWith("appendix_");
}
});
for (final File e : existing) {
e.delete();
}
// Compute the sections we'll be outputting, and get them into a
// nice order
final SortedMap<String, Object> sections = new TreeMap<String, Object>(
COMPARATOR);
next_target: for (final Object target : commands) {
final Method[] methods = target.getClass().getMethods();
for (final Method m : methods) {
final CliCommand cmd = m.getAnnotation(CliCommand.class);
if (cmd != null) {
String sectionName = target.getClass().getSimpleName();
final Pattern p = Pattern.compile("[A-Z][^A-Z]*");
final Matcher matcher = p.matcher(sectionName);
final StringBuilder string = new StringBuilder();
while (matcher.find()) {
string.append(matcher.group()).append(" ");
}
sectionName = string.toString().trim();
if (sections.containsKey(sectionName)) {
throw new IllegalStateException("Section name '"
+ sectionName + "' not unique");
}
sections.put(sectionName, target);
continue next_target;
}
}
}
// Build each section of the appendix
final DocumentBuilder builder = XmlUtils.getDocumentBuilder();
final Document document = builder.newDocument();
final List<Element> builtSections = new ArrayList<Element>();
for (final Entry<String, Object> entry : sections.entrySet()) {
final String section = entry.getKey();
final Object target = entry.getValue();
final SortedMap<String, Element> individualCommands = new TreeMap<String, Element>(
COMPARATOR);
final Method[] methods = target.getClass().getMethods();
for (final Method m : methods) {
final CliCommand cmd = m.getAnnotation(CliCommand.class);
if (cmd != null) {
final StringBuilder cmdSyntax = new StringBuilder();
cmdSyntax.append(cmd.value()[0]);
// Build the syntax list
// Store the order options appear
final List<String> optionKeys = new ArrayList<String>();
// key: option key, value: help text
final Map<String, String> optionDetails = new HashMap<String, String>();
for (final Annotation[] ann : m
.getParameterAnnotations()) {
for (final Annotation a : ann) {
if (a instanceof CliOption) {
final CliOption option = (CliOption) a;
// Figure out which key we want to use (use
// first non-empty string, or make it
// "(default)" if needed)
String key = option.key()[0];
if ("".equals(key)) {
for (final String otherKey : option
.key()) {
if (!"".equals(otherKey)) {
key = otherKey;
break;
}
}
if ("".equals(key)) {
key = "[default]";
}
}
final StringBuilder help = new StringBuilder();
if ("".equals(option.help())) {
help.append("No help available");
}
else {
help.append(option.help());
}
if (option.specifiedDefaultValue().equals(
option.unspecifiedDefaultValue())) {
if (option.specifiedDefaultValue()
.equals(NULL)) {
help.append("; no default value");
}
else {
help.append("; default: '")
.append(option
.specifiedDefaultValue())
.append("'");
}
}
else {
if (!"".equals(option
.specifiedDefaultValue())
&& !NULL
.equals(option
.specifiedDefaultValue())) {
help.append(
"; default if option present: '")
.append(option
.specifiedDefaultValue())
.append("'");
}
if (!"".equals(option
.unspecifiedDefaultValue())
&& !NULL
.equals(option
.unspecifiedDefaultValue())) {
help.append(
"; default if option not present: '")
.append(option
.unspecifiedDefaultValue())
.append("'");
}
}
help.append(option.mandatory() ? " (mandatory) "
: "");
// Store details for later
key = "--" + key;
optionKeys.add(key);
optionDetails.put(key, help.toString());
// Include it in the mandatory syntax
if (option.mandatory()) {
cmdSyntax.append(" ").append(key);
}
}
}
}
// Make a variable list element
Element variableListElement = document
.createElement("variablelist");
boolean anyVars = false;
for (final String optionKey : optionKeys) {
anyVars = true;
final String help = optionDetails.get(optionKey);
variableListElement
.appendChild(new XmlElementBuilder(
"varlistentry", document)
.addChild(
new XmlElementBuilder(
"term", document)
.setText(optionKey)
.build())
.addChild(
new XmlElementBuilder(
"listitem",
document)
.addChild(
new XmlElementBuilder(
"para",
document)
.setText(
help)
.build())
.build()).build());
}
if (!anyVars) {
variableListElement = new XmlElementBuilder("para",
document)
.setText(
"This command does not accept any options.")
.build();
}
// Now we've figured out the options, store this
// individual command
final CDATASection progList = document
.createCDATASection(cmdSyntax.toString());
final String safeName = cmd.value()[0]
.replace("\\", "BCK").replace("/", "FWD")
.replace("*", "ASX");
final Element element = new XmlElementBuilder(
"section", document)
.addAttribute(
"xml:id",
"command-index-"
+ safeName.toLowerCase()
.replace(' ', '-'))
.addChild(
new XmlElementBuilder("title", document)
.setText(cmd.value()[0])
.build())
.addChild(
new XmlElementBuilder("para", document)
.setText(cmd.help()).build())
.addChild(
new XmlElementBuilder("programlisting",
document).addChild(progList)
.build())
.addChild(variableListElement).build();
individualCommands.put(cmdSyntax.toString(), element);
}
}
final Element topSection = document.createElement("section");
topSection.setAttribute("xml:id", "command-index-"
+ section.toLowerCase().replace(' ', '-'));
topSection.appendChild(new XmlElementBuilder("title", document)
.setText(section).build());
topSection.appendChild(new XmlElementBuilder("para", document)
.setText(
section + " are contained in "
+ target.getClass().getName() + ".")
.build());
for (final Element value : individualCommands.values()) {
topSection.appendChild(value);
}
builtSections.add(topSection);
}
final Element appendix = document.createElement("appendix");
appendix.setAttribute("xmlns", "http://docbook.org/ns/docbook");
appendix.setAttribute("version", "5.0");
appendix.setAttribute("xml:id", "command-index");
appendix.appendChild(new XmlElementBuilder("title", document)
.setText("Command Index").build());
appendix.appendChild(new XmlElementBuilder("para", document)
.setText(
"This appendix was automatically built from Roo "
+ AbstractShell.versionInfo() + ".")
.build());
appendix.appendChild(new XmlElementBuilder("para", document)
.setText(
"Commands are listed in alphabetic order, and are shown in monospaced font with any mandatory options you must specify when using the command. Most commands accept a large number of options, and all of the possible options for each command are presented in this appendix.")
.build());
for (final Element section : builtSections) {
appendix.appendChild(section);
}
document.appendChild(appendix);
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final Transformer transformer = XmlUtils
.createIndentingTransformer();
// Causes an
// "Error reported by XML parser: Multiple notations were used which had the name 'linespecific', but which were not determined to be duplicates."
// when creating the DocBook
// transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
// "-//OASIS//DTD DocBook XML V4.5//EN");
// transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
// "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd");
XmlUtils.writeXml(transformer, byteArrayOutputStream, document);
try {
final File output = new File(f, "appendix-command-index.xml");
FileUtils.writeByteArrayToFile(output,
byteArrayOutputStream.toByteArray());
}
catch (final IOException ioe) {
throw new IllegalStateException(ioe);
}
finally {
IOUtils.closeQuietly(byteArrayOutputStream);
}
}
}
private Collection<MethodTarget> locateTargets(final String buffer,
final boolean strictMatching,
final boolean checkAvailabilityIndicators) {
Validate.notNull(buffer, "Buffer required");
final Collection<MethodTarget> result = new HashSet<MethodTarget>();
// The reflection could certainly be optimised, but it's good enough for
// now (and cached reflection
// is unlikely to be noticeable to a human being using the CLI)
for (final CommandMarker command : commands) {
for (final Method method : command.getClass().getMethods()) {
final CliCommand cmd = method.getAnnotation(CliCommand.class);
if (cmd != null) {
// We have a @CliCommand.
if (checkAvailabilityIndicators) {
// Decide if this @CliCommand is available at this
// moment
Boolean available = null;
for (final String value : cmd.value()) {
final MethodTarget mt = getAvailabilityIndicator(value);
if (mt != null) {
Validate.isTrue(available == null,
"More than one availability indicator is defined for '"
+ method.toGenericString()
+ "'");
try {
available = (Boolean) mt.getMethod()
.invoke(mt.getTarget());
// We should "break" here, but we loop over
// all to ensure no conflicting availability
// indicators are defined
}
catch (final Exception e) {
available = false;
}
}
}
// Skip this @CliCommand if it's not available
if (available != null && !available) {
continue;
}
}
for (final String value : cmd.value()) {
final String remainingBuffer = isMatch(buffer, value,
strictMatching);
if (remainingBuffer != null) {
result.add(new MethodTarget(method, command,
remainingBuffer, value));
}
}
}
}
}
return result;
}
/**
* Normalises the given raw user input string ready for parsing
*
* @param rawInput the string to normalise; can't be <code>null</code>
* @return a non-<code>null</code> string
*/
String normalise(final String rawInput) {
// Replace all multiple spaces with a single space and then trim
return rawInput.replaceAll(" +", " ").trim();
}
public void obtainHelp(
@CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for") String buffer) {
synchronized (mutex) {
if (buffer == null) {
buffer = "";
}
final StringBuilder sb = new StringBuilder();
// Figure out if there's a single command we can offer help for
final Collection<MethodTarget> matchingTargets = locateTargets(
buffer, false, false);
if (matchingTargets.size() == 1) {
// Single command help
final MethodTarget methodTarget = matchingTargets.iterator()
.next();
// Argument conversion time
final Annotation[][] parameterAnnotations = methodTarget
.getMethod().getParameterAnnotations();
if (parameterAnnotations.length > 0) {
// Offer specified help
final CliCommand cmd = methodTarget.getMethod()
.getAnnotation(CliCommand.class);
Validate.notNull(cmd, "CliCommand not found");
for (final String value : cmd.value()) {
sb.append("Keyword: ").append(value)
.append(LINE_SEPARATOR);
}
sb.append("Description: ").append(cmd.help())
.append(LINE_SEPARATOR);
for (final Annotation[] annotations : parameterAnnotations) {
CliOption cliOption = null;
for (final Annotation a : annotations) {
if (a instanceof CliOption) {
cliOption = (CliOption) a;
for (String key : cliOption.key()) {
if ("".equals(key)) {
key = "** default **";
}
sb.append(" Keyword: ")
.append(key).append(LINE_SEPARATOR);
}
sb.append(" Help: ")
.append(cliOption.help())
.append(LINE_SEPARATOR);
sb.append(" Mandatory: ")
.append(cliOption.mandatory())
.append(LINE_SEPARATOR);
sb.append(" Default if specified: '")
.append(cliOption
.specifiedDefaultValue())
.append("'").append(LINE_SEPARATOR);
sb.append(" Default if unspecified: '")
.append(cliOption
.unspecifiedDefaultValue())
.append("'").append(LINE_SEPARATOR);
sb.append(LINE_SEPARATOR);
}
}
Validate.notNull(cliOption,
"CliOption not found for parameter '%s'",
Arrays.toString(annotations));
}
}
// Only a single argument, so default to the normal help
// operation
}
final SortedSet<String> result = new TreeSet<String>(COMPARATOR);
for (final MethodTarget mt : matchingTargets) {
final CliCommand cmd = mt.getMethod().getAnnotation(
CliCommand.class);
if (cmd != null) {
for (final String value : cmd.value()) {
if ("".equals(cmd.help())) {
result.add("* " + value);
}
else {
result.add("* " + value + " - " + cmd.help());
}
}
}
}
for (final String s : result) {
sb.append(s).append(LINE_SEPARATOR);
}
LOGGER.info(sb.toString());
LOGGER.warning("** Type 'hint' (without the quotes) and hit ENTER for step-by-step guidance **"
+ LINE_SEPARATOR);
}
}
public ParseResult parse(final String rawInput) {
synchronized (mutex) {
Validate.notNull(rawInput, "Raw input required");
final String input = normalise(rawInput);
// Locate the applicable targets which match this buffer
final Collection<MethodTarget> matchingTargets = locateTargets(
input, true, true);
if (matchingTargets.isEmpty()) {
// Before we just give up, let's see if we can offer a more
// informative message to the user
// by seeing the command is simply unavailable at this point in
// time
CollectionUtils.populate(matchingTargets,
locateTargets(input, true, false));
if (matchingTargets.isEmpty()) {
commandNotFound(LOGGER, input);
}
else {
LOGGER.warning("Command '"
+ input
+ "' was found but is not currently available (type 'help' then ENTER to learn about this command)");
}
return null;
}
if (matchingTargets.size() > 1) {
LOGGER.warning("Ambigious command '" + input
+ "' (for assistance press "
+ AbstractShell.completionKeys
+ " or type \"hint\" then hit ENTER)");
return null;
}
final MethodTarget methodTarget = matchingTargets.iterator().next();
// Argument conversion time
final Annotation[][] parameterAnnotations = methodTarget
.getMethod().getParameterAnnotations();
if (parameterAnnotations.length == 0) {
// No args
return new ParseResult(methodTarget.getMethod(),
methodTarget.getTarget(), null);
}
// Oh well, we need to convert some arguments
final List<Object> arguments = new ArrayList<Object>(methodTarget
.getMethod().getParameterTypes().length);
// Attempt to parse
Map<String, String> options = null;
try {
options = ParserUtils.tokenize(methodTarget
.getRemainingBuffer());
}
catch (final IllegalArgumentException e) {
LOGGER.warning(StringUtils.defaultIfBlank(
ExceptionUtils.getRootCauseMessage(e), e.getMessage()));
return null;
}
final Set<CliOption> cliOptions = getCliOptions(parameterAnnotations);
for (final CliOption cliOption : cliOptions) {
final Class<?> requiredType = methodTarget.getMethod()
.getParameterTypes()[arguments.size()];
if (cliOption.systemProvided()) {
Object result;
if (SimpleParser.class.isAssignableFrom(requiredType)) {
result = this;
}
else {
LOGGER.warning("Parameter type '" + requiredType
+ "' is not system provided");
return null;
}
arguments.add(result);
continue;
}
// Obtain the value the user specified, taking care to ensure
// they only specified it via a single alias
String value = null;
String sourcedFrom = null;
for (final String possibleKey : cliOption.key()) {
if (options.containsKey(possibleKey)) {
if (sourcedFrom != null) {
LOGGER.warning("You cannot specify option '"
+ possibleKey
+ "' when you have also specified '"
+ sourcedFrom + "' in the same command");
return null;
}
sourcedFrom = possibleKey;
value = options.get(possibleKey);
}
}
// Ensure the user specified a value if the value is mandatory
if (StringUtils.isBlank(value) && cliOption.mandatory()) {
if ("".equals(cliOption.key()[0])) {
final StringBuilder message = new StringBuilder(
"You must specify a default option ");
if (cliOption.key().length > 1) {
message.append("(otherwise known as option '")
.append(cliOption.key()[1]).append("') ");
}
message.append("for this command");
LOGGER.warning(message.toString());
}
else {
LOGGER.warning("You must specify option '"
+ cliOption.key()[0] + "' for this command");
}
return null;
}
// Accept a default if the user specified the option, but didn't
// provide a value
if ("".equals(value)) {
value = cliOption.specifiedDefaultValue();
}
// Accept a default if the user didn't specify the option at all
if (value == null) {
value = cliOption.unspecifiedDefaultValue();
}
// Special token that denotes a null value is sought (useful for
// default values)
if (NULL.equals(value)) {
if (requiredType.isPrimitive()) {
LOGGER.warning("Nulls cannot be presented to primitive type "
+ requiredType.getSimpleName()
+ " for option '"
+ StringUtils.join(cliOption.key(), ",") + "'");
return null;
}
arguments.add(null);
continue;
}
// Change the empty string marker back into an empty string now
// that we are passed the default and null value checks.
if (EMPTY.equals(value)) {
value = "";
}
// Now we're ready to perform a conversion
try {
CliOptionContext
.setOptionContext(cliOption.optionContext());
CliSimpleParserContext.setSimpleParserContext(this);
Object result;
Converter<?> c = null;
for (final Converter<?> candidate : converters) {
if (candidate.supports(requiredType,
cliOption.optionContext())) {
// Found a usable converter
c = candidate;
break;
}
}
if (c == null) {
throw new IllegalStateException(
"TODO: Add basic type conversion");
// TODO Fall back to a normal SimpleTypeConverter and
// attempt conversion
// SimpleTypeConverter simpleTypeConverter = new
// SimpleTypeConverter();
// result =
// simpleTypeConverter.convertIfNecessary(value,
// requiredType, mp);
}
// Use the converter
result = c.convertFromText(value, requiredType,
cliOption.optionContext());
// If the option has been specified to be mandatory then the
// result should never be null
if (result == null && cliOption.mandatory()) {
throw new IllegalStateException();
}
arguments.add(result);
}
catch (final RuntimeException e) {
LOGGER.warning(e.getClass().getName()
+ ": Failed to convert '" + value + "' to type "
+ requiredType.getSimpleName() + " for option '"
+ StringUtils.join(cliOption.key(), ",") + "'");
if (StringUtils.isNotBlank(e.getMessage())) {
LOGGER.warning(e.getMessage());
}
return null;
}
finally {
CliOptionContext.resetOptionContext();
CliSimpleParserContext.resetSimpleParserContext();
}
}
// Check for options specified by the user but are unavailable for
// the command
final Set<String> unavailableOptions = getSpecifiedUnavailableOptions(
cliOptions, options);
if (!unavailableOptions.isEmpty()) {
final StringBuilder message = new StringBuilder();
if (unavailableOptions.size() == 1) {
message.append("Option '")
.append(unavailableOptions.iterator().next())
.append("' is not available for this command. ");
}
else {
message.append("Options ")
.append(collectionToDelimitedString(
unavailableOptions, ", ", "'", "'"))
.append(" are not available for this command. ");
}
message.append("Use tab assist or the \"help\" command to see the legal options");
LOGGER.warning(message.toString());
return null;
}
return new ParseResult(methodTarget.getMethod(),
methodTarget.getTarget(), arguments.toArray());
}
}
private String collectionToDelimitedString(final Collection<?> coll,
final String delim, final String prefix, final String suffix) {
if (CollectionUtils.isEmpty(coll)) {
return "";
}
final StringBuilder sb = new StringBuilder();
final Iterator<?> it = coll.iterator();
while (it.hasNext()) {
sb.append(prefix).append(it.next()).append(suffix);
if (it.hasNext() && delim != null) {
sb.append(delim);
}
}
return sb.toString();
}
public final void remove(final CommandMarker command) {
synchronized (mutex) {
commands.remove(command);
for (final Method m : command.getClass().getMethods()) {
final CliAvailabilityIndicator availability = m
.getAnnotation(CliAvailabilityIndicator.class);
if (availability != null) {
for (final String cmd : availability.value()) {
availabilityIndicators.remove(cmd);
}
}
}
}
}
public final void remove(final Converter<?> converter) {
synchronized (mutex) {
converters.remove(converter);
}
}
}