org.apache.geode.management.internal.cli.help.Helper.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.geode.management.internal.cli.help.Helper.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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.apache.geode.management.internal.cli.help;

import org.apache.commons.lang.StringUtils;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.internal.cli.GfshParser;
import org.apache.geode.management.internal.cli.i18n.CliStrings;
import org.springframework.shell.core.MethodTarget;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;

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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * 
 * 
 * @since GemFire 7.0
 */
public class Helper {

    static final String NAME_NAME = "NAME";
    static final String SYNONYMS_NAME = "SYNONYMS";
    static final String SYNOPSIS_NAME = "SYNOPSIS";
    static final String SYNTAX_NAME = "SYNTAX";
    static final String OPTIONS_NAME = "PARAMETERS";
    static final String IS_AVAILABLE_NAME = "IS AVAILABLE";

    static final String REQUIRED_SUB_NAME = "Required: ";
    static final String SYNONYMS_SUB_NAME = "Synonyms: ";
    static final String SPECIFIEDDEFAULTVALUE_SUB_NAME = "Default (if the parameter is specified without value): ";
    static final String UNSPECIFIEDDEFAULTVALUE_VALUE_SUB_NAME = "Default (if the parameter is not specified): ";

    static final String VALUE_FIELD = "value";
    static final String TRUE_TOKEN = "true";
    static final String FALSE_TOKEN = "false";
    static final String AVAILABLE = "Available";
    static final String NOT_AVAILABLE = "Not Available";

    private final Map<String, Topic> topics = new HashMap<>();
    private final Map<String, Method> commands = new TreeMap<String, Method>();
    private final Map<String, MethodTarget> availabilityIndicators = new HashMap<String, MethodTarget>();

    public Helper() {
        initTopic(CliStrings.DEFAULT_TOPIC_GEODE, CliStrings.DEFAULT_TOPIC_GEODE__DESC);
        initTopic(CliStrings.TOPIC_GEODE_REGION, CliStrings.TOPIC_GEODE_REGION__DESC);
        initTopic(CliStrings.TOPIC_GEODE_WAN, CliStrings.TOPIC_GEODE_WAN__DESC);
        initTopic(CliStrings.TOPIC_GEODE_JMX, CliStrings.TOPIC_GEODE_JMX__DESC);
        initTopic(CliStrings.TOPIC_GEODE_DISKSTORE, CliStrings.TOPIC_GEODE_DISKSTORE__DESC);
        initTopic(CliStrings.TOPIC_GEODE_LOCATOR, CliStrings.TOPIC_GEODE_LOCATOR__DESC);
        initTopic(CliStrings.TOPIC_GEODE_SERVER, CliStrings.TOPIC_GEODE_SERVER__DESC);
        initTopic(CliStrings.TOPIC_GEODE_MANAGER, CliStrings.TOPIC_GEODE_MANAGER__DESC);
        initTopic(CliStrings.TOPIC_GEODE_STATISTICS, CliStrings.TOPIC_GEODE_STATISTICS__DESC);
        initTopic(CliStrings.TOPIC_GEODE_LIFECYCLE, CliStrings.TOPIC_GEODE_LIFECYCLE__DESC);
        initTopic(CliStrings.TOPIC_GEODE_M_AND_M, CliStrings.TOPIC_GEODE_M_AND_M__DESC);
        initTopic(CliStrings.TOPIC_GEODE_DATA, CliStrings.TOPIC_GEODE_DATA__DESC);
        initTopic(CliStrings.TOPIC_GEODE_CONFIG, CliStrings.TOPIC_GEODE_CONFIG__DESC);
        initTopic(CliStrings.TOPIC_GEODE_FUNCTION, CliStrings.TOPIC_GEODE_FUNCTION__DESC);
        initTopic(CliStrings.TOPIC_GEODE_HELP, CliStrings.TOPIC_GEODE_HELP__DESC);
        initTopic(CliStrings.TOPIC_GEODE_DEBUG_UTIL, CliStrings.TOPIC_GEODE_DEBUG_UTIL__DESC);
        initTopic(CliStrings.TOPIC_GFSH, CliStrings.TOPIC_GFSH__DESC);
        initTopic(CliStrings.TOPIC_LOGS, CliStrings.TOPIC_LOGS__DESC);
        initTopic(CliStrings.TOPIC_CLIENT, CliStrings.TOPIC_CLIENT__DESC);
    }

    private void initTopic(String topic, String desc) {
        topics.put(topic, new Topic(topic, desc));
    }

    public void addCommand(CliCommand command, Method commandMethod) {
        // put all the command synonyms in the command map
        Arrays.stream(command.value()).forEach(cmd -> {
            commands.put(cmd, commandMethod);
        });

        // resolve the hint message for each method
        CliMetaData cliMetaData = commandMethod.getDeclaredAnnotation(CliMetaData.class);
        if (cliMetaData == null)
            return;
        String[] related = cliMetaData.relatedTopic();

        // for hint message, we only need to show the first synonym
        String commandString = command.value()[0];
        if (related == null) {
            return;
        }
        Arrays.stream(related).forEach(topic -> {
            Topic foundTopic = topics.get(topic);
            if (foundTopic == null) {
                throw new IllegalArgumentException("No such topic found in the initial map: " + topic);
            }
            foundTopic.addRelatedCommand(commandString, command.help());
        });
    }

