dmg.util.command.TextHelpPrinter.java Source code

Java tutorial

Introduction

Here is the source code for dmg.util.command.TextHelpPrinter.java

Source

/* 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), (s) -> 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);
}