/*
* 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;
import org.apache.geode.distributed.ConfigurationProperties;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.internal.cli.annotation.CliArgument;
import org.apache.geode.management.internal.cli.help.CliTopic;
import org.apache.geode.management.internal.cli.parser.*;
import org.apache.geode.management.internal.cli.parser.jopt.JoptOptionParser;
import org.apache.geode.management.internal.cli.util.ClasspathScanLoadHelper;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Map.Entry;
import static org.apache.geode.distributed.ConfigurationProperties.*;
/**
*
* @since GemFire 7.0
*/
public class CommandManager {
// 1. Load Commands, availability indicators - Take from GfshParser
// 2. Load Converters - Take from GfshParser
// 3. Load Result Converters - Add
private static final Object INSTANCE_LOCK = new Object();
private static CommandManager INSTANCE = null;
public static final String USER_CMD_PACKAGES_PROPERTY =
DistributionConfig.GEMFIRE_PREFIX + USER_COMMAND_PACKAGES;
public static final String USER_CMD_PACKAGES_ENV_VARIABLE = "GEMFIRE_USER_COMMAND_PACKAGES";
private Properties cacheProperties;
private LogWrapper logWrapper;
private CommandManager(final boolean loadDefaultCommands, final Properties cacheProperties)
throws ClassNotFoundException, IOException {
if (cacheProperties != null) {
this.cacheProperties = cacheProperties;
}
logWrapper = LogWrapper.getInstance();
if (loadDefaultCommands) {
loadCommands();
if (logWrapper.fineEnabled()) {
logWrapper.fine("Commands Loaded: " + commands.keySet());
logWrapper
.fine("Command Availability Indicators Loaded: " + availabilityIndicators.keySet());
logWrapper.fine("Converters Loaded: " + converters);
}
}
}
private void loadUserCommands() throws ClassNotFoundException, IOException {
final Set<String> userCommandPackages = new HashSet<String>();
// Find by packages specified by the system property
if (System.getProperty(USER_CMD_PACKAGES_PROPERTY) != null) {
StringTokenizer tokenizer =
new StringTokenizer(System.getProperty(USER_CMD_PACKAGES_PROPERTY), ",");
while (tokenizer.hasMoreTokens()) {
userCommandPackages.add(tokenizer.nextToken());
}
}
// Find by packages specified by the environment variable
if (System.getenv().containsKey(USER_CMD_PACKAGES_ENV_VARIABLE)) {
StringTokenizer tokenizer =
new StringTokenizer(System.getenv().get(USER_CMD_PACKAGES_ENV_VARIABLE), ",");
while (tokenizer.hasMoreTokens()) {
userCommandPackages.add(tokenizer.nextToken());
}
}
// Find by packages specified in the distribution config
if (this.cacheProperties != null) {
String cacheUserCmdPackages =
this.cacheProperties.getProperty(ConfigurationProperties.USER_COMMAND_PACKAGES);
if (cacheUserCmdPackages != null && !cacheUserCmdPackages.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(cacheUserCmdPackages, ",");
while (tokenizer.hasMoreTokens()) {
userCommandPackages.add(tokenizer.nextToken());
}
}
}
// Load commands found in all of the packages
for (String userCommandPackage : userCommandPackages) {
try {
Set<Class<?>> foundClasses =
ClasspathScanLoadHelper.loadAndGet(userCommandPackage, CommandMarker.class, true);
for (Class<?> klass : foundClasses) {
try {
add((CommandMarker) klass.newInstance());
} catch (Exception e) {
logWrapper.warning("Could not load User Commands from: " + klass + " due to "
+ e.getLocalizedMessage()); // continue
}
}
raiseExceptionIfEmpty(foundClasses, "User Command");
} catch (ClassNotFoundException e) {
logWrapper.warning("Could not load User Commands due to " + e.getLocalizedMessage());
throw e;
} catch (IOException e) {
logWrapper.warning("Could not load User Commands due to " + e.getLocalizedMessage());
throw e;
} catch (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
}
}
/**
* Loads commands via {@link ServiceLoader} from {@link ClassPathLoader}.
*
* @since GemFire 8.1
*/
private void loadPluginCommands() {
final Iterator<CommandMarker> iterator = ServiceLoader
.load(CommandMarker.class, ClassPathLoader.getLatest().asClassLoader()).iterator();
while (iterator.hasNext()) {
try {
final CommandMarker commandMarker = iterator.next();
try {
add(commandMarker);
} catch (Exception e) {
logWrapper.warning("Could not load Command from: " + commandMarker.getClass() + " due to "
+ e.getLocalizedMessage(), e); // continue
}
} catch (ServiceConfigurationError e) {
logWrapper.severe("Could not load Command: " + e.getLocalizedMessage(), e); // continue
}
}
}
private void loadCommands() throws ClassNotFoundException, IOException {
loadUserCommands();
loadPluginCommands();
// CommandMarkers
Set<Class<?>> foundClasses = null;
try {
foundClasses = ClasspathScanLoadHelper.loadAndGet(
"org.apache.geode.management.internal.cli.commands", CommandMarker.class, true);
for (Class<?> klass : foundClasses) {
try {
add((CommandMarker) klass.newInstance());
} catch (Exception e) {
logWrapper.warning(
"Could not load Command from: " + klass + " due to " + e.getLocalizedMessage()); // continue
}
}
raiseExceptionIfEmpty(foundClasses, "Commands");
} catch (ClassNotFoundException e) {
logWrapper.warning("Could not load Commands due to " + e.getLocalizedMessage());
throw e;
} catch (IOException e) {
logWrapper.warning("Could not load Commands due to " + e.getLocalizedMessage());
throw e;
} catch (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
// Converters
try {
foundClasses = ClasspathScanLoadHelper
.loadAndGet("org.apache.geode.management.internal.cli.converters", Converter.class, true);
for (Class<?> klass : foundClasses) {
try {
add((Converter<?>) klass.newInstance());
} catch (Exception e) {
logWrapper.warning(
"Could not load Converter from: " + klass + " due to " + e.getLocalizedMessage()); // continue
}
}
raiseExceptionIfEmpty(foundClasses, "Converters");
} catch (ClassNotFoundException e) {
logWrapper.warning("Could not load Converters due to " + e.getLocalizedMessage());
throw e;
} catch (IOException e) {
logWrapper.warning("Could not load Converters due to " + e.getLocalizedMessage());
throw e;
} catch (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
// Roo's Converters
try {
foundClasses = ClasspathScanLoadHelper.loadAndGet("org.springframework.shell.converters",
Converter.class, true);
for (Class<?> klass : foundClasses) {
try {
if (!SHL_CONVERTERS_TOSKIP.contains(klass.getName())) {
add((Converter<?>) klass.newInstance());
}
} catch (Exception e) {
logWrapper.warning(
"Could not load Converter from: " + klass + " due to " + e.getLocalizedMessage()); // continue
}
}
raiseExceptionIfEmpty(foundClasses, "Basic Converters");
} catch (ClassNotFoundException e) {
logWrapper.warning("Could not load Default Converters due to " + e.getLocalizedMessage());// TODO
// -
// Abhishek:
// Should
// these
// converters
// be
// moved
// in
// GemFire?
throw e;
} catch (IOException e) {
logWrapper.warning("Could not load Default Converters due to " + e.getLocalizedMessage());// TODO
// -
// Abhishek:
// Should
// these
// converters
// be
// moved
// in
// GemFire?
throw e;
} catch (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
}
private static void raiseExceptionIfEmpty(Set<Class<?>> foundClasses, String errorFor)
throws IllegalStateException {
if (foundClasses == null || foundClasses.isEmpty()) {
throw new IllegalStateException(
"Required " + errorFor + " classes were not loaded. Check logs for errors.");
}
}
public static CommandManager getInstance() throws ClassNotFoundException, IOException {
return getInstance(true);
}
public static CommandManager getInstance(Properties cacheProperties)
throws ClassNotFoundException, IOException {
return getInstance(true, cacheProperties);
}
// For testing.
public static void clearInstance() {
synchronized (INSTANCE_LOCK) {
INSTANCE = null;
}
}
// This method exists for test code use only ...
/* package */static CommandManager getInstance(boolean loadDefaultCommands)
throws ClassNotFoundException, IOException {
return getInstance(loadDefaultCommands, null);
}
private static CommandManager getInstance(boolean loadDefaultCommands, Properties cacheProperties)
throws ClassNotFoundException, IOException {
synchronized (INSTANCE_LOCK) {
if (INSTANCE == null) {
INSTANCE = new CommandManager(loadDefaultCommands, cacheProperties);
}
return INSTANCE;
}
}
public static CommandManager getExisting() {
// if (INSTANCE == null) {
// throw new IllegalStateException("CommandManager doesn't exist.");
// }
return INSTANCE;
}
/** Skip some of the Converters from Spring Shell for our customization */
private static List<String> SHL_CONVERTERS_TOSKIP = new ArrayList<String>();
static {
// Over-ridden by cggm.internal.cli.converters.BooleanConverter
SHL_CONVERTERS_TOSKIP.add("org.springframework.shell.converters.BooleanConverter");
// Over-ridden by cggm.internal.cli.converters.EnumConverter
SHL_CONVERTERS_TOSKIP.add("org.springframework.shell.converters.EnumConverter");
}
/**
* List of converters which should be populated first before any command can be added
*/
private final List<Converter<?>> converters = new ArrayList<Converter<?>>();
/**
* Map of command string and actual CommandTarget object
*
* This map can also be implemented as a trie to support command abbreviation
*/
private final Map<String, CommandTarget> commands = new TreeMap<String, CommandTarget>();
/**
* This method will store the all the availabilityIndicators
*/
private final Map<String, AvailabilityTarget> availabilityIndicators =
new HashMap<String, AvailabilityTarget>();
/**
*/
private final Map<String, CliTopic> topics = new TreeMap<String, CliTopic>();
/**
* Method to add new Converter
*
* @param converter
*/
public void add(Converter<?> converter) {
converters.add(converter);
}
/**
* Method to add new Commands to the parser
*
* @param commandMarker
*/
public void add(CommandMarker commandMarker) {
// First we need to find out all the methods marked with
// Command annotation
Method[] methods = commandMarker.getClass().getMethods();
for (Method method : methods) {
if (method.getAnnotation(CliCommand.class) != null) {
//
// First Build the option parser
//
// Create the empty LinkedLists for storing the argument and
// options
LinkedList<Argument> arguments = new LinkedList<Argument>();
LinkedList<Option> options = new LinkedList<Option>();
// Also we need to create the OptionParser for each command
GfshOptionParser optionParser = getOptionParser();
// Now get all the parameters annotations of the method
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
// Also get the parameter Types
Class<?>[] parameterTypes = method.getParameterTypes();
int parameterNo = 0;
for (int i = 0; i < parameterAnnotations.length; i++) {
// Get all the annotations for this specific parameter
Annotation[] annotations = parameterAnnotations[i];
// Also get the parameter type for this parameter
Class<?> parameterType = parameterTypes[i];
boolean paramFound = false;
String valueSeparator = CliMetaData.ANNOTATION_NULL_VALUE;
for (Annotation annotation : annotations) {
if (annotation instanceof CliArgument) {
// Here we need to create the argument Object
Argument argumentToAdd =
createArgument((CliArgument) annotation, parameterType, parameterNo);
arguments.add(argumentToAdd);
parameterNo++;
} else if (annotation instanceof CliOption) {
Option createdOption =
createOption((CliOption) annotation, parameterType, parameterNo);
if (!CliMetaData.ANNOTATION_NULL_VALUE.equals(valueSeparator)) { // CliMetaData was
// found earlier
createdOption.setValueSeparator(valueSeparator);
// reset valueSeparator back to null
valueSeparator = CliMetaData.ANNOTATION_NULL_VALUE;
} else { // CliMetaData is yet to be found
paramFound = true;
}
options.add(createdOption);
parameterNo++;
} else if (annotation instanceof CliMetaData) {
valueSeparator = ((CliMetaData) annotation).valueSeparator();
if (!CliMetaData.ANNOTATION_NULL_VALUE.equals(valueSeparator)) {
if (paramFound) { // CliOption was detected earlier
Option lastAddedOption = options.getLast();
lastAddedOption.setValueSeparator(valueSeparator);
// reset valueSeparator back to null
valueSeparator = CliMetaData.ANNOTATION_NULL_VALUE;
} // param not found yet, store valueSeparator value
} else {
// reset valueSeparator back to null
valueSeparator = CliMetaData.ANNOTATION_NULL_VALUE;
}
}
}
}
optionParser.setArguments(arguments);
optionParser.setOptions(options);
//
// Now build the commandTarget
//
// First build the MethodTarget for the command Method
GfshMethodTarget gfshMethodTarget = new GfshMethodTarget(method, commandMarker);
// Fetch the value array from the cliCommand annotation
CliCommand cliCommand = method.getAnnotation(CliCommand.class);
String[] values = cliCommand.value();
// First string will point to the command
// rest of them will act as synonyms
String commandName = null;
String[] synonyms = null;
if (values.length > 1) {
synonyms = new String[values.length - 1];
}
commandName = values[0];
for (int j = 1; j < values.length; j++) {
synonyms[j - 1] = values[j];
}
// Create the commandTarget object
CommandTarget commandTarget = new CommandTarget(commandName, synonyms, gfshMethodTarget,
optionParser, null, cliCommand.help());
// Now for each string in values put an entry in the commands
// map
for (String string : values) {
if (commands.get(string) == null) {
commands.put(string, commandTarget);
} else {
// TODO Handle collision
logWrapper.info("Multiple commands configured with the same name: " + string);
}
}
if (CliUtil.isGfshVM()) {
CliMetaData commandMetaData = method.getAnnotation(CliMetaData.class);
if (commandMetaData != null) {
String[] relatedTopics = commandMetaData.relatedTopic();
// System.out.println("relatedTopic :: "+Arrays.toString(relatedTopics));
for (String topicName : relatedTopics) {
CliTopic topic = topics.get(topicName);
if (topic == null) {
topic = new CliTopic(topicName);
topics.put(topicName, topic);
}
topic.addCommandTarget(commandTarget);
}
}
}
} else if (method.getAnnotation(CliAvailabilityIndicator.class) != null) {
// Now add this availability Indicator to the list of
// availability Indicators
CliAvailabilityIndicator cliAvailabilityIndicator =
method.getAnnotation(CliAvailabilityIndicator.class);
// Create a AvailabilityTarget for this availability Indicator
AvailabilityTarget availabilityIndicator = new AvailabilityTarget(commandMarker, method);
String[] value = cliAvailabilityIndicator.value();
for (String string : value) {
availabilityIndicators.put(string, availabilityIndicator);
}
}
}
// Now we must update all the existing CommandTargets to add
// this availability Indicator if it applies to them
updateAvailabilityIndicators();
}
/**
* Will update all the references to availability Indicators for commands
*
*/
public void updateAvailabilityIndicators() {
for (String string : availabilityIndicators.keySet()) {
CommandTarget commandTarget = commands.get(string);
if (commandTarget != null) {
commandTarget.setAvailabilityIndicator(availabilityIndicators.get(string));
}
}
}
/**
* Creates a new {@link Option} instance
*
* @param cliOption
* @param parameterType
* @param parameterNo
* @return Option
*/
public Option createOption(CliOption cliOption, Class<?> parameterType, int parameterNo) {
Option option = new Option();
// First set the Option identifiers
List<String> synonyms = new ArrayList<String>();
for (String string : cliOption.key()) {
if (!option.setLongOption(string)) {
synonyms.add(string);
}
}
option.setSynonyms(synonyms);
if (!(option.getAggregate().size() > 0)) {
logWrapper.warning("Option should have a name");
}
// Set the option Help
option.setHelp(cliOption.help());
// Set whether the option is required or not
option.setRequired(cliOption.mandatory());
// Set the fields related to option value
option.setSystemProvided(cliOption.systemProvided());
option.setSpecifiedDefaultValue(cliOption.specifiedDefaultValue());
option.setUnspecifiedDefaultValue(cliOption.unspecifiedDefaultValue());
// Set the things which are useful for value conversion and
// auto-completion
option.setContext(cliOption.optionContext());
// Find the matching Converter<?> for this option
option.setConverter(getConverter(parameterType, option.getContext()));
option.setDataType(parameterType);
option.setParameterNo(parameterNo);
return option;
}
/**
* Creates a new {@link Argument} instance
*
* @param cliArgument
* @param parameterType
* @param parameterNo
* @return Argument
*/
public Argument createArgument(CliArgument cliArgument, Class<?> parameterType, int parameterNo) {
Argument argument = new Argument();
argument.setArgumentName(cliArgument.name());
argument.setContext(cliArgument.argumentContext());
argument.setConverter(getConverter(parameterType, argument.getContext()));
argument.setHelp(cliArgument.help());
argument.setRequired(cliArgument.mandatory());
argument.setDataType(parameterType);
argument.setParameterNo(parameterNo);
argument.setUnspecifiedDefaultValue(cliArgument.unspecifiedDefaultValue());
argument.setSystemProvided(cliArgument.systemProvided());
return argument;
}
/**
* Looks for a matching {@link Converter}
*
* @param parameterType
* @param context
* @return {@link Converter}
*/
public Converter<?> getConverter(Class<?> parameterType, String context) {
for (Converter<?> converter : converters) {
if (converter.supports(parameterType, context)) {
return converter;
}
}
return null;
}
/**
* For the time being this method returns a {@link JoptOptionParser} object but in the future we
* can change which optionParser should be returned.
*
* @return {@link GfshOptionParser}
*/
private GfshOptionParser getOptionParser() {
return new JoptOptionParser();
}
/**
* @return the commands
*/
public Map<String, CommandTarget> getCommands() {
return Collections.unmodifiableMap(commands);
}
AvailabilityTarget getAvailabilityIndicator(Object key) {
return availabilityIndicators.get(key);
}
public Set<String> getTopicNames() {
Set<String> topicsNames = topics.keySet();
return Collections.unmodifiableSet(topicsNames);
}
public List<CliTopic> getTopics() {
List<CliTopic> topicsList = new ArrayList<CliTopic>(topics.values());
return Collections.unmodifiableList(topicsList);
}
public CliTopic getTopic(String topicName) {
CliTopic foundTopic = topics.get(topicName);
if (foundTopic == null) {
Set<Entry<String, CliTopic>> entries = topics.entrySet();
for (Entry<String, CliTopic> entry : entries) {
if (entry.getKey().equalsIgnoreCase(topicName)) {
foundTopic = entry.getValue();
break;
}
}
}
return foundTopic;
}
}