    public void addAvailabilityIndicator(CliAvailabilityIndicator availability, MethodTarget target) {
        Arrays.stream(availability.value()).forEach(command -> {
            availabilityIndicators.put(command, target);
        });
    }

    /**
     * get help string for a specific command, or a brief description of all the commands if buffer is
     * null or empty
     */
    public String getHelp(String buffer, int terminalWidth) {
        if (StringUtils.isBlank(buffer)) {
            return getHelp().toString(terminalWidth);
        }

        Method method = commands.get(buffer);
        if (method == null) {
            return "no help exists for this command.";
        }

        HelpBlock helpBlock = getHelp(method.getDeclaredAnnotation(CliCommand.class),
                method.getParameterAnnotations(), method.getParameterTypes());
        return helpBlock.toString(terminalWidth);
    }

    public String getHint(String buffer) {
        StringBuilder builder = new StringBuilder();
        // if no topic is provided, return a list of topics
        if (StringUtils.isBlank(buffer)) {
            builder.append(CliStrings.HINT__MSG__TOPICS_AVAILABLE).append(GfshParser.LINE_SEPARATOR)
                    .append(GfshParser.LINE_SEPARATOR);

            List<String> sortedTopics = new ArrayList<>(topics.keySet());
            Collections.sort(sortedTopics);
            sortedTopics.stream().forEachOrdered(topic -> builder.append(topic).append(GfshParser.LINE_SEPARATOR));
            return builder.toString();
        }

        Topic topic = topics.get(buffer);
        if (topic == null) {
            return CliStrings.format(CliStrings.HINT__MSG__UNKNOWN_TOPIC, buffer);
        }

        builder.append(topic.desc).append(GfshParser.LINE_SEPARATOR).append(GfshParser.LINE_SEPARATOR);
        Collections.sort(topic.relatedCommands);
        topic.relatedCommands.stream().forEachOrdered(command -> builder.append(command.command).append(": ")
                .append(command.desc).append(GfshParser.LINE_SEPARATOR));
        return builder.toString();
    }

    public Set<String> getTopicNames() {
        return topics.keySet();
    }

    boolean isAvailable(String command) {
        MethodTarget target = availabilityIndicators.get(command);
        if (target == null) {
            return true;
        }
        try {
            return (Boolean) target.getMethod().invoke(target.getTarget());
        } catch (Exception e) {
            return false;
        }
    }

    HelpBlock getHelp() {
        HelpBlock root = new HelpBlock();
        commands.keySet().stream().sorted().map(commands::get).forEach(method -> {
            root.addChild(getHelp(method.getDeclaredAnnotation(CliCommand.class), null, null));
        });
        return root;
    }

    /**
     * returns a short description and help string of the command if annotations is null or returns a
     * details description of the command with the syntax and parameter description
     */
    HelpBlock getHelp(CliCommand cliCommand, Annotation[][] annotations, Class<?>[] parameterTypes) {
        String commandName = cliCommand.value()[0];
        boolean isAvailable = isAvailable(commandName);

        if (annotations == null && parameterTypes == null) {
            String available = isAvailable ? AVAILABLE : NOT_AVAILABLE;
            HelpBlock help = new HelpBlock(commandName + " (" + available + ")");
            help.addChild(new HelpBlock(cliCommand.help()));
            return help;
        }

        HelpBlock root = new HelpBlock();
        // First we will have the block for NAME of the command
        HelpBlock name = new HelpBlock(NAME_NAME);
        name.addChild(new HelpBlock(commandName));
        root.addChild(name);

        // add the availability flag
        HelpBlock availability = new HelpBlock(IS_AVAILABLE_NAME);
        availability.addChild(new HelpBlock(isAvailable + ""));
        root.addChild(availability);

        // Now add synonyms if any
        String[] allNames = cliCommand.value();
        if (allNames.length > 1) {
            HelpBlock synonyms = new HelpBlock(SYNONYMS_NAME);
            for (int i = 1; i < allNames.length; i++) {
                synonyms.addChild(new HelpBlock(allNames[i]));
            }
            root.addChild(synonyms);
        }

        // Now comes the turn to display synopsis if any
        if (StringUtils.isNotBlank(cliCommand.help())) {
            HelpBlock synopsis = new HelpBlock(SYNOPSIS_NAME);
            synopsis.addChild(new HelpBlock(cliCommand.help()));
            root.addChild(synopsis);
        }

        // Now display the syntax for the command
        HelpBlock syntaxBlock = new HelpBlock(SYNTAX_NAME);
        String syntax = getSyntaxString(commandName, annotations, parameterTypes);
        syntaxBlock.addChild(new HelpBlock(syntax));
        root.addChild(syntaxBlock);

        // Detailed description of all the Options
        if (annotations.length > 0) {
            HelpBlock options = new HelpBlock(OPTIONS_NAME);
            for (int i = 0; i < annotations.length; i++) {
                CliOption cliOption = getAnnotation(annotations[i], CliOption.class);
                HelpBlock optionNode = getOptionDetail(cliOption);
                options.addChild(optionNode);
            }
            root.addChild(options);
        }
        return root;
    }

