blob: ee14dd4af2ace7aa6fdf6f0bb5aded1bcc875e46 [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.
*/
/*
* Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*/
package org.apache.karaf.shell.console.completer;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.felix.gogo.commands.Action;
import org.apache.felix.gogo.commands.Argument;
import org.apache.felix.gogo.commands.CompleterValues;
import org.apache.felix.gogo.commands.Option;
import org.apache.felix.gogo.commands.basic.AbstractCommand;
import org.apache.felix.gogo.commands.basic.DefaultActionPreparator;
import org.apache.felix.service.command.CommandSession;
import org.apache.karaf.shell.console.CompletableFunction;
import org.apache.karaf.shell.console.Completer;
import org.apache.karaf.shell.console.NameScoping;
import org.apache.karaf.shell.console.jline.CommandSessionHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ArgumentCompleter implements Completer {
private static final Logger LOGGER = LoggerFactory.getLogger(ArgumentCompleter.class);
public static final String ARGUMENTS_LIST = "ARGUMENTS_LIST";
final Completer commandCompleter;
final Completer optionsCompleter;
final List<Completer> argsCompleters;
final Map<String, Completer> optionalCompleters;
final AbstractCommand function;
final Map<Option, Field> fields = new HashMap<Option, Field>();
final Map<String, Option> options = new HashMap<String, Option>();
final Map<Integer, Field> arguments = new HashMap<Integer, Field>();
boolean strict = true;
public ArgumentCompleter(CommandSession session, AbstractCommand function, String command) {
this.function = function;
// Command name completer
commandCompleter = new StringsCompleter(getNames(session, command));
// Build options completer
for (Class type = function.getActionClass(); type != null; type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
Option option = field.getAnnotation(Option.class);
if (option != null) {
fields.put(option, field);
options.put(option.name(), option);
String[] aliases = option.aliases();
if (aliases != null) {
for (String alias : aliases) {
options.put(alias, option);
}
}
}
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
Integer key = argument.index();
if (arguments.containsKey(key)) {
LOGGER.warn("Duplicate @Argument annotations on class " + type.getName() + " for index: " + key + " see: " + field);
} else {
arguments.put(key, field);
}
}
}
}
options.put(DefaultActionPreparator.HELP.name(), DefaultActionPreparator.HELP);
optionsCompleter = new StringsCompleter(options.keySet());
// Build arguments completers
argsCompleters = new ArrayList<Completer>();
optionalCompleters = new HashMap<String, Completer>();
if (function instanceof CompletableFunction) {
try {
Map completers = ((CompletableFunction) function).getOptionalCompleters();
if (completers != null) {
optionalCompleters.putAll(completers);
}
} catch (AbstractMethodError err) {
// Allow old commands to not break the completers
}
List<Completer> fcl = ((CompletableFunction) function).getCompleters();
if (fcl != null) {
for (Completer c : fcl) {
argsCompleters.add(c == null ? NullCompleter.INSTANCE : c);
}
} else {
argsCompleters.add(NullCompleter.INSTANCE);
}
} else {
final Map<Integer, Method> methods = new HashMap<Integer, Method>();
for (Class type = function.getActionClass(); type != null; type = type.getSuperclass()) {
for (Method method : type.getDeclaredMethods()) {
CompleterValues completerMethod = method.getAnnotation(CompleterValues.class);
if (completerMethod != null) {
int index = completerMethod.index();
Integer key = index;
if (index >= arguments.size() || index < 0) {
LOGGER.warn("Index out of range on @CompleterValues on class " + type.getName() + " for index: " + key + " see: " + method);
}
if (methods.containsKey(key)) {
LOGGER.warn("Duplicate @CompleterMethod annotations on class " + type.getName() + " for index: " + key + " see: " + method);
} else {
methods.put(key, method);
}
}
}
}
for (int i = 0, size = arguments.size(); i < size; i++) {
Completer argCompleter = NullCompleter.INSTANCE;
Method method = methods.get(i);
if (method != null) {
// lets invoke the method
Action action = function.createNewAction();
try {
Object value = method.invoke(action);
if (value instanceof String[]) {
argCompleter = new StringsCompleter((String[]) value);
} else if (value instanceof Collection) {
argCompleter = new StringsCompleter((Collection<String>) value);
} else {
LOGGER.warn("Could not use value " + value + " as set of completions!");
}
} catch (IllegalAccessException e) {
LOGGER.warn("Could not invoke @CompleterMethod on " + function + ". " + e, e);
} catch (InvocationTargetException e) {
Throwable target = e.getTargetException();
if (target == null) {
target = e;
}
LOGGER.warn("Could not invoke @CompleterMethod on " + function + ". " + target, target);
} finally {
try {
function.releaseAction(action);
} catch (Exception e) {
LOGGER.warn("Failed to release action: " + action + ". " + e, e);
}
}
} else {
Field field = arguments.get(i);
Class<?> type = field.getType();
if (type.isAssignableFrom(File.class)) {
argCompleter = new FileCompleter(session);
} else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) {
argCompleter = new StringsCompleter(new String[] {"false", "true"}, false);
} else if (type.isAssignableFrom(Enum.class)) {
Set<String> values = new HashSet<String>();
for (Object o : EnumSet.allOf((Class<Enum>) type)) {
values.add(o.toString());
}
argCompleter = new StringsCompleter(values, false);
} else {
// TODO any other completers we can add?
}
}
argsCompleters.add(argCompleter);
}
}
}
private String[] getNames(CommandSession session, String scopedCommand) {
String command = NameScoping.getCommandNameWithoutGlobalPrefix(session, scopedCommand);
String[] s = command.split(":");
if (s.length == 1) {
return s;
} else {
return new String[] { command, s[1] };
}
}
/**
* If true, a completion at argument index N will only succeed
* if all the completions from 0-(N-1) also succeed.
*/
public void setStrict(final boolean strict) {
this.strict = strict;
}
/**
* Returns whether a completion at argument index N will succees
* if all the completions from arguments 0-(N-1) also succeed.
*/
public boolean getStrict() {
return this.strict;
}
public int complete(final String buffer, final int cursor,
final List<String> candidates) {
ArgumentList list = delimit(buffer, cursor);
int argpos = list.getArgumentPosition();
int argIndex = list.getCursorArgumentIndex();
//Store the argument list so that it can be used by completers.
CommandSession commandSession = CommandSessionHolder.getSession();
if(commandSession != null) {
commandSession.put(ARGUMENTS_LIST,list);
}
Completer comp = null;
String[] args = list.getArguments();
int index = 0;
// First argument is command name
if (index < argIndex) {
// Verify command name
if (!verifyCompleter(commandCompleter, args[index])) {
return -1;
}
index++;
} else {
comp = commandCompleter;
}
// Now, check options
if (comp == null) {
while (index < argIndex && args[index].startsWith("-")) {
if (!verifyCompleter(optionsCompleter, args[index])) {
return -1;
}
Option option = options.get(args[index]);
if (option == null) {
return -1;
}
Field field = fields.get(option);
if (field != null && field.getType() != boolean.class && field.getType() != Boolean.class) {
if (++index == argIndex) {
comp = NullCompleter.INSTANCE;
}
}
index++;
}
if (comp == null && index >= argIndex && index < args.length && args[index].startsWith("-")) {
comp = optionsCompleter;
}
}
//Now check for if last Option has a completer
int lastAgurmentIndex = argIndex - 1;
if (lastAgurmentIndex >= 1) {
Option lastOption = options.get(args[lastAgurmentIndex]);
if (lastOption != null) {
Field lastField = fields.get(lastOption);
if (lastField != null && lastField.getType() != boolean.class && lastField.getType() != Boolean.class) {
Option option = lastField.getAnnotation(Option.class);
if (option != null) {
Completer optionValueCompleter = null;
String name = option.name();
if (name != null) {
optionValueCompleter = optionalCompleters.get(name);
if (optionValueCompleter == null) {
String[] aliases = option.aliases();
if (aliases.length > 0) {
for (int i = 0; i < aliases.length && optionValueCompleter == null; i++) {
optionValueCompleter = optionalCompleters.get(option.aliases()[i]);
}
}
}
}
if(optionValueCompleter != null) {
comp = optionValueCompleter;
}
}
}
}
}
// Check arguments
if (comp == null) {
int indexArg = 0;
while (index < argIndex) {
Completer sub = argsCompleters.get(indexArg >= argsCompleters.size() ? argsCompleters.size() - 1 : indexArg);
if (!verifyCompleter(sub, args[index])) {
return -1;
}
index++;
indexArg++;
}
comp = argsCompleters.get(indexArg >= argsCompleters.size() ? argsCompleters.size() - 1 : indexArg);
}
int ret = comp.complete(list.getCursorArgument(), argpos, candidates);
if (ret == -1) {
return -1;
}
int pos = ret + (list.getBufferPosition() - argpos);
/**
* Special case: when completing in the middle of a line, and the
* area under the cursor is a delimiter, then trim any delimiters
* from the candidates, since we do not need to have an extra
* delimiter.
*
* E.g., if we have a completion for "foo", and we
* enter "f bar" into the buffer, and move to after the "f"
* and hit TAB, we want "foo bar" instead of "foo bar".
*/
if ((buffer != null) && (cursor != buffer.length()) && isDelimiter(buffer, cursor)) {
for (int i = 0; i < candidates.size(); i++) {
String val = candidates.get(i);
while ((val.length() > 0)
&& isDelimiter(val, val.length() - 1)) {
val = val.substring(0, val.length() - 1);
}
candidates.set(i, val);
}
}
return pos;
}
protected boolean verifyCompleter(Completer completer, String argument) {
List<String> candidates = new ArrayList<String>();
return completer.complete(argument, argument.length(), candidates) != -1 && !candidates.isEmpty();
}
public ArgumentList delimit(final String buffer, final int cursor) {
Parser parser = new Parser(buffer, cursor);
try {
List<List<List<String>>> program = parser.program();
List<String> pipe = program.get(parser.c0).get(parser.c1);
return new ArgumentList(pipe.toArray(new String[pipe.size()]), parser.c2, parser.c3, cursor);
} catch (Throwable t) {
return new ArgumentList(new String[] { buffer }, 0, cursor, cursor);
}
}
/**
* Returns true if the specified character is a whitespace
* parameter. Check to ensure that the character is not
* escaped and returns true from
* {@link #isDelimiterChar}.
*
* @param buffer the complete command buffer
* @param pos the index of the character in the buffer
* @return true if the character should be a delimiter
*/
public boolean isDelimiter(final String buffer, final int pos) {
return !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
}
public boolean isEscaped(final String buffer, final int pos) {
return pos > 0 && buffer.charAt(pos) == '\\' && !isEscaped(buffer, pos - 1);
}
/**
* The character is a delimiter if it is whitespace, and the
* preceeding character is not an escape character.
*/
public boolean isDelimiterChar(String buffer, int pos) {
return Character.isWhitespace(buffer.charAt(pos));
}
/**
* The result of a delimited buffer.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public static class ArgumentList {
private String[] arguments;
private int cursorArgumentIndex;
private int argumentPosition;
private int bufferPosition;
/**
* @param arguments the array of tokens
* @param cursorArgumentIndex the token index of the cursor
* @param argumentPosition the position of the cursor in the
* current token
* @param bufferPosition the position of the cursor in
* the whole buffer
*/
public ArgumentList(String[] arguments, int cursorArgumentIndex,
int argumentPosition, int bufferPosition) {
this.arguments = arguments;
this.cursorArgumentIndex = cursorArgumentIndex;
this.argumentPosition = argumentPosition;
this.bufferPosition = bufferPosition;
}
public void setCursorArgumentIndex(int cursorArgumentIndex) {
this.cursorArgumentIndex = cursorArgumentIndex;
}
public int getCursorArgumentIndex() {
return this.cursorArgumentIndex;
}
public String getCursorArgument() {
if ((cursorArgumentIndex < 0)
|| (cursorArgumentIndex >= arguments.length)) {
return null;
}
return arguments[cursorArgumentIndex];
}
public void setArgumentPosition(int argumentPosition) {
this.argumentPosition = argumentPosition;
}
public int getArgumentPosition() {
return this.argumentPosition;
}
public void setArguments(String[] arguments) {
this.arguments = arguments;
}
public String[] getArguments() {
return this.arguments;
}
public void setBufferPosition(int bufferPosition) {
this.bufferPosition = bufferPosition;
}
public int getBufferPosition() {
return this.bufferPosition;
}
}
}