/*
* Copyright 2013-2014 the original author or authors.
*
* 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.
*/
package org.springframework.xd.shell.util;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.shell.CommandLine;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.xd.shell.command.AggregateCounterCommands;
import org.springframework.xd.shell.command.ConfigCommands;
import org.springframework.xd.shell.command.CounterCommands;
import org.springframework.xd.shell.command.FieldValueCounterCommands;
import org.springframework.xd.shell.command.GaugeCommands;
import org.springframework.xd.shell.command.HttpCommands;
import org.springframework.xd.shell.command.JobCommands;
import org.springframework.xd.shell.command.ModuleCommands;
import org.springframework.xd.shell.command.RichGaugeCommands;
import org.springframework.xd.shell.command.RuntimeCommands;
import org.springframework.xd.shell.command.StreamCommands;
import org.springframework.xd.shell.hadoop.ConfigurationCommands;
import org.springframework.xd.shell.hadoop.FsShellCommands;
/**
* A quick and dirty tool to collect command help() text and generate an asciidoc page. Also enforces some constraints
* on commands. Can be run as a unit test to enforce constraints only.
*
* @author Eric Bottard
* @author Gunnar Hillert
*
*/
public class ReferenceDoc {
private final static Pattern COMMAND_FORMAT = Pattern.compile("[a-z][a-zA-Z \\-]+");
private final static Pattern OPTION_FORMAT = Pattern.compile("[a-z][a-zA-Z ]+");
/* Enforce uppercase first, prevent final dot. */
private final static Pattern COMMAND_HELP = Pattern.compile("[A-Z].+[^\\.]$");
/* Enforce lowercase first, prevent final dot. */
private final static Pattern OPTION_HELP = Pattern.compile("[a-z].+[^\\.]$");
/**
* A mapping from class to Title in the doc. Insertion order will become rendering order.
*/
private Map<Class<? extends CommandMarker>, String> titles = new LinkedHashMap<Class<? extends CommandMarker>, String>();
private PrintStream out = new PrintStream(new NullOutputStream());
private static class NullOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
}
}
public ReferenceDoc() {
/*
* Set titles for commands. Please note that insertion order matters!
*/
titles.put(ConfigCommands.class, "Configuration Commands");
// ===== Runtime Containers/Modules ======
titles.put(RuntimeCommands.class, "Runtime Commands");
// ===== Streams etc. ======
titles.put(StreamCommands.class, "Stream Commands");
titles.put(JobCommands.class, "Job Commands");
titles.put(ModuleCommands.class, "Module Commands");
// ======= Analytics =======
// Use of repeated title here on purpose
titles.put(CounterCommands.class, "Metrics Commands");
titles.put(FieldValueCounterCommands.class, "Metrics Commands");
titles.put(AggregateCounterCommands.class, "Metrics Commands");
titles.put(GaugeCommands.class, "Metrics Commands");
titles.put(RichGaugeCommands.class, "Metrics Commands");
// ======= Http Post =======
titles.put(HttpCommands.class, "Http Commands");
// ======== Hadoop =========
titles.put(ConfigurationCommands.class, "Hadoop Configuration Commands");
titles.put(FsShellCommands.class, "Hadoop FileSystem Commands");
}
private static final class CommandsCollector implements MethodCallback {
private final Map<CliCommand, List<CliOption>> commands;
private CommandsCollector(Map<CliCommand, List<CliOption>> commands) {
this.commands = commands;
}
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
List<CliOption> list = new ArrayList<CliOption>();
commands.put(method.getAnnotation(CliCommand.class), list);
for (Annotation[] anns : method.getParameterAnnotations()) {
for (Annotation ann : anns) {
if (ann instanceof CliOption) {
list.add((CliOption) ann);
break;
}
}
}
}
}
public static void main(String[] args) throws Exception {
ReferenceDoc doc = new ReferenceDoc();
doc.out = args.length == 1 ? new PrintStream(args[0]) : System.out;
doc.doIt();
}
@Test
public void doIt() {
out.println("[[shell-command-reference]]");
out.println("ifndef::env-github[]");
out.println("== XD Shell Command Reference");
out.println("endif::[]");
out.println("Below is a reference list of all Spring XD specific commands you can use in the link:Shell#interactive-shell[XD Shell].\n");
GenericApplicationContext ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("commandLine", new CommandLine(null, 100, null, false));
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx);
reader.loadBeanDefinitions("classpath*:META-INF/spring/spring-shell-plugin.xml");
ctx.refresh();
Comparator<Class<? extends CommandMarker>> comparator = new Comparator<Class<? extends CommandMarker>>() {
@Override
public int compare(Class<? extends CommandMarker> arg0, Class<? extends CommandMarker> arg1) {
List<Class<? extends CommandMarker>> sorted = new ArrayList<Class<? extends CommandMarker>>(
titles.keySet());
int diff = sorted.indexOf(arg0) - sorted.indexOf(arg1);
return diff != 0 ? diff : arg0.getSimpleName().compareTo(arg1.getSimpleName());
}
};
final Map<Class<? extends CommandMarker>, Map<CliCommand, List<CliOption>>> plugins = new TreeMap<Class<? extends CommandMarker>, Map<CliCommand, List<CliOption>>>(
comparator);
Map<String, CommandMarker> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, CommandMarker.class);
final MethodFilter filter = new MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getAnnotation(CliCommand.class) != null;
}
};
for (CommandMarker plugin : beans.values()) {
LinkedHashMap<CliCommand, List<CliOption>> commands = new LinkedHashMap<CliCommand, List<CliOption>>();
plugins.put(plugin.getClass(), commands);
ReflectionUtils.doWithMethods(plugin.getClass(), new CommandsCollector(commands), filter);
}
String lastTitleUsed = null;
for (Class<? extends CommandMarker> plugin : plugins.keySet()) {
// == Stream Commands
String title = titleFor(plugin);
if (lastTitleUsed == null || !lastTitleUsed.equals(title)) {
out.printf("=== %s%n", title);
lastTitleUsed = title;
}
Map<CliCommand, List<CliOption>> commands = plugins.get(plugin);
for (CliCommand command : commands.keySet()) {
// === stream create
out.printf("==== %s%n", pt(check(command.value()[0], COMMAND_FORMAT)));
// Create a new stream definition.
out.printf("%s.%n%n", pt(check(command.help(), COMMAND_HELP)));
// stream create [--name]=<name> [--definition=<definition>]
out.printf(" %s", command.value()[0]);
for (CliOption option : commands.get(command)) {
String paramName = check(paramName(option), OPTION_FORMAT);
String optionText = String.format("<%s>", paramName);
if (valueOptional(option)) {
optionText = String.format("--%s [%s]", paramName, optionText);
}
else if (keyOptional(option)) {
optionText = String.format("[--%s] %s", paramName, optionText);
}
else {
optionText = String.format("--%s %s", paramName, optionText);
}
if (!option.mandatory()) {
optionText = String.format("[%s]", optionText);
}
if (option.mandatory() && valueOptional(option)) {
// This combination does not make sense. Or does it?
throw new IllegalStateException("" + command + " " + option);
}
out.printf(" %s", optionText);
}
out.println("");
out.println();
// *definition*:: the stream definition
for (CliOption option : commands.get(command)) {
out.printf("*%s*:: %s.", pt(paramName(option)), pt(check(option.help(), OPTION_HELP)));
if (!option.mandatory()) {
// There can be non-mandatory, w/o default options (e.g. mutually exclusive, with 1 required,
// options)
if (!"__NULL__".equals(option.unspecifiedDefaultValue())) {
out.printf(" *(default: `%s`", option.unspecifiedDefaultValue());
if (valueOptional(option)) {
if (option.specifiedDefaultValue().equals(option.unspecifiedDefaultValue())) {
throw new IllegalStateException("" + option);
}
out.printf(", or `%s` if +--%s+ is specified without a value",
option.specifiedDefaultValue(), paramName(option));
}
out.printf(")*");
}
}
else {
out.printf(" *(required)*");
}
out.printf("%n");
}
out.println();
}
out.println();
}
ctx.close();
}
private boolean valueOptional(CliOption option) {
return !"__NULL__".equals(option.specifiedDefaultValue());
}
private String titleFor(Class<? extends CommandMarker> plugin) {
if (!titles.containsKey(plugin)) {
throw new IllegalArgumentException("Missing title for " + plugin);
}
return titles.get(plugin);
}
private boolean keyOptional(CliOption option) {
return Arrays.asList(option.key()).contains("");
}
private String check(String candidate, Pattern regex) {
if (!regex.matcher(candidate).matches()) {
throw new IllegalArgumentException("'" + candidate + "' should match " + regex);
}
return candidate;
}
/**
* Return an asciidoc passthrough version of some text, in case the original text contains characters
* that would be (mis)interpreted by asciidoc.
*/
private String pt(String original) {
return "$$" + original + "$$";
}
private String paramName(CliOption option) {
String[] possibleValues = option.key();
for (String s : possibleValues) {
if (!"".equals(s)) {
return s;
}
}
throw new IllegalStateException("Option should have a non empty key: " + option.help());
}
}