blob: 517f84d849437b7a3bfbcc6622fef0e639cf8711 [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.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.cache.InternalCache;
import org.apache.geode.internal.classloader.ClassPathLoader;
import org.apache.geode.management.cli.Disabled;
import org.apache.geode.management.cli.GfshCommand;
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 static final String SPRING_CONVERTER_PACKAGE = "org.springframework.shell.converters";
/** Skip some of the Converters from Spring Shell for our customization */
@Immutable
private static final List<Class<?>> SPRING_CONVERTERS_TO_SKIP =
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));
private final Helper helper = new Helper();
private final List<Converter<?>> converters = new ArrayList<>();
private final List<CommandMarker> commandMarkers = new ArrayList<>();
private final Properties cacheProperties = new Properties();
private final LogWrapper logWrapper;
private final 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 newCacheProperties, InternalCache cache) {
if (newCacheProperties != null) {
cacheProperties.putAll(newCacheProperties);
}
this.cache = cache;
logWrapper = LogWrapper.getInstance(cache);
loadCommands();
loadConverters();
}
private static void raiseExceptionIfEmpty(Set<?> 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
String cacheUserCmdPackages =
cacheProperties.getProperty(ConfigurationProperties.USER_COMMAND_PACKAGES, "");
if (!cacheUserCmdPackages.isEmpty()) {
userCommandSources.add(cacheUserCmdPackages);
}
for (String source : userCommandSources) {
userCommandPackages.addAll(Arrays.asList(source.split(",")));
}
return userCommandPackages;
}
private void loadUserDefinedCommands() {
String[] userCommandPackages = getUserCommandPackages().toArray(new String[] {});
if (userCommandPackages.length == 0) {
return;
}
// Load commands found in all of the packages
try (ClasspathScanLoadHelper scanner = new ClasspathScanLoadHelper(userCommandPackages)) {
Set<Class<?>> foundClasses =
scanner.scanPackagesForClassesImplementing(CommandMarker.class, userCommandPackages);
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 loadGeodeCommands() {
ServiceLoader<CommandMarker> commandMarkers =
ServiceLoader.load(CommandMarker.class, ClassPathLoader.getLatest().asClassLoader());
boolean loadedAtLeastOneCommand = false;
for (CommandMarker commandMarker : commandMarkers) {
add(commandMarker);
loadedAtLeastOneCommand = true;
}
if (!loadedAtLeastOneCommand) {
throw new IllegalStateException(
"Required Command classes were not loaded. Check logs for errors.");
}
}
private void loadConverters() {
loadGeodeDefinedConverters();
loadSpringDefinedConverters();
}
private void loadCommands() {
loadGeodeCommands();
loadUserDefinedCommands();
}
private void loadSpringDefinedConverters() {
try (ClasspathScanLoadHelper scanner = new ClasspathScanLoadHelper(SPRING_CONVERTER_PACKAGE)) {
// Spring shell's converters
Set<Class<?>> foundClasses =
scanner.scanPackagesForClassesImplementing(Converter.class, SPRING_CONVERTER_PACKAGE);
for (Class<?> klass : foundClasses) {
if (!SPRING_CONVERTERS_TO_SKIP.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, "Spring Converter");
} catch (IllegalStateException e) {
logWrapper.warning(e.getMessage(), e);
throw e;
}
}
private void loadGeodeDefinedConverters() {
ServiceLoader<Converter> converters =
ServiceLoader.load(Converter.class, ClassPathLoader.getLatestAsClassLoader());
boolean loadedAtLeastOneConverter = false;
for (Converter<?> converter : converters) {
add(converter);
loadedAtLeastOneConverter = true;
}
if (!loadedAtLeastOneConverter) {
throw new IllegalStateException(
"Required Converter classes were not loaded. Check logs for errors.");
}
}
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);
}
}