/**
*
*/
package vnet.sms.common.shell.springshell.internal.commands;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import vnet.sms.common.shell.springshell.MethodTarget;
import vnet.sms.common.shell.springshell.command.CliAvailabilityIndicator;
import vnet.sms.common.shell.springshell.command.CliCommand;
import vnet.sms.common.shell.springshell.command.CommandMarker;
import vnet.sms.common.shell.springshell.internal.NaturalOrderComparator;
import vnet.sms.common.shell.springshell.internal.util.Assert;
import vnet.sms.common.shell.springshell.internal.util.StringUtils;
/**
* @author obergner
*
*/
public class CommandsRegistry implements BeanPostProcessor {
private static final Comparator<Object> COMPARATOR = new NaturalOrderComparator<Object>();
private final Logger log = LoggerFactory
.getLogger(getClass());
private final Set<CommandMarker> commands = new HashSet<CommandMarker>();
private final Map<String, MethodTarget> availabilityIndicators = new HashMap<String, MethodTarget>();
// ------------------------------------------------------------------------
// API
// ------------------------------------------------------------------------
public Set<CommandMarker> getAllCommands() {
return Collections.unmodifiableSet(this.commands);
}
public SortedSet<String> getAllCommandNames() {
final SortedSet<String> result = new TreeSet<String>(COMPARATOR);
for (final Object o : this.commands) {
final Method[] methods = o.getClass().getMethods();
for (final Method m : methods) {
final CliCommand cmd = m.getAnnotation(CliCommand.class);
if (cmd != null) {
result.addAll(Arrays.asList(cmd.value()));
}
}
}
return result;
}
public Set<MethodTarget> findMatchingCommands(final String buffer,
final boolean strictMatching,
final boolean checkAvailabilityIndicators) {
Assert.notNull(buffer, "Buffer required");
final Set<MethodTarget> result = new HashSet<MethodTarget>();
// The reflection could certainly be optimised, but it's good enough for
// now (and cached reflection
// is unlikely to be noticeable to a human being using the CLI)
for (final CommandMarker command : this.commands) {
for (final Method method : command.getClass().getMethods()) {
final CliCommand cmd = method.getAnnotation(CliCommand.class);
if (cmd != null) {
// We have a @CliCommand.
if (checkAvailabilityIndicators) {
// Decide if this @CliCommand is available at this
// moment
Boolean available = null;
for (final String value : cmd.value()) {
final MethodTarget mt = getAvailabilityIndicator(value);
if (mt != null) {
Assert.isNull(available,
"More than one availability indicator is defined for '"
+ method.toGenericString()
+ "'");
try {
available = (Boolean) mt.getMethod()
.invoke(mt.getTarget());
// We should "break" here, but we loop over
// all to ensure no conflicting availability
// indicators are defined
} catch (final Exception e) {
available = false;
}
}
}
// Skip this @CliCommand if it's not available
if ((available != null) && !available) {
continue;
}
}
for (final String value : cmd.value()) {
final String remainingBuffer = isMatch(buffer, value,
strictMatching);
if (remainingBuffer != null) {
result.add(new MethodTarget(method, command,
remainingBuffer, value));
}
}
}
}
}
return result;
}
public MethodTarget getAvailabilityIndicator(final String command) {
return this.availabilityIndicators.get(command);
}
// ------------------------------------------------------------------------
// BeanPostProcessor
// ------------------------------------------------------------------------
/**
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object,
* java.lang.String)
*/
@Override
public Object postProcessBeforeInitialization(final Object bean,
final String beanName) throws BeansException {
this.log.trace(
"Testing if bean [name = {} | bean = {}] implements [{}] ...",
new Object[] { beanName, bean, CommandMarker.class.getName() });
if (bean instanceof CommandMarker) {
this.log.info(
"Bean [name = {} | bean = {}] implements [{}] - will inject be added to set of known commands",
new Object[] { beanName, bean,
CommandMarker.class.getName() });
add(CommandMarker.class.cast(bean));
} else {
this.log.trace(
"Bean [name = {} | bean = {}] does NOT implement [{}] - SKIPPING",
new Object[] { beanName, bean,
CommandMarker.class.getName() });
}
return bean;
}
/**
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object,
* java.lang.String)
*/
@Override
public Object postProcessAfterInitialization(final Object bean,
final String beanName) throws BeansException {
return bean;
}
// ------------------------------------------------------------------------
// Internal
// ------------------------------------------------------------------------
private void add(final CommandMarker command) {
this.commands.add(command);
for (final Method method : command.getClass().getMethods()) {
final CliAvailabilityIndicator availability = method
.getAnnotation(CliAvailabilityIndicator.class);
if (availability != null) {
Assert.isTrue(method.getParameterTypes().length == 0,
"CliAvailabilityIndicator is only legal for 0 parameter methods ("
+ method.toGenericString() + ")");
Assert.isTrue(method.getReturnType().equals(Boolean.TYPE),
"CliAvailabilityIndicator is only legal for primitive boolean return types ("
+ method.toGenericString() + ")");
for (final String cmd : availability.value()) {
Assert.isTrue(
!this.availabilityIndicators.containsKey(cmd),
"Cannot specify an availability indicator for '"
+ cmd + "' more than once");
this.availabilityIndicators.put(cmd, new MethodTarget(
method, command));
}
}
}
}
private String isMatch(final String buffer, final String command,
final boolean strictMatching) {
if ("".equals(buffer.trim())) {
return "";
}
final String[] commandWords = StringUtils.delimitedListToStringArray(
command, " ");
int lastCommandWordUsed = 0;
Assert.notEmpty(commandWords, "Command required");
String bufferToReturn = null;
String lastWord = null;
next_buffer_loop: for (int bufferIndex = 0; bufferIndex < buffer
.length(); bufferIndex++) {
final String bufferSoFarIncludingThis = buffer.substring(0,
bufferIndex + 1);
final String bufferRemaining = buffer.substring(bufferIndex + 1);
final int bufferLastIndexOfWord = bufferSoFarIncludingThis
.lastIndexOf(" ");
String wordSoFarIncludingThis = bufferSoFarIncludingThis;
if (bufferLastIndexOfWord != -1) {
wordSoFarIncludingThis = bufferSoFarIncludingThis
.substring(bufferLastIndexOfWord);
}
if (wordSoFarIncludingThis.equals(" ")
|| (bufferIndex == buffer.length() - 1)) {
if ((bufferIndex == buffer.length() - 1)
&& !"".equals(wordSoFarIncludingThis.trim())) {
lastWord = wordSoFarIncludingThis.trim();
}
// At end of word or buffer. Let's see if a word matched or not
for (int candidate = lastCommandWordUsed; candidate < commandWords.length; candidate++) {
if ((lastWord != null) && (lastWord.length() > 0)
&& commandWords[candidate].startsWith(lastWord)) {
if (bufferToReturn == null) {
// This is the first match, so ensure the intended
// match really represents the start of a command
// and not a later word within it
if ((lastCommandWordUsed == 0) && (candidate > 0)) {
// This is not a valid match
break next_buffer_loop;
}
}
if (bufferToReturn != null) {
// We already matched something earlier, so ensure
// we didn't skip any word
if (candidate != lastCommandWordUsed + 1) {
// User has skipped a word
bufferToReturn = null;
break next_buffer_loop;
}
}
bufferToReturn = bufferRemaining;
lastCommandWordUsed = candidate;
if (candidate + 1 == commandWords.length) {
// This was a match for the final word in the
// command, so abort
break next_buffer_loop;
}
// There are more words left to potentially match, so
// continue
continue next_buffer_loop;
}
}
// This word is unrecognised as part of a command, so abort
bufferToReturn = null;
break next_buffer_loop;
}
lastWord = wordSoFarIncludingThis.trim();
}
// We only consider it a match if ALL words were actually used
if (bufferToReturn != null) {
if (!strictMatching
|| (lastCommandWordUsed + 1 == commandWords.length)) {
return bufferToReturn;
}
}
return null; // Not a match
}
}