# HG changeset patch # User Andrew Azores # Date 1487089593 18000 # Node ID 767b2627c92d4ec8546e3061e64b3e2bf2963398 # Parent 6d713f2411063c446a81dd9d1791c4e58500da2d Implement cli/shell command-group support Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-November/021748.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-December/021824.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-January/021934.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-February/022139.html diff -r 6d713f241106 -r 767b2627c92d common/core/src/main/java/com/redhat/thermostat/common/utils/StringUtils.java --- a/common/core/src/main/java/com/redhat/thermostat/common/utils/StringUtils.java Tue Feb 14 09:07:27 2017 -0500 +++ b/common/core/src/main/java/com/redhat/thermostat/common/utils/StringUtils.java Tue Feb 14 11:26:33 2017 -0500 @@ -90,5 +90,23 @@ return in; } + /** + * Compare nullable Strings. + * The "null string" is considered to be "greater than", or "come after", any non-null String. + * For two non-null values, this method is equal to s1.compareTo(s2). + */ + public static int compare(String s1, String s2) { + if (s1 == null && s2 == null) { + return 0; + } + if (s1 == null) { + return 1; + } + if (s2 == null) { + return -1; + } + return s1.compareTo(s2); + } + } diff -r 6d713f241106 -r 767b2627c92d common/core/src/test/java/com/redhat/thermostat/common/utils/StringUtilsTest.java --- a/common/core/src/test/java/com/redhat/thermostat/common/utils/StringUtilsTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/common/core/src/test/java/com/redhat/thermostat/common/utils/StringUtilsTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -36,8 +36,10 @@ package com.redhat.thermostat.common.utils; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.Arrays; @@ -58,7 +60,7 @@ static List list(String... items) { if (items == null) { - return new ArrayList(); + return new ArrayList<>(); } else { return Arrays.asList(items); } @@ -77,4 +79,12 @@ assertFalse(escapedFunnyCharacters.contains("/")); } + @Test + public void testStringCompare() { + assertThat(StringUtils.compare("a", "b"), is("a".compareTo("b"))); + assertThat(StringUtils.compare("a", null), is(-1)); + assertThat(StringUtils.compare(null, "a"), is(1)); + assertThat(StringUtils.compare(null, null), is(0)); + } + } diff -r 6d713f241106 -r 767b2627c92d distribution/docs/thermostat-plugin.xsd --- a/distribution/docs/thermostat-plugin.xsd Tue Feb 14 09:07:27 2017 -0500 +++ b/distribution/docs/thermostat-plugin.xsd Tue Feb 14 11:26:33 2017 -0500 @@ -28,6 +28,54 @@ + + + + + + + + + + + + A simple one-word tag used to group related commands together, + such as "vm", "agent", "thread", or "numa". Useful for scenarios + where subcommands are not applicable. Commands within a group are + placed together in help output and receive a "see also" mention + in help output for other commands within their group. + + + + + + + + + + + + + + + + Metadata for the "command-group" elements which tag collections of related + commands. This metadata is used to provide stylized names and descriptions + as well as the order of appearance of command groups as displayed in 'help' + output. Metadata elements are expected to map 1:1 with command-group + elements and may be provided within the same thermostat-plugin.xml, or in + a separate thermostat-plugin.xml. + + + + + + + + + + + @@ -49,6 +97,7 @@ + @@ -93,6 +142,7 @@ + diff -r 6d713f241106 -r 767b2627c92d distribution/packaging/shared/bash-completion/thermostat-completion --- a/distribution/packaging/shared/bash-completion/thermostat-completion Tue Feb 14 09:07:27 2017 -0500 +++ b/distribution/packaging/shared/bash-completion/thermostat-completion Tue Feb 14 11:26:33 2017 -0500 @@ -11,7 +11,7 @@ # Thermostat Options # All valid commands and options are prefixed with a single space - opts="$( "${thermostat_install_dir}/bin/thermostat" "${thermostat_logging_opts}" help | grep '^ ' | cut -d " " -f 2 | tr '\n' ' ')" + opts="$( "${thermostat_install_dir}/bin/thermostat" "${thermostat_logging_opts}" help | grep '^ ' | cut -d " " -f 2 | sort | uniq | tr '\n' ' ')" COMPREPLY=($(compgen -W "${opts}" -- ${cur})) return 0 diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java Tue Feb 14 11:26:33 2017 -0500 @@ -79,6 +79,7 @@ private ServiceRegistration cmdInfoReg; private ServiceRegistration exitStatusReg; private ServiceRegistration pluginConfReg; + private ServiceRegistration commandGroupMetaReg; private BundleContext context; private CurrentEnvironment env; @@ -107,6 +108,8 @@ ConfigurationInfoSource configurations = pluginSource; pluginConfReg = context.registerService(ConfigurationInfoSource.class, configurations, null); + commandGroupMetaReg = context.registerService(CommandGroupMetadataSource.class, pluginSource, null); + CommandInfoSource commands = new CompoundCommandInfoSource(builtInCommandSource, pluginSource); cmdInfoReg = context.registerService(CommandInfoSource.class, commands, null); @@ -134,6 +137,7 @@ cmdInfoReg.unregister(); exitStatusReg.unregister(); pluginConfReg.unregister(); + commandGroupMetaReg.unregister(); } } @@ -146,7 +150,7 @@ private ShellCommand shellCommand; private TabCompletion tabCompletion; @SuppressWarnings("rawtypes") - private ServiceTracker commandInfoSourceTracker; + private MultipleServiceTracker commandInfoSourceTracker; private ServiceTracker dbServiceTracker; @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -217,28 +221,34 @@ shellTracker.open(); final HelpCommandCompleterService helpCommandCompleterService = new HelpCommandCompleterService(); - commandInfoSourceTracker = new ServiceTracker(context, CommandInfoSource.class, null) { + final Class[] helpCommandClasses = new Class[] { + CommandInfoSource.class, + CommandGroupMetadataSource.class + }; + commandInfoSourceTracker = new MultipleServiceTracker(context, helpCommandClasses, new Action() { @Override - public Object addingService(ServiceReference reference) { - CommandInfoSource infoSource = (CommandInfoSource) super.addingService(reference); + public void dependenciesAvailable(DependencyProvider services) { + CommandInfoSource infoSource = services.get(CommandInfoSource.class); helpCommand.setCommandInfoSource(infoSource); helpCommandCompleterService.bindCommandInfoSource(infoSource); if (shellCommand != null) { shellCommand.setCommandInfoSource(infoSource); } - return infoSource; + + CommandGroupMetadataSource commandGroupMetadataSource = services.get(CommandGroupMetadataSource.class); + helpCommand.setCommandGroupMetadataSource(commandGroupMetadataSource); } @Override - public void removedService(ServiceReference reference, Object service) { + public void dependenciesUnavailable() { helpCommand.setCommandInfoSource(null); + helpCommand.setCommandGroupMetadataSource(null); helpCommandCompleterService.unbindCommandInfoSource(); if (shellCommand != null) { shellCommand.setCommandInfoSource(null); } - super.removedService(reference, service); } - }; + }); commandInfoSourceTracker.open(); dbServiceTracker = new ServiceTracker(context, DbService.class.getName(), new ServiceTrackerCustomizer() { diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java Tue Feb 14 11:26:33 2017 -0500 @@ -39,6 +39,7 @@ import java.util.List; import java.util.Set; +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; import org.apache.commons.cli.Options; import com.redhat.thermostat.launcher.BundleInformation; @@ -48,17 +49,19 @@ private final String name; private final String summary; private final String description; + private final List commandGroups; private final String usage; private final List subcommands; private final Options options; private final Set environments; private final List bundles; - public BasicCommandInfo(String name, String summary, String description, String usage, Options options, List subcommands, - Set environments, List bundles) { + public BasicCommandInfo(String name, String summary, String description, List commandGroups, String usage, + Options options, List subcommands, Set environments, List bundles) { this.name = name; this.summary = summary; this.description = description; + this.commandGroups = commandGroups; this.usage = usage; this.options = options; this.subcommands = subcommands; @@ -82,6 +85,11 @@ } @Override + public List getCommandGroups() { + return commandGroups; + } + + @Override public String getUsage() { return usage; } diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/BuiltInCommandInfo.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BuiltInCommandInfo.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BuiltInCommandInfo.java Tue Feb 14 11:26:33 2017 -0500 @@ -61,6 +61,7 @@ private static final String PROPERTY_BUNDLES = "bundles"; private static final String PROPERTY_SUMMARY = "summary"; private static final String PROPERTY_DESC = "description"; + private static final String COMMAND_GROUP_DESC = "command-groups"; private static final String PROPERTY_USAGE = "usage"; private static final String PROPERTY_OPTIONS = "options"; private static final String PROPERTY_ENVIRONMENTS = "environments"; @@ -72,6 +73,7 @@ private static final String PROP_OPTDESC = ".description"; private String name, summary, description, usage; + private List commandGroups = new ArrayList<>(); private Options options; private EnumSet environment; private List dependencies; @@ -87,6 +89,15 @@ summary = properties.getProperty(key); } else if (key.equals(PROPERTY_DESC)) { description = properties.getProperty(key); + } else if (key.equals(COMMAND_GROUP_DESC)) { + String raw = properties.getProperty(key); + if (raw == null || raw.isEmpty()) { + continue; + } + String[] parts = raw.split(","); + for (String part : parts) { + commandGroups.add(part.trim()); + } } else if (key.equals(PROPERTY_USAGE)) { usage = properties.getProperty(key); } else if (key.equals(PROPERTY_OPTIONS)) { @@ -378,6 +389,11 @@ } @Override + public List getCommandGroups() { + return commandGroups; + } + + @Override public String getUsage() { return usage; } diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandGroupMetadataSource.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandGroupMetadataSource.java Tue Feb 14 11:26:33 2017 -0500 @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.launcher.internal; + +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; + +import java.util.Map; + +interface CommandGroupMetadataSource { + Map getCommandGroupMetadata(); +} diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandInfo.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandInfo.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CommandInfo.java Tue Feb 14 11:26:33 2017 -0500 @@ -39,6 +39,7 @@ import java.util.List; import java.util.Set; +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; import org.apache.commons.cli.Options; import com.redhat.thermostat.launcher.BundleInformation; @@ -64,6 +65,15 @@ public String getDescription(); /** + * A simple one-word tag used to group related commands together, + * such as "vm", "agent", "thread", or "numa". Useful for scenarios + * where subcommands are not applicable. Commands within a group are + * placed together in help output and receive a "see also" mention + * in help output for other commands within their group. + */ + public List getCommandGroups(); + + /** * How the user should invoke this command */ public String getUsage(); diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java Tue Feb 14 11:26:33 2017 -0500 @@ -132,6 +132,9 @@ String summary = selectBest(info1.getSummary(), info2.getSummary()); String description = selectBest(info1.getDescription(), info2.getDescription()); + List commandGroups = new ArrayList<>(); + commandGroups.addAll(info1.getCommandGroups()); + commandGroups.addAll(info2.getCommandGroups()); String usage = selectBest(info1.getUsage(), info2.getUsage()); List subcommands = selectBest(info1.getSubcommands(), info2.getSubcommands()); Options options = selectBest(info1.getOptions(), info2.getOptions()); @@ -140,7 +143,7 @@ bundles.addAll(info1.getBundles()); bundles.addAll(info2.getBundles()); - return new BasicCommandInfo(name, summary, description, usage, options, subcommands, environment, bundles); + return new BasicCommandInfo(name, summary, description, commandGroups, usage, options, subcommands, environment, bundles); } private T selectBest(T first, T second) { diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/HelpCommand.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/HelpCommand.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/HelpCommand.java Tue Feb 14 11:26:33 2017 -0500 @@ -37,12 +37,21 @@ package com.redhat.thermostat.launcher.internal; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +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 com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.common.utils.StringUtils; +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; @@ -58,21 +67,33 @@ public class HelpCommand extends AbstractCommand { private static final Translate translator = LocaleResources.createLocalizer(); + private static final Logger logger = LoggingUtils.getLogger(HelpCommand.class); static final String COMMAND_NAME = "help"; private static final int COMMANDS_COLUMNS_WIDTH = 14; + public static final int MAX_COLUMN_WIDTH = 80; private static final String APP_NAME = "thermostat"; private static final CommandInfoComparator comparator = new CommandInfoComparator(); + private static final CommandGroupMetadata UNGROUPED_COMMANDS_METADATA = new CommandGroupMetadata(null, null, Integer.MAX_VALUE); private CommandInfoSource commandInfoSource; + private CommandGroupMetadataSource commandGroupMetadataSource; private Environment currentEnvironment; + private SortedMap> commandGroupMap; + private Map commandGroupMetadataMap; + private Set contextualCommands = new HashSet<>(); + public void setCommandInfoSource(CommandInfoSource source) { this.commandInfoSource = source; } + public void setCommandGroupMetadataSource(CommandGroupMetadataSource commandGroupMetadataSource) { + this.commandGroupMetadataSource = commandGroupMetadataSource; + } + public void setEnvironment(Environment env) { currentEnvironment = env; } @@ -87,6 +108,20 @@ return; } + if (commandGroupMetadataSource == null) { + ctx.getConsole().getError().print(translator.localize(LocaleResources.CANNOT_GET_COMMAND_GROUP_METADATA).getContents()); + return; + } + + for (CommandInfo info: commandInfoSource.getCommandInfos()) { + if (info.getEnvironments().contains(currentEnvironment)) { + contextualCommands.add(info); + } + } + + commandGroupMetadataMap = new HashMap<>(commandGroupMetadataSource.getCommandGroupMetadata()); + commandGroupMap = createCommandGroupMap(); + if (nonParsed.isEmpty()) { if (currentEnvironment == Environment.CLI) { //CLI only since the framework will already be @@ -100,24 +135,60 @@ } } + private SortedMap> createCommandGroupMap() { + Set seen = new HashSet<>(); + Map> groupNameMap = new HashMap<>(); + for (CommandInfo commandInfo : contextualCommands) { + for (String commandGroup : commandInfo.getCommandGroups()) { + seen.add(commandInfo); + if (!groupNameMap.containsKey(commandGroup)) { + groupNameMap.put(commandGroup, new TreeSet<>(comparator)); + } + groupNameMap.get(commandGroup).add(commandInfo); + } + } + + SortedMap> result = new TreeMap<>(new CommandGroupMetadataComparator()); + for (Map.Entry> entry : groupNameMap.entrySet()) { + String groupName = entry.getKey(); + CommandGroupMetadata metadata = commandGroupMetadataMap.get(groupName); + if (metadata == null) { + logger.warning("No metadata provided for command group \"" + groupName + "\""); + metadata = new CommandGroupMetadata(groupName, groupName, Integer.MAX_VALUE); + commandGroupMetadataMap.put(groupName, metadata); + } + result.put(metadata, entry.getValue()); + } + + SortedSet ungrouped = new TreeSet<>(comparator); + ungrouped.addAll(contextualCommands); + ungrouped.removeAll(seen); + result.put(UNGROUPED_COMMANDS_METADATA, ungrouped); + + return result; + } + private void printCommandSummaries(CommandContext ctx) { ctx.getConsole().getOutput().print(translator.localize(LocaleResources.COMMAND_HELP_COMMAND_LIST_HEADER).getContents()); TableRenderer renderer = new TableRenderer(2, COMMANDS_COLUMNS_WIDTH); - Collection commandInfos = new ArrayList<>(); - for (CommandInfo info: commandInfoSource.getCommandInfos()) { - if (info.getEnvironments().contains(currentEnvironment)) { - commandInfos.add(info); + for (Map.Entry> group : commandGroupMap.entrySet()) { + CommandGroupMetadata commandGroupMetadata = group.getKey(); + if (commandGroupMetadata.equals(UNGROUPED_COMMANDS_METADATA) || group.getValue().isEmpty()) { + continue; } + renderer.printLine(translator.localize(LocaleResources.COMMAND_GROUP_HEADER, + commandGroupMetadata.getDescription()).getContents(), ""); + for (CommandInfo info : group.getValue()) { + printCommandSummary(renderer, info); + } + renderer.printLine("", ""); + } + for (CommandInfo ungroupedCommand : commandGroupMap.get(UNGROUPED_COMMANDS_METADATA)) { + printCommandSummary(renderer, ungroupedCommand); } - List sortedCommandInfos = new ArrayList<>(commandInfos); - - Collections.sort(sortedCommandInfos, comparator); - for (CommandInfo info : sortedCommandInfos) { - printCommandSummary(renderer, info); - } renderer.render(ctx.getConsole().getOutput()); } @@ -160,28 +231,41 @@ String usage = APP_NAME + " " + info.getUsage() + "\n" + info.getDescription(); String header = ""; if (isAvailabilityNoteNeeded(info)) { - header = header + getAvailabilityNote(info); + header = getAvailabilityNote(info); } header = header + "\n" + APP_NAME + " " + info.getName(); Option help = CommonOptions.getHelpOption(); options.addOption(help); - helpFormatter.printHelp(pw, 80, usage, header, options, 2, 4, null); + helpFormatter.printHelp(pw, MAX_COLUMN_WIDTH, usage, header, options, 2, 4, null); if (!info.getSubcommands().isEmpty()) { pw.println(); - helpFormatter.printWrapped(pw, 80, translator.localize(LocaleResources.SUBCOMMANDS_SECTION_HEADER).getContents()); + helpFormatter.printWrapped(pw, MAX_COLUMN_WIDTH, translator.localize(LocaleResources.SUBCOMMANDS_SECTION_HEADER).getContents()); pw.println(); for (PluginConfiguration.Subcommand subcommand : info.getSubcommands()) { pw.println(translator.localize(LocaleResources.SUBCOMMAND_ENTRY_HEADER, subcommand.getName()).getContents()); pw.println(subcommand.getDescription()); Options o = subcommand.getOptions(); - helpFormatter.printOptions(pw, 80, o, 2, 4); + helpFormatter.printOptions(pw, MAX_COLUMN_WIDTH, o, 2, 4); if (!o.getOptions().isEmpty()) { pw.println(); } } } + SortedSet relatedCommands = new TreeSet<>(comparator); + for (String commandGroup : info.getCommandGroups()) { + relatedCommands.addAll(commandGroupMap.get(commandGroupMetadataMap.get(commandGroup))); + } + relatedCommands.remove(info); + if (!relatedCommands.isEmpty()) { + pw.println(); + pw.println(translator.localize(LocaleResources.SEE_ALSO_HEADER).getContents()); + pw.print(' '); + RelatedCommandsFormatter relatedCommandsFormatter = new RelatedCommandsFormatter(relatedCommands); + pw.println(relatedCommandsFormatter.format()); + } + pw.flush(); } @@ -191,20 +275,15 @@ /** Describe where command is available */ private String getAvailabilityNote(CommandInfo info) { - - String availabilityNote = ""; - // there are two mutually exclusive environments: if an availability // note is needed, it will just be about one if (info.getEnvironments().contains(Environment.SHELL)) { - availabilityNote = translator.localize(LocaleResources.COMMAND_AVAILABLE_INSIDE_SHELL).getContents(); + return translator.localize(LocaleResources.COMMAND_AVAILABLE_INSIDE_SHELL).getContents(); } else if (info.getEnvironments().contains(Environment.CLI)) { - availabilityNote = translator.localize(LocaleResources.COMMAND_AVAILABLE_OUTSIDE_SHELL).getContents(); + return translator.localize(LocaleResources.COMMAND_AVAILABLE_OUTSIDE_SHELL).getContents(); } else { throw new AssertionError("Need to handle a third environment"); } - - return availabilityNote; } @Override @@ -226,11 +305,46 @@ if (o2.getName().equals("help")) { return 1; } - return o1.getName().compareTo(o2.getName()); } } + private static class CommandGroupMetadataComparator implements Comparator { + @Override + public int compare(CommandGroupMetadata cgm1, CommandGroupMetadata cgm2) { + int sortOrderComparison = Integer.compare(cgm1.getSortOrder(), cgm2.getSortOrder()); + if (sortOrderComparison != 0) { + return sortOrderComparison; + } + return StringUtils.compare(cgm1.getName(), cgm2.getName()); + } + } + + private static class RelatedCommandsFormatter { + + private final Set commandInfos; + + public RelatedCommandsFormatter(Set commandInfos) { + this.commandInfos = commandInfos; + } + + public String format() { + StringBuilder sb = new StringBuilder(); + for (CommandInfo info : commandInfos) { + String next = info.getName(); + if (sb.length() + (" " + next + ",").length() > MAX_COLUMN_WIDTH) { + sb.append("\n "); + } + sb.append(' ').append(next).append(","); + } + String result = sb.toString(); + if (result.endsWith(",")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + } + } diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/LocaleResources.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LocaleResources.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LocaleResources.java Tue Feb 14 11:26:33 2017 -0500 @@ -43,6 +43,7 @@ MISSING_LAUNCHER, CANNOT_GET_COMMAND_INFO, + CANNOT_GET_COMMAND_GROUP_METADATA, UNKNOWN_COMMAND, COMMAND_COULD_NOT_LOAD_BUNDLES, COMMAND_DESCRIBED_BUT_NOT_AVAILALBE, @@ -54,12 +55,16 @@ COMMAND_HELP_COMMAND_LIST_HEADER, COMMAND_HELP_COMMAND_OPTION_HEADER, + COMMAND_GROUP_HEADER, + COMMAND_SHELL_USER_GUIDE, COMMAND_SHELL_IO_EXCEPTION, SUBCOMMANDS_SECTION_HEADER, SUBCOMMAND_ENTRY_HEADER, + SEE_ALSO_HEADER, + OPTION_DB_URL_DESC, OPTION_LOG_LEVEL_DESC, OPTION_HELP_DESC, diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java Tue Feb 14 11:26:33 2017 -0500 @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.apache.commons.cli.Options; @@ -49,14 +50,17 @@ public class PluginConfiguration { + private final List newCommands; + private final List commandGroupMetadataList; private final List extensions; - private final List newCommands; private final PluginID pluginID; private final Configurations configurations; - public PluginConfiguration(List newCommands, List extensions, PluginID pluginID, Configurations config) { + public PluginConfiguration(List newCommands, List commandGroupMetadataList, + List extensions, PluginID pluginID, Configurations config) { this.newCommands = newCommands; + this.commandGroupMetadataList = commandGroupMetadataList; this.extensions = extensions; this.pluginID = pluginID; this.configurations = config; @@ -70,6 +74,10 @@ return newCommands; } + public List getCommandGroupMetadata() { + return commandGroupMetadataList; + } + public PluginID getPluginID() { return this.pluginID; } @@ -115,6 +123,7 @@ private final String commandName; private final String summary; private final String description; + private final List commandGroups; private final String usage; private final List positionalArguments; private final Options options; @@ -122,12 +131,13 @@ private final Set environment; private final List bundles; - public NewCommand(String name, String summary, String description, String usage, + public NewCommand(String name, String summary, String description, List commandGroups, String usage, List positionalArguments, Options options, List subcommands, Set environment, List bundles) { this.commandName = name; this.summary = summary; this.description = description; + this.commandGroups = commandGroups; this.usage = usage; this.positionalArguments = positionalArguments; this.options = options; @@ -148,6 +158,10 @@ return description; } + public List getCommandGroups() { + return commandGroups; + } + /** * The usage string may be null if no usage string was explicitly * provided. In that case, usage should be "computed" using options and @@ -182,6 +196,47 @@ } + public static class CommandGroupMetadata { + private final String name; + private final String description; + private final int sortOrder; + + public CommandGroupMetadata(String name, String description, int sortOrder) { + this.name = name; + this.description = description; + this.sortOrder = sortOrder; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public int getSortOrder() { + return sortOrder; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CommandGroupMetadata)) { + return false; + } + if (this == o) { + return true; + } + CommandGroupMetadata other = (CommandGroupMetadata) o; + return Objects.equals(this.getName(), other.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + public static class PluginID { private final String pluginID; diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java Tue Feb 14 11:26:33 2017 -0500 @@ -55,6 +55,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; @@ -239,6 +240,7 @@ private PluginConfiguration parseRootElement(String pluginName, Node root) { List commands = Collections.emptyList(); + List commandGroupMetadataList = Collections.emptyList(); List extensions = Collections.emptyList(); Configurations configurations = null; String pluginID = null; @@ -249,6 +251,8 @@ Node node = nodes.item(i); if (node.getNodeName().equals("commands")) { commands = parseCommands(pluginName, node); + } else if (node.getNodeName().equals("command-group-metadatas")) { + commandGroupMetadataList = parseCommandGroupMetadatas(node); } else if (node.getNodeName().equals("extensions")) { extensions = parseExtensions(pluginName, node); } else if (node.getNodeName().equals("id")) { @@ -270,7 +274,7 @@ pluginID = ""; } - return new PluginConfiguration(commands, extensions, new PluginID(pluginID), configurations); + return new PluginConfiguration(commands, commandGroupMetadataList, extensions, new PluginID(pluginID), configurations); } private List parseCommands(String pluginName, Node commandsNode) { @@ -288,6 +292,36 @@ return newCommands; } + private List parseCommandGroupMetadatas(Node commandGroupMetadatasNode) { + NodeList childNodes = commandGroupMetadatasNode.getChildNodes(); + List commandGroupMetadatas = new ArrayList<>(childNodes.getLength()); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (node.getNodeName().equals("command-group-metadata")) { + commandGroupMetadatas.add(parseCommandGroupMetadata(node)); + } + } + return commandGroupMetadatas; + } + + private CommandGroupMetadata parseCommandGroupMetadata(Node commandGroupMetadataNode) { + String name = null; + String description = null; + int sortOrder = -1; + NodeList childNodes = commandGroupMetadataNode.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (node.getNodeName().equals("name")) { + name = node.getTextContent().trim(); + } else if (node.getNodeName().equals("description")) { + description = node.getTextContent().trim(); + } else if (node.getNodeName().equals("sort-order")) { + sortOrder = Integer.parseInt(node.getTextContent().trim()); + } + } + return new CommandGroupMetadata(name, description, sortOrder); + } + private List parseExtensions(String pluginName, Node extensionsNode) { List commandExtensions = new ArrayList(); NodeList childNodes = extensionsNode.getChildNodes(); @@ -354,6 +388,7 @@ String usage = null; String summary = null; String description = null; + List commandGroups = new ArrayList<>(); List subcommands = new ArrayList<>(); List arguments = new ArrayList<>(); Options options = new Options(); @@ -371,6 +406,8 @@ summary = node.getTextContent().trim(); } else if (node.getNodeName().equals("description")) { description = parseDescription(node); + } else if (node.getNodeName().equals("command-groups")) { + commandGroups = parseCommandGroups(node); } else if (node.getNodeName().equals("subcommands")) { subcommands = parseSubcommands(node); } else if (node.getNodeName().equals("arguments")) { @@ -393,7 +430,8 @@ "name='" + name + "', summary='" + summary + ", description='" + description + "', options='" + options + "'"); return null; } else { - return new NewCommand(name, summary, description, usage, arguments, options, subcommands, availableInEnvironments, bundles); + return new NewCommand(name, summary, description, commandGroups, usage, arguments, options, subcommands, + availableInEnvironments, bundles); } } @@ -449,6 +487,23 @@ return result.toString().trim(); } + private List parseCommandGroups(Node commandGroupsNode) { + NodeList nodes = commandGroupsNode.getChildNodes(); + List groups = new ArrayList<>(nodes.getLength()); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + if (node.getNodeName().equals("command-group")) { + String group = node.getTextContent().trim(); + groups.add(group); + } + } + return groups; + } + + private String parseCommandGroup(Node commandGroupNode) { + return commandGroupNode.getTextContent().trim().toLowerCase(); + } + private List parseArguments(String pluginName, String commandName, Node argumentsNode) { return parseNodeAsList(pluginName, commandName, argumentsNode, "argument"); } diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginInfoSource.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginInfoSource.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginInfoSource.java Tue Feb 14 11:26:33 2017 -0500 @@ -45,12 +45,14 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Map.Entry; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -58,6 +60,7 @@ import com.redhat.thermostat.common.utils.LoggingUtils; import com.redhat.thermostat.launcher.BundleInformation; import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandExtensions; +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; import com.redhat.thermostat.launcher.internal.PluginConfiguration.Configurations; import com.redhat.thermostat.launcher.internal.PluginConfiguration.NewCommand; import com.redhat.thermostat.launcher.internal.PluginConfiguration.PluginID; @@ -74,7 +77,7 @@ * * @see PluginConfigurationParser how the thermostat-plugin.xml file is parsed */ -public class PluginInfoSource implements CommandInfoSource, ConfigurationInfoSource { +public class PluginInfoSource implements CommandInfoSource, ConfigurationInfoSource, CommandGroupMetadataSource { private static final String PLUGIN_CONFIG_FILE = "thermostat-plugin.xml"; @@ -83,6 +86,8 @@ private final UsageStringBuilder usageBuilder; private Map allNewCommands = new HashMap<>(); + private Map> pluginMetadataMap = new HashMap<>(); + private Map commandGroupMetadataMap = new HashMap<>(); private Map> additionalBundlesForExistingCommands = new HashMap<>(); private Map allConfigs = new HashMap<>(); @@ -112,6 +117,7 @@ File configurationFile = new File(pluginDir, PLUGIN_CONFIG_FILE); PluginConfiguration pluginConfig = parser.parse(configurationFile); loadNewAndExtendedCommands(internalJarRoot, pluginDir, pluginConfig); + processCommandGroupMetadata(pluginConfig, pluginDir); if (allConfigs.containsKey(pluginConfig.getPluginID())) { logger.log(Level.WARNING, "Plugin with ID: " + pluginConfig.getPluginID() + " conflicts with a previous plugin's ID and the config file will not be overwritten."); } else if (pluginConfig.hasValidID() && pluginConfig.hasConfigurations()) { @@ -131,6 +137,7 @@ } } combineCommands(); + validateCommandGroupMetadataSources(); } private void addPluginDirectory(List allPluginDirectories, File aPluginRoot) { @@ -176,6 +183,7 @@ BasicCommandInfo info = new BasicCommandInfo(commandName, command.getSummary(), command.getDescription(), + command.getCommandGroups(), usage, command.getOptions(), command.getSubcommands(), command.getEnvironments(), @@ -187,6 +195,33 @@ } + private void processCommandGroupMetadata(PluginConfiguration pluginConfiguration, File pluginDir) { + List commandGroupMetadata = pluginConfiguration.getCommandGroupMetadata(); + for (CommandGroupMetadata metadata : commandGroupMetadata) { + if (!pluginMetadataMap.containsKey(metadata)) { + pluginMetadataMap.put(metadata, new HashSet()); + } + pluginMetadataMap.get(metadata).add(pluginDir.getName()); + String name = metadata.getName(); + if (commandGroupMetadataMap.containsKey(name)) { + logger.warning("Already found metadata for command group \"" + name + "\""); + continue; + } + commandGroupMetadataMap.put(name, metadata); + } + } + + private void validateCommandGroupMetadataSources() { + for (Map.Entry> entry : pluginMetadataMap.entrySet()) { + CommandGroupMetadata metadata = entry.getKey(); + Set providingPlugins = entry.getValue(); + if (providingPlugins.size() > 1) { + logger.warning(String.format("Metadata for command group \"%s\" is provided by multiple plugins: %s", + metadata.getName(), providingPlugins)); + } + } + } + private void combineCommands() { Iterator>> iter = additionalBundlesForExistingCommands.entrySet().iterator(); while (iter.hasNext()) { @@ -199,6 +234,7 @@ BasicCommandInfo updated = new BasicCommandInfo(old.getName(), old.getSummary(), old.getDescription(), + old.getCommandGroups(), old.getUsage(), old.getOptions(), old.getSubcommands(), old.getEnvironments(), @@ -232,8 +268,13 @@ return result; } + @Override + public Map getCommandGroupMetadata() { + return new HashMap<>(commandGroupMetadataMap); + } + private BasicCommandInfo createCommandInfo(String name, List bundles) { - return new BasicCommandInfo(name, null, null, null, null, null, null, bundles); + return new BasicCommandInfo(name, null, null, Collections.emptyList(), null, null, null, null, bundles); } public Map getConfiguration(String pluginID, String fileName) throws IOException { diff -r 6d713f241106 -r 767b2627c92d launcher/src/main/resources/com/redhat/thermostat/launcher/internal/strings.properties --- a/launcher/src/main/resources/com/redhat/thermostat/launcher/internal/strings.properties Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/main/resources/com/redhat/thermostat/launcher/internal/strings.properties Tue Feb 14 11:26:33 2017 -0500 @@ -1,6 +1,7 @@ MISSING_LAUNCHER = Fatal Error: Could not locate launcher CANNOT_GET_COMMAND_INFO = no information about commands +CANNOT_GET_COMMAND_GROUP_METADATA = command group metadata source is missing UNKNOWN_COMMAND = unknown command ''{0}''\n COMMAND_COULD_NOT_LOAD_BUNDLES = Could not load necessary bundles for {0} COMMAND_DESCRIBED_BUT_NOT_AVAILALBE = ERROR: Information about the command {0} is provided, but the command itself is not available. Was the Command object registered as an OSGi service? Was the bundle providing the command activated? @@ -16,9 +17,13 @@ COMMAND_HELP_COMMAND_LIST_HEADER = list of commands:\n\n COMMAND_HELP_COMMAND_OPTION_HEADER = list of global options:\n\n +COMMAND_GROUP_HEADER = {0}: + SUBCOMMANDS_SECTION_HEADER=Subcommands: SUBCOMMAND_ENTRY_HEADER={0}: +SEE_ALSO_HEADER=See also: + COMMAND_SHELL_USER_GUIDE = Please see the User Guide at {0} COMMAND_SHELL_IO_EXCEPTION = IOException caught during Thermostat shell session. diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -155,12 +155,12 @@ assertCommandIsRegistered(context, "help", HelpCommand.class); - verify(mockTracker, times(2)).open(); + verify(mockTracker, times(3)).open(); Action action = actionCaptor.getValue(); assertNotNull(action); activator.stop(context); - verify(mockTracker, times(2)).close(); + verify(mockTracker, times(3)).close(); } @Test @@ -194,6 +194,12 @@ }; whenNew(MultipleServiceTracker.class).withParameterTypes(BundleContext.class, Class[].class, Action.class).withArguments(eq(context), eq(agentIdCompleterDeps), actionCaptor.capture()).thenReturn(unusedTracker); + Class[] helpCommandDeps = new Class[] { + CommandInfoSource.class, + CommandGroupMetadataSource.class + }; + whenNew(MultipleServiceTracker.class).withParameterTypes(BundleContext.class, Class[].class, Action.class).withArguments(eq(context), + eq(helpCommandDeps), actionCaptor.capture()).thenReturn(unusedTracker); Activator activator = new Activator(); context.registerService(Keyring.class, mock(Keyring.class), null); @@ -276,6 +282,12 @@ }; whenNew(MultipleServiceTracker.class).withParameterTypes(BundleContext.class, Class[].class, Action.class).withArguments(eq(context), eq(agentIdCompleterDeps), actionCaptor.capture()).thenReturn(unusedTracker); + Class[] helpCommandDeps = new Class[] { + CommandInfoSource.class, + CommandGroupMetadataSource.class + }; + whenNew(MultipleServiceTracker.class).withParameterTypes(BundleContext.class, Class[].class, Action.class).withArguments(eq(context), + eq(helpCommandDeps), actionCaptor.capture()).thenReturn(unusedTracker); Activator activator = new Activator(); ConfigurationInfoSource configurationInfoSource = mock(ConfigurationInfoSource.class); diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -55,17 +55,20 @@ final String NAME = "the_name"; final String SUMMARY = "some-summary"; final String DESCRIPTION = "some-description"; + final List COMMAND_GROUPS = Collections.singletonList("some-command-group"); + final List COMMAND_GROUP_METADATAS = Collections.emptyList(); final String USAGE = "some-usage"; final Options OPTIONS = new Options(); final List SUBCOMMANDS = Collections.emptyList(); final Set ENVIRONMENT = EnumSet.noneOf(Environment.class); final List BUNDLES = Collections.emptyList(); - BasicCommandInfo info = new BasicCommandInfo(NAME, SUMMARY, DESCRIPTION, USAGE, OPTIONS, SUBCOMMANDS, ENVIRONMENT, BUNDLES); + BasicCommandInfo info = new BasicCommandInfo(NAME, SUMMARY, DESCRIPTION, COMMAND_GROUPS, USAGE, OPTIONS, SUBCOMMANDS, ENVIRONMENT, BUNDLES); assertEquals(NAME, info.getName()); assertEquals(SUMMARY, info.getSummary()); assertEquals(DESCRIPTION, info.getDescription()); + assertEquals(COMMAND_GROUPS, info.getCommandGroups()); assertEquals(USAGE, info.getUsage()); assertEquals(SUBCOMMANDS, info.getSubcommands()); assertEquals(OPTIONS, info.getOptions()); diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/BuiltInCommandInfoTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BuiltInCommandInfoTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BuiltInCommandInfoTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -42,6 +42,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Properties; @@ -105,6 +106,18 @@ } @Test + public void verifyGetCommandGroup() { + Properties props = new Properties(); + String name = "command-groups"; + String groups = "a,b"; + props.put(name, groups); + BuiltInCommandInfo info = new BuiltInCommandInfo(name, props); + + List commandGroups = info.getCommandGroups(); + assertEquals(Arrays.asList("a", "b"), commandGroups); + } + + @Test public void verifyGetUsage() { Properties props = new Properties(); String name = "name"; diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSourceTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSourceTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSourceTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -102,6 +102,7 @@ public void verifyGetCommandInfoMergesResultFromBothSources() { String NAME = "test-command-please-ignore"; String DESCRIPTION = "test-description"; + List COMMAND_GROUPS = Collections.singletonList("test-command-group"); String USAGE = "test-usage"; List SUBCOMMANDS = Collections.emptyList(); Options OPTIONS = new Options(); @@ -111,6 +112,7 @@ CommandInfo cmdInfo1 = mock(CommandInfo.class); when(cmdInfo1.getName()).thenReturn(NAME); when(cmdInfo1.getDescription()).thenReturn(DESCRIPTION); + when(cmdInfo1.getCommandGroups()).thenReturn(COMMAND_GROUPS); when(cmdInfo1.getUsage()).thenReturn(USAGE); when(cmdInfo1.getOptions()).thenReturn(OPTIONS); when(cmdInfo1.getSubcommands()).thenReturn(SUBCOMMANDS); @@ -119,6 +121,7 @@ CommandInfo cmdInfo2 = mock(CommandInfo.class); when(cmdInfo2.getName()).thenReturn(NAME); when(cmdInfo2.getBundles()).thenReturn(DEPS2); + when(cmdInfo2.getCommandGroups()).thenReturn(Collections.emptyList()); when(source1.getCommandInfo(NAME)).thenReturn(cmdInfo1); when(source2.getCommandInfo(NAME)).thenReturn(cmdInfo2); @@ -126,6 +129,7 @@ CommandInfo result = compoundSource.getCommandInfo(NAME); assertEquals(NAME, result.getName()); assertEquals(DESCRIPTION, result.getDescription()); + assertEquals(COMMAND_GROUPS, result.getCommandGroups()); assertEquals(USAGE, result.getUsage()); assertEquals(OPTIONS, result.getOptions()); assertEquals(SUBCOMMANDS, result.getSubcommands()); diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/HelpCommandTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/HelpCommandTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/HelpCommandTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -65,13 +65,16 @@ + " --boot-delegation boot delegation string passed on to the OSGi framework\n"; private TestCommandContextFactory ctxFactory; - private CommandInfoSource infos; + private CommandInfoSource commandInfoSource; + private CommandGroupMetadataSource commandGroupMetadataSource; @Before public void setUp() { ctxFactory = new TestCommandContextFactory(); - infos = mock(CommandInfoSource.class); + commandInfoSource = mock(CommandInfoSource.class); + commandGroupMetadataSource = mock(CommandGroupMetadataSource.class); + when(commandGroupMetadataSource.getCommandGroupMetadata()).thenReturn(Collections.emptyMap()); } @Test @@ -86,6 +89,7 @@ Arguments args = mock(Arguments.class); when(args.getNonOptionArguments()).thenReturn(Arrays.asList("test1")); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); cmd.run(ctxFactory.createContext(args)); assertEquals("no information about commands", ctxFactory.getError()); @@ -93,9 +97,23 @@ } @Test + public void verifyHelpFailsWithoutCommandGroupMetadataSource() { + HelpCommand cmd = new HelpCommand(); + + Arguments args = mock(Arguments.class); + when(args.getNonOptionArguments()).thenReturn(Arrays.asList("test1")); + cmd.setCommandInfoSource(commandInfoSource); + cmd.run(ctxFactory.createContext(args)); + + assertEquals("command group metadata source is missing", ctxFactory.getError()); + assertEquals("", ctxFactory.getOutput()); + } + + @Test public void verifyHelpNoArgPrintsListOfCommandsNoCommands() { HelpCommand cmd = new HelpCommand(); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); Arguments args = mock(Arguments.class); cmd.run(ctxFactory.createContext(args)); String expected = "list of commands:\n\n"; @@ -120,11 +138,12 @@ when(info2.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI, Environment.SHELL)); infoList.add(info2); - when(infos.getCommandInfos()).thenReturn(infoList); + when(commandInfoSource.getCommandInfos()).thenReturn(infoList); HelpCommand cmd = new HelpCommand(); cmd.setEnvironment(Environment.CLI); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); Arguments args = mock(Arguments.class); cmd.run(ctxFactory.createContext(args)); @@ -156,10 +175,11 @@ when(subcommand.getOptions()).thenReturn(subOptions); when(testCommandInfo.getSubcommands()).thenReturn(Collections.singletonList(subcommand)); - when(infos.getCommandInfo("test1")).thenReturn(testCommandInfo); + when(commandInfoSource.getCommandInfo("test1")).thenReturn(testCommandInfo); HelpCommand cmd = new HelpCommand(); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); Arguments args = mock(Arguments.class); when(args.getNonOptionArguments()).thenReturn(Arrays.asList("test1")); cmd.run(ctxFactory.createContext(args)); @@ -204,11 +224,12 @@ when(info4.getSummary()).thenReturn("test command 4"); when(info4.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI, Environment.SHELL)); - when(infos.getCommandInfos()).thenReturn(Arrays.asList(info2, helpInfo, info4, info3, info1)); + when(commandInfoSource.getCommandInfos()).thenReturn(Arrays.asList(info2, helpInfo, info4, info3, info1)); HelpCommand cmd = new HelpCommand(); cmd.setEnvironment(Environment.CLI); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); Arguments args = mock(Arguments.class); when(args.getNonOptionArguments()).thenReturn(new ArrayList()); cmd.run(ctxFactory.createContext(args)); @@ -253,11 +274,12 @@ when(info4.getSummary()).thenReturn("test command 4"); when(info4.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI)); - when(infos.getCommandInfos()).thenReturn(Arrays.asList(info2, helpInfo, info4, info3, info1)); + when(commandInfoSource.getCommandInfos()).thenReturn(Arrays.asList(info2, helpInfo, info4, info3, info1)); HelpCommand cmd = new HelpCommand(); cmd.setEnvironment(Environment.CLI); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); Arguments args = mock(Arguments.class); when(args.getNonOptionArguments()).thenReturn(new ArrayList()); cmd.run(ctxFactory.createContext(args)); @@ -291,11 +313,12 @@ when(info2.getSummary()).thenReturn("test command 2"); when(info2.getEnvironments()).thenReturn(EnumSet.of(Environment.SHELL)); - when(infos.getCommandInfos()).thenReturn(Arrays.asList(info2, helpInfo, info1)); + when(commandInfoSource.getCommandInfos()).thenReturn(Arrays.asList(info2, helpInfo, info1)); HelpCommand cmd = new HelpCommand(); cmd.setEnvironment(Environment.SHELL); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); Arguments args = mock(Arguments.class); when(args.getNonOptionArguments()).thenReturn(new ArrayList()); cmd.run(ctxFactory.createContext(args)); @@ -310,10 +333,11 @@ @Test public void verifyHelpUnknownCmdPrintsSummaries() { - when(infos.getCommandInfo("test1")).thenThrow(new CommandInfoNotFoundException("test1")); + when(commandInfoSource.getCommandInfo("test1")).thenThrow(new CommandInfoNotFoundException("test1")); HelpCommand cmd = new HelpCommand(); - cmd.setCommandInfoSource(infos); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); SimpleArguments args = new SimpleArguments(); args.addNonOptionArgument("test1"); cmd.run(ctxFactory.createContext(args)); @@ -325,5 +349,98 @@ assertEquals(expected, actual); } + @Test + public void verifyHelpKnownCmdPrintsCommandGroupSeeAlso() { + CommandInfo info1 = mock(CommandInfo.class); + when(info1.getName()).thenReturn("test1"); + when(info1.getUsage()).thenReturn("usage of test1 command"); + when(info1.getDescription()).thenReturn("description of test1 command"); + when(info1.getCommandGroups()).thenReturn(Collections.singletonList("group")); + when(info1.getOptions()).thenReturn(new Options()); + when(info1.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI)); + + CommandInfo info2 = mock(CommandInfo.class); + when(info2.getName()).thenReturn("test2"); + when(info2.getUsage()).thenReturn("usage of test2 command"); + when(info2.getDescription()).thenReturn("description of test2 command"); + when(info2.getCommandGroups()).thenReturn(Collections.singletonList("group")); + when(info2.getOptions()).thenReturn(new Options()); + when(info2.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI)); + + CommandInfo info3 = mock(CommandInfo.class); + when(info3.getName()).thenReturn("test3"); + when(info3.getUsage()).thenReturn("usage of test3 command"); + when(info3.getDescription()).thenReturn("description of test3 command"); + when(info3.getCommandGroups()).thenReturn(Collections.emptyList()); + when(info3.getOptions()).thenReturn(new Options()); + when(info3.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI)); + + when(commandInfoSource.getCommandInfo("test1")).thenReturn(info1); + when(commandInfoSource.getCommandInfo("test2")).thenReturn(info2); + when(commandInfoSource.getCommandInfo("test3")).thenReturn(info3); + when(commandInfoSource.getCommandInfos()).thenReturn(Arrays.asList(info1, info2, info3)); + + HelpCommand cmd = new HelpCommand(); + cmd.setEnvironment(Environment.CLI); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); + Arguments args = mock(Arguments.class); + when(args.getNonOptionArguments()).thenReturn(Collections.singletonList("test1")); + cmd.run(ctxFactory.createContext(args)); + + String actual = ctxFactory.getOutput(); + assertEquals("usage: thermostat usage of test1 command\n" + + " description of test1 command\n" + + "\n" + + "thermostat test1\n" + + " --help show usage of command\n" + + "\n" + + "See also:\n" + + " test2\n", actual); + } + + @Test + public void testCommandGroupMetadataDescriptions() { + when(commandGroupMetadataSource.getCommandGroupMetadata()).thenReturn(Collections.singletonMap( + "group1", new PluginConfiguration.CommandGroupMetadata("group1", "Group Name", 10) + )); + + CommandInfo commandInfo = mock(CommandInfo.class); + when(commandInfo.getName()).thenReturn("test1"); + when(commandInfo.getUsage()).thenReturn("usage of test1 command"); + when(commandInfo.getDescription()).thenReturn("description of test1 command"); + when(commandInfo.getSummary()).thenReturn("summary of test1 command"); + when(commandInfo.getCommandGroups()).thenReturn(Arrays.asList("group1", "group2")); + when(commandInfo.getOptions()).thenReturn(new Options()); + when(commandInfo.getEnvironments()).thenReturn(EnumSet.of(Environment.CLI)); + + when(commandInfoSource.getCommandInfo("test1")).thenReturn(commandInfo); + when(commandInfoSource.getCommandInfos()).thenReturn(Collections.singleton(commandInfo)); + + HelpCommand cmd = new HelpCommand(); + cmd.setEnvironment(Environment.CLI); + cmd.setCommandInfoSource(commandInfoSource); + cmd.setCommandGroupMetadataSource(commandGroupMetadataSource); + Arguments args = mock(Arguments.class); + cmd.run(ctxFactory.createContext(args)); + + String output = ctxFactory.getOutput(); + assertEquals("list of global options:\n" + + "\n" + + " --version display the version of the current thermostat installation\n" + + " --print-osgi-info print debug information related to the OSGi framework's boot/shutdown process\n" + + " --ignore-bundle-versions ignore exact bundle versions and use whatever version is available\n" + + " --boot-delegation boot delegation string passed on to the OSGi framework\n" + + "\n" + + "list of commands:\n" + + "\n" + + "Group Name: \n" + + " test1 summary of test1 command\n" + + " \n" + + "group2: \n" + + " test1 summary of test1 command\n" + + " \n", output); + } + } diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -157,6 +157,7 @@ private Version version; private DbServiceFactory dbServiceFactory; private CommandInfoSource infos; + private CommandGroupMetadataSource commandGroupMetadataSource; private ActionNotifier notifier; private LauncherImpl launcher; @@ -271,6 +272,10 @@ helpCommand.setCommandInfoSource(infos); + commandGroupMetadataSource = mock(CommandGroupMetadataSource.class); + when(commandGroupMetadataSource.getCommandGroupMetadata()).thenReturn(Collections.emptyMap()); + helpCommand.setCommandGroupMetadataSource(commandGroupMetadataSource); + registry = mock(BundleManager.class); timerFactory = new TestTimerFactory(); diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java Tue Feb 14 11:26:33 2017 -0500 @@ -36,6 +36,7 @@ package com.redhat.thermostat.launcher.internal; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -51,8 +52,10 @@ import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandGroupMetadata; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; @@ -136,10 +139,13 @@ " test\n" + " summary\n" + " description\n" + - " " + - " shell" + - " cli" + - " " + + " \n" + + " group\n" + + " \n" + + " \n" + + " shell\n" + + " cli\n" + + " \n" + " \n" + " foo1.0\n" + " bar1.0\n" + @@ -165,6 +171,7 @@ assertEquals("test", newCommand.getCommandName()); assertEquals("summary", newCommand.getSummary()); assertEquals("description", newCommand.getDescription()); + assertEquals(Collections.singletonList("group"), newCommand.getCommandGroups()); Options opts = newCommand.getOptions(); assertTrue(opts.getOptions().isEmpty()); assertTrue(opts.getRequiredOptions().isEmpty()); @@ -244,6 +251,58 @@ } @Test + public void testConfigurationThatAddsNewCommandWithCommandGroupAndMetadata() throws UnsupportedEncodingException { + String config = "\n" + + "\n" + + " \n" + + " \n" + + " test\n" + + " summary\n" + + " description\n" + + " \n" + + " group\n" + + " \n" + + " \n" + + " shell\n" + + " cli\n" + + " \n" + + " \n" + + " foo1.0\n" + + " bar1.0\n" + + " baz1.0\n" + + " \n" + + " \n" + + " thermostat-foo\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " group\n" + + " Group Name\n" + + " 5\n" + + " \n" + + " \n" + + " foo\n" + + " FooGroup\n" + + " 7\n" + + " \n" + + " \n" + + ""; + + PluginConfiguration result = new PluginConfigurationParser() + .parse("test", new ByteArrayInputStream(config.getBytes("UTF-8"))); + + List metadata = result.getCommandGroupMetadata(); + assertThat(metadata, is(equalTo(Arrays.asList( + new CommandGroupMetadata("group", "Group Name", 5), + new CommandGroupMetadata("foo", "FooGroup", 7) + )))); + } + + @Test public void testSpacesAtStartAndEndAreTrimmed() throws UnsupportedEncodingException { String config = "\n" + " COMMAND_GROUPS = Collections.singletonList("group"); final String USAGE = "usage"; final Options OPTIONS = new Options(); final Set ENVIRONMENTS = EnumSet.of(Environment.SHELL); @@ -204,6 +210,7 @@ NewCommand cmd = mock(NewCommand.class); when(cmd.getCommandName()).thenReturn(NAME); when(cmd.getDescription()).thenReturn(DESCRIPTION); + when(cmd.getCommandGroups()).thenReturn(COMMAND_GROUPS); when(usageBuilder.getUsage(NAME, false, OPTIONS)).thenReturn(USAGE); when(cmd.getOptions()).thenReturn(OPTIONS); when(cmd.getEnvironments()).thenReturn(ENVIRONMENTS); @@ -219,6 +226,7 @@ assertEquals(NAME, result.getName()); assertEquals(DESCRIPTION, result.getDescription()); + assertEquals(COMMAND_GROUPS, result.getCommandGroups()); assertEquals(USAGE, result.getUsage()); assertEquals(OPTIONS, result.getOptions()); @@ -228,6 +236,30 @@ } @Test + public void verifyCommandGroupMetadataAreCreated() throws IOException { + CommandGroupMetadata metadata1 = new CommandGroupMetadata("foo", "FooGroup", 7); + CommandGroupMetadata metadata2 = new CommandGroupMetadata("bar", "BarGroup", 9); + CommandGroupMetadata metadata3 = new CommandGroupMetadata("foo", "FooGroup2", 11); // this one should get ignored + + Path pluginDir = sysPluginRootDir.resolve("plugin1"); + Files.createDirectories(pluginDir); + + when(parserResult.getCommandGroupMetadata()).thenReturn(Arrays.asList(metadata1, metadata2, metadata3)); + + PluginInfoSource source = new PluginInfoSource(jarRootDir.toFile(), sysPluginRootDir.toFile(), + userPluginRootDir.toFile(), sysConfRootDir.toFile(), userConfRootDir.toFile(), + parser, usageBuilder); + + Map actual = source.getCommandGroupMetadata(); + + Map expected = new HashMap<>(); + expected.put("foo", metadata1); + expected.put("bar", metadata2); + + assertThat(actual, is(equalTo(expected))); + } + + @Test public void testConfigurationSysConfig() throws IOException { String pluginID = "com.redhat.thermostat.sys"; String configName = "config.conf"; diff -r 6d713f241106 -r 767b2627c92d launcher/src/test/java/com/redhat/thermostat/launcher/internal/TestCommandInfo.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/TestCommandInfo.java Tue Feb 14 09:07:27 2017 -0500 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/TestCommandInfo.java Tue Feb 14 11:26:33 2017 -0500 @@ -50,6 +50,7 @@ private String name; private String summary; private String description; + private List commandGroups; private String usage; private Options options = new Options(); @@ -83,6 +84,15 @@ } @Override + public List getCommandGroups() { + return commandGroups; + } + + public void addCommandGroup(String commandGroup) { + commandGroups.add(commandGroup); + } + + @Override public String getUsage() { return usage; }