blob: c5e6f61d5c6e26c460eb04d8b5b898c3f8c6e9ec [file] [log] [blame]
/*
* 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 static org.apache.geode.distributed.ConfigurationProperties.USER_COMMAND_PACKAGES;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import org.springframework.shell.converters.EnumConverter;
import org.springframework.shell.converters.SimpleFileConverter;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.MethodTarget;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.distributed.ConfigurationProperties;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.management.cli.Disabled;
import org.apache.geode.management.cli.GfshCommand;
import org.apache.geode.management.internal.cli.commands.VersionCommand;
import org.apache.geode.management.internal.cli.help.Helper;
import org.apache.geode.management.internal.cli.shell.Gfsh;
import org.apache.geode.management.internal.util.ClasspathScanLoadHelper;
import org.apache.geode.util.internal.GeodeGlossary;
/**
*
* this only takes care of loading all available command markers and converters from the application
*
* @since GemFire 7.0
*/
public class CommandManager {
private static final String USER_CMD_PACKAGES_PROPERTY =
GeodeGlossary.GEMFIRE_PREFIX + USER_COMMAND_PACKAGES;
private static final String USER_CMD_PACKAGES_ENV_VARIABLE = "GEMFIRE_USER_COMMAND_PACKAGES";
private final Helper helper = new Helper();
private final List<Converter<?>> converters = new ArrayList<>();
private final List<CommandMarker> commandMarkers = new ArrayList<>();
private Properties cacheProperties;
private LogWrapper logWrapper;
private InternalCache cache;
/**
* this constructor is used from Gfsh VM. We are getting the user-command-package from system
* environment. used by Gfsh.
*/
public CommandManager() {
this(null, null);
}
/**
* this is used when getting the instance in a cache server. We are getting the
* user-command-package from distribution properties. used by OnlineCommandProcessor.
*/
public CommandManager(final Properties cacheProperties, InternalCache cache) {
if (cacheProperties != null) {
this.cacheProperties = cacheProperties;
}
this.cache = cache;
logWrapper = LogWrapper.getInstance(cache);
loadCommands();
}
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.");
}
}
private Set<String> getUserCommandPackages() {
final Set<String> userCommandPackages = new HashSet<>();
List<String> userCommandSources = new ArrayList<>();
// Find by packages specified by the system property
if (System.getProperty(USER_CMD_PACKAGES_PROPERTY) != null) {
userCommandSources.add(System.getProperty(USER_CMD_PACKAGES_PROPERTY));
}
// Find by packages specified by the environment variable
if (System.getenv().containsKey(USER_CMD_PACKAGES_ENV_VARIABLE)) {
userCommandSources.add(System.getenv().get(USER_CMD_PACKAGES_ENV_VARIABLE));
}
// 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()) {
userCommandSources.add(cacheUserCmdPackages);
}
}
for (String source : userCommandSources) {
userCommandPackages.addAll(Arrays.asList(source.split(",")));
}
return userCommandPackages;
}
private void loadUserCommands(ClasspathScanLoadHelper scanner, Set<String> restrictedToPackages) {
if (restrictedToPackages.size() == 0) {
return;
}
// Load commands found in all of the packages
try {
Set<Class<?>> foundClasses = scanner.scanPackagesForClassesImplementing(CommandMarker.class,
restrictedToPackages.toArray(new String[] {}));
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 (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
}
/**
* Loads commands via {@link ServiceLoader} from {@link ClassPathLoader}.
*
* @since GemFire 8.1
*/
private void loadPluginCommands() {
ServiceLoader<CommandMarker> loader =
ServiceLoader.load(CommandMarker.class, ClassPathLoader.getLatest().asClassLoader());
Iterator<CommandMarker> iterator = loader.iterator();
try {
while (iterator.hasNext()) {
try {
add(iterator.next());
} catch (Throwable t) {
logWrapper.warning("Could not load plugin command: " + t.getMessage());
}
}
} catch (Throwable th) {
logWrapper.severe("Could not load plugin commands in the latest classLoader.", th);
}
}
private void loadCommands() {
Set<String> userCommandPackages = getUserCommandPackages();
Set<String> packagesToScan = new HashSet<>(userCommandPackages);
packagesToScan.add("org.apache.geode.management.internal.cli.converters");
packagesToScan.add("org.springframework.shell.converters");
packagesToScan.add(GfshCommand.class.getPackage().getName());
packagesToScan.add(VersionCommand.class.getPackage().getName());
// Create one scanner to be used everywhere
try (ClasspathScanLoadHelper scanner = new ClasspathScanLoadHelper(packagesToScan)) {
loadUserCommands(scanner, userCommandPackages);
loadPluginCommands();
loadGeodeCommands(scanner);
loadConverters(scanner);
}
}
private void loadConverters(ClasspathScanLoadHelper scanner) {
Set<Class<?>> foundClasses;
// Converters
try {
foundClasses = scanner.scanPackagesForClassesImplementing(Converter.class,
"org.apache.geode.management.internal.cli.converters");
for (Class<?> klass : foundClasses) {
try {
Converter<?> object = (Converter<?>) klass.newInstance();
add(object);
} catch (Exception e) {
logWrapper.warning(
"Could not load Converter from: " + klass + " due to " + e.getLocalizedMessage()); // continue
}
}
raiseExceptionIfEmpty(foundClasses, "Converters");
// Spring shell's converters
foundClasses = scanner.scanPackagesForClassesImplementing(Converter.class,
"org.springframework.shell.converters");
for (Class<?> klass : foundClasses) {
if (!SHL_CONVERTERS_TOSKIP.contains(klass)) {
try {
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 (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
}
private void loadGeodeCommands(ClasspathScanLoadHelper scanner) {
// CommandMarkers
Set<Class<?>> foundClasses;
try {
// geode's commands
foundClasses = scanner.scanPackagesForClassesImplementing(CommandMarker.class,
GfshCommand.class.getPackage().getName(),
VersionCommand.class.getPackage().getName());
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");
// do not add Spring shell's commands for now. When we add it, we need to tell the parser that
// these are offline commands.
} catch (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
}
/** Skip some of the Converters from Spring Shell for our customization */
@Immutable
private static final List<Class> SHL_CONVERTERS_TOSKIP =
Collections.unmodifiableList(Arrays.asList(
// skip springs SimpleFileConverter to use our own FilePathConverter
SimpleFileConverter.class,
// skip spring's EnumConverter to use our own EnumConverter
EnumConverter.class));
public List<Converter<?>> getConverters() {
return converters;
}
public List<CommandMarker> getCommandMarkers() {
return commandMarkers;
}
/**
* Method to add new Converter
*/
private void add(Converter<?> converter) {
if (CommandManagerAware.class.isAssignableFrom(converter.getClass())) {
((CommandManagerAware) converter).setCommandManager(this);
}
converters.add(converter);
}
/**
* Method to add new Commands to the parser
*/
void add(CommandMarker commandMarker) {
Disabled classDisabled = commandMarker.getClass().getAnnotation(Disabled.class);
if (classDisabled != null && (classDisabled.unlessPropertyIsSet().isEmpty()
|| System.getProperty(classDisabled.unlessPropertyIsSet()) == null)) {
return;
}
// inject the cache into the commands
if (GfshCommand.class.isAssignableFrom(commandMarker.getClass())) {
((GfshCommand) commandMarker).setCache(cache);
}
// inject the commandManager into the commands
if (CommandManagerAware.class.isAssignableFrom(commandMarker.getClass())) {
((CommandManagerAware) commandMarker).setCommandManager(this);
}
commandMarkers.add(commandMarker);
for (Method method : commandMarker.getClass().getMethods()) {
CliCommand cliCommand = method.getAnnotation(CliCommand.class);
CliAvailabilityIndicator availability = method.getAnnotation(CliAvailabilityIndicator.class);
if (cliCommand == null && availability == null) {
continue;
}
if (cliCommand != null) {
helper.addCommand(cliCommand, method);
}
if (availability != null) {
helper.addAvailabilityIndicator(availability, new MethodTarget(method, commandMarker));
}
}
}
public Helper getHelper() {
return helper;
}
public String obtainHelp(String buffer) {
int terminalWidth = -1;
Gfsh gfsh = Gfsh.getCurrentInstance();
if (gfsh != null) {
terminalWidth = gfsh.getTerminalWidth();
}
return helper.getHelp(buffer, terminalWidth);
}
public String obtainHint(String topic) {
return helper.getHint(topic);
}
}