| /* |
| * 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; |
| } |
| } |
| } |