/* dCache - http://www.dcache.org/
*
* Copyright (C) 2013 - 2016 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dmg.util.command;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
import com.google.common.collect.Multimap;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.dcache.util.Strings;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.transform;
import static java.util.Arrays.asList;
/**
* Abstract base class for help printers generating man-page style textual help.
*/
public abstract class TextHelpPrinter implements AnnotatedCommandHelpPrinter
{
// Split between any
//
// [ ] | ...
//
// and any sequence of upper case letters.
private static final Pattern VALUESPEC_SEPARATOR =
Pattern.compile("(?<=[\\[\\]|]|\\.{3})|(?=[\\[\\]|]|\\.{3})|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Z])(?=[^A-Z])");
private static final int WIDTH = 72;
private static final Predicate<? super Field> shouldBeDocumented =
new Predicate<Field>()
{
@Override
public boolean apply(Field field)
{
Argument argument = field.getAnnotation(Argument.class);
ExpandWith expandWith = field.getAnnotation(ExpandWith.class);
/* Arguments that are not required might have a default value and should thus
* be included in the help output.
*/
return argument != null && (expandWith != null || !argument.usage().isEmpty() || !argument.required());
}
};
private <T> Iterable<String> literal(T[] values)
{
return transform(asList(values), new Function<T, String>()
{
@Override
public String apply(T s)
{
return literal(s.toString());
}
});
}
protected String valuespec(String valuespec)
{
StringBuilder out = new StringBuilder();
for (String s : VALUESPEC_SEPARATOR.split(valuespec, 0)) {
switch (s) {
case "[":
case "]":
case "|":
case "...":
out.append(s);
break;
default:
if (CharMatcher.javaUpperCase().matchesAllOf(s)) {
out.append(value(s));
} else {
out.append(literal(s));
}
break;
}
}
return out.toString();
}
private String getMetaVar(Class<?> type, Option option)
{
if (!option.metaVar().isEmpty()) {
return value(option.metaVar().toUpperCase());
}
if (!option.valueSpec().isEmpty()) {
return valuespec(option.valueSpec());
}
if (option.values().length > 0) {
return Joiner.on("|").join(literal(option.values()));
}
if (type.isEnum()) {
return Joiner.on("|").join(literal(type.getEnumConstants()));
}
return value(type.getSimpleName().toUpperCase());
}
private String getMetaVar(Field field, Argument argument)
{
if (!argument.valueSpec().isEmpty()) {
return valuespec(argument.valueSpec());
}
if (!argument.metaVar().isEmpty()) {
return value(argument.metaVar().toUpperCase());
}
return value(field.getName().toUpperCase());
}
private String getSignature(Class<?> clazz)
{
StringBuilder signature = new StringBuilder();
Multimap<String,Field> options = AnnotatedCommandUtils.getOptionsByCategory(clazz);
for (Field field: options.values()) {
Class<?> type = field.getType();
Option option = field.getAnnotation(Option.class);
if (option != null) {
if (!type.isArray()) {
if (!option.required()) {
signature.append("[");
}
signature.append(literal("-" + option.name()));
if (!Boolean.class.equals(type) && !Boolean.TYPE
.equals(type)) {
signature.append("=")
.append(getMetaVar(type, option));
}
if (!option.required()) {
signature.append("]");
}
} else if (option.separator().isEmpty()) {
if (!option.required()) {
signature.append("[");
}
signature.append(literal("-" + option.name()));
signature.append("=").append(getMetaVar(type
.getComponentType(), option));
if (!option.required()) {
signature.append("]");
}
signature.append(value("..."));
} else {
if (!option.required()) {
signature.append("[");
}
String metaVar = getMetaVar(type
.getComponentType(), option);
signature.append(literal("-" + option.name()));
signature.append("=").append(metaVar);
signature.append("[").append(option.separator())
.append(metaVar).append("]").append(value("..."));
if (!option.required()) {
signature.append("]").append(value("..."));
}
}
signature.append(" ");
}
CommandLine commandLine = field.getAnnotation(CommandLine.class);
if (commandLine != null && commandLine.allowAnyOption()) {
signature.append(valuespec(commandLine.valueSpec())).append(" ");
}
}
for (Field field: AnnotatedCommandUtils.getArguments(clazz)) {
Argument argument = field.getAnnotation(Argument.class);
String metaVar = getMetaVar(field, argument);
if (argument.required()) {
signature.append(metaVar);
} else {
signature.append("[").append(metaVar).append("]");
}
if (field.getType().isArray()) {
signature.append("...");
}
signature.append(" ");
}
return signature.toString();
}
private String getShortSignature(Class<?> clazz)
{
StringBuilder signature = new StringBuilder();
if (!AnnotatedCommandUtils.getOptionsByCategory(clazz).isEmpty()) {
signature.append("[OPTIONS] ");
}
for (Field field: AnnotatedCommandUtils.getArguments(clazz)) {
Argument argument = field.getAnnotation(Argument.class);
String metaVar = getMetaVar(field, argument);
if (argument.required()) {
signature.append(metaVar);
} else {
signature.append("[").append(metaVar).append("]");
}
if (field.getType().isArray()) {
signature.append("...");
}
signature.append(" ");
}
return signature.toString();
}
@Override
public String getHelpHint(Command command, Class<?> clazz)
{
String hint = (command.hint().isEmpty() ? "" : "# " + command.hint());
String signature = getSignature(clazz);
if (plainLength(signature) + plainLength(hint) > 78) {
signature = getShortSignature(clazz);
}
return (signature.isEmpty() ? "" : signature + " ") + hint;
}
@Override
public String getHelp(Object instance)
{
Class<?> clazz = instance.getClass();
Command command = clazz.getAnnotation(Command.class);
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out);
writer.println(heading("NAME"));
writer.append(" ").append(literal(command.name()));
if (!command.hint().isEmpty()) {
writer.append(" -- ").append(command.hint());
}
writer.println();
writer.println();
writer.println(heading("SYNOPSIS"));
writer.append(Strings.wrap(" ", literal(command.name()) + " " + getSignature(clazz), WIDTH));
writer.println();
if (clazz.getAnnotation(Deprecated.class) != null) {
writer.append(Strings.wrap(" ", "This command is deprecated and will be removed in a future release.", WIDTH));
writer.println();
}
if (!command.description().isEmpty()) {
writer.println(heading("DESCRIPTION"));
writer.append(Strings.wrap(" ", command.description(), WIDTH));
}
writer.println();
List<Field> arguments = AnnotatedCommandUtils.getArguments(clazz);
if (!arguments.isEmpty() && any(arguments, shouldBeDocumented)) {
writer.println(heading("ARGUMENTS"));
for (Field field : arguments) {
Argument argument = field.getAnnotation(Argument.class);
writer.append(" ").println(getMetaVar(field, argument));
String help = argument.usage();
if (!argument.required()) {
help = Joiner.on(' ').join(help, getDefaultDescription(instance, field));
}
if (field.getAnnotation(ExpandWith.class) != null) {
help = Joiner.on(' ').join(help, "Glob patterns will be expanded.");
}
if (!help.isEmpty()) {
writer.append(Strings.wrap(" ", help, WIDTH));
}
}
writer.println();
}
Multimap<String,Field> options = AnnotatedCommandUtils.getOptionsByCategory(clazz);
if (!options.isEmpty()) {
writer.println(heading("OPTIONS"));
for (Map.Entry<String,Collection<Field>> category: options.asMap().entrySet()) {
if (!category.getKey().isEmpty()) {
writer.println();
writer.append(" ").println(heading(category.getKey() + ":"));
}
for (Field field: category.getValue()) {
Class<?> type = field.getType();
Option option = field.getAnnotation(Option.class);
if (option != null) {
writer.append(" ").append(literal(" -" + option.name()));
if (!type.isArray()) {
if (!Boolean.class.equals(type) && !Boolean.TYPE.equals(type)) {
writer.append("=").append(getMetaVar(type, option));
}
} else if (option.separator().isEmpty()) {
writer.append("=").append(getMetaVar(type.getComponentType(), option));
writer.append(value("..."));
} else {
String metaVar = getMetaVar(type.getComponentType(), option);
writer.append("=").append(metaVar);
writer.append("[").append(option.separator()).append(metaVar).append("]");
writer.append(value("..."));
}
writer.println();
String usage = option.usage();
if (!option.required()) {
usage = Joiner.on(' ').join(usage, getDefaultDescription(instance, field));
}
if (!usage.isEmpty()) {
writer.append(Strings.wrap(" ", usage, WIDTH));
}
}
CommandLine cmd = field.getAnnotation(CommandLine.class);
if (cmd != null && cmd.allowAnyOption()) {
writer.append(" ").append(valuespec(cmd.valueSpec())).println();
String usage = cmd.usage();
if (!usage.isEmpty()) {
writer.append(Strings.wrap(" ", usage, WIDTH));
}
}
}
}
}
writer.flush();
return out.toString();
}
private String getDefaultDescription(Object instance, Field field)
{
try {
field.setAccessible(true);
Object value = field.get(instance);
if (value != null && hasDefaultDescription(field.getType(), value)) {
return "Defaults to " + literal(value) + '.';
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return "";
}
private String literal(Object value)
{
if (value.getClass().isArray()) {
int length = Array.getLength(value);
StringBuilder s = new StringBuilder();
if (length > 0) {
s.append(literal(Array.get(value, 0).toString()));
for (int i = 1; i < length; i++) {
s.append(' ').append(literal(Array.get(value, i).toString()));
}
}
return s.toString();
}
return literal(value.toString());
}
private boolean hasDefaultDescription(Class<?> type, Object value)
{
if (type.isArray()) {
if (Array.getLength(value) == 0) {
return false;
}
} else if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) {
if (!(Boolean) value) {
return false;
}
}
return true;
}
protected int plainLength(String s)
{
return s.length();
}
protected abstract String value(String value);
protected abstract String literal(String option);
protected abstract String heading(String heading);
}