    HelpBlock getOptionDetail(CliOption cliOption) {
        HelpBlock optionNode = new HelpBlock(getPrimaryKey(cliOption));
        String help = cliOption.help();
        optionNode.addChild(new HelpBlock((StringUtils.isNotBlank(help) ? help : "")));
        if (getSynonyms(cliOption).size() > 0) {
            StringBuilder builder = new StringBuilder();
            for (String string : getSynonyms(cliOption)) {
                if (builder.length() > 0) {
                    builder.append(",");
                }
                builder.append(string);
            }
            optionNode.addChild(new HelpBlock(SYNONYMS_SUB_NAME + builder.toString()));
        }
        optionNode
                .addChild(new HelpBlock(REQUIRED_SUB_NAME + ((cliOption.mandatory()) ? TRUE_TOKEN : FALSE_TOKEN)));
        if (!isNullOrBlank(cliOption.specifiedDefaultValue())) {
            optionNode.addChild(new HelpBlock(SPECIFIEDDEFAULTVALUE_SUB_NAME + cliOption.specifiedDefaultValue()));
        }
        if (!isNullOrBlank(cliOption.unspecifiedDefaultValue())) {
            optionNode.addChild(
                    new HelpBlock(UNSPECIFIEDDEFAULTVALUE_VALUE_SUB_NAME + cliOption.unspecifiedDefaultValue()));
        }
        return optionNode;
    }

    private <T> T getAnnotation(Annotation[] annotations, Class<?> klass) {
        for (Annotation annotation : annotations) {
            if (klass.isAssignableFrom(annotation.getClass())) {
                return (T) annotation;
            }
        }
        return null;
    }

    String getSyntaxString(String commandName, Annotation[][] annotations, Class[] parameterTypes) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(commandName);
        for (int i = 0; i < annotations.length; i++) {
            CliOption cliOption = getAnnotation(annotations[i], CliOption.class);
            String optionString = getOptionString(cliOption, parameterTypes[i]);
            if (cliOption.mandatory()) {
                buffer.append(" ").append(optionString);
            } else {
                buffer.append(" [").append(optionString).append("]");
            }
        }
        return buffer.toString();
    }

    /**
     * this builds the following format of strings: key (as in sh and help) --key=value --key(=value)?
     * (if has specifiedDefaultValue) --key=value(,value)* (if the value is a list)
     *
     * @return option string
     */
    private static String getOptionString(CliOption cliOption, Class<?> optionType) {
        String key0 = cliOption.key()[0];
        if ("".equals(key0)) {
            return (cliOption.key()[1]);
        }

        StringBuffer buffer = new StringBuffer();
        buffer.append(GfshParser.LONG_OPTION_SPECIFIER).append(key0);

        boolean hasSpecifiedDefault = !isNullOrBlank(cliOption.specifiedDefaultValue());

        if (hasSpecifiedDefault) {
            buffer.append("(");
        }

        buffer.append(GfshParser.OPTION_VALUE_SPECIFIER).append(VALUE_FIELD);

        if (hasSpecifiedDefault) {
            buffer.append(")?");
        }

        if (isCollectionOrArrayType(optionType)) {
            buffer.append("(").append(",").append(VALUE_FIELD).append(")*");
        }

        return buffer.toString();
    }

    private static boolean isCollectionOrArrayType(Class<?> typeToCheck) {
        return typeToCheck != null && (typeToCheck.isArray() || Collection.class.isAssignableFrom(typeToCheck));
    }

    private static String getPrimaryKey(CliOption option) {
        String[] keys = option.key();
        if (keys.length == 0) {
            throw new RuntimeException("Invalid option keys");
        } else if ("".equals(keys[0])) {
            return keys[1];
        } else {
            return keys[0];
        }
    }

    private static List<String> getSynonyms(CliOption option) {
        List<String> synonyms = new ArrayList<>();
        String[] keys = option.key();
        if (keys.length < 2)
            return synonyms;
        // if the primary key is empty (like sh and help command), then there should be no synonyms.
        if ("".equals(keys[0]))
            return synonyms;

        for (int i = 1; i < keys.length; i++) {
            synonyms.add(keys[i]);
        }
        return synonyms;
    }

    private static boolean isNullOrBlank(String value) {
        return StringUtils.isBlank(value) || CliMetaData.ANNOTATION_NULL_VALUE.equals(value);
    }

    public Set<String> getCommands() {
        return commands.keySet();
    }
}