You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2014/03/05 16:08:26 UTC
[04/10] [KARAF-2805] Clean console and commands model
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
new file mode 100644
index 0000000..9cd0343
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
@@ -0,0 +1,481 @@
+/*
+ * 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.karaf.shell.impl.action.command;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.converter.DefaultConverter;
+import org.apache.karaf.shell.support.converter.GenericType;
+import org.apache.karaf.shell.support.CommandException;
+import org.apache.karaf.shell.support.NameScoping;
+
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.COLOR_DEFAULT;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.COLOR_RED;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.INTENSITY_BOLD;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.INTENSITY_NORMAL;
+
+public class DefaultActionPreparator {
+
+ public boolean prepare(Action action, Session session, List<Object> params) throws Exception {
+
+ Command command = action.getClass().getAnnotation(Command.class);
+ Map<Option, Field> options = new HashMap<Option, Field>();
+ Map<Argument, Field> arguments = new HashMap<Argument, Field>();
+ List<Argument> orderedArguments = new ArrayList<Argument>();
+
+ for (Class<?> type = action.getClass(); type != null; type = type.getSuperclass()) {
+ for (Field field : type.getDeclaredFields()) {
+ Option option = field.getAnnotation(Option.class);
+ if (option != null) {
+ options.put(option, field);
+ }
+
+ Argument argument = field.getAnnotation(Argument.class);
+ if (argument != null) {
+ argument = replaceDefaultArgument(field, argument);
+ arguments.put(argument, field);
+ int index = argument.index();
+ while (orderedArguments.size() <= index) {
+ orderedArguments.add(null);
+ }
+ if (orderedArguments.get(index) != null) {
+ throw new IllegalArgumentException("Duplicate argument index: " + index + " on Action " + action.getClass().getName());
+ }
+ orderedArguments.set(index, argument);
+ }
+ }
+ }
+ assertIndexesAreCorrect(action.getClass(), orderedArguments);
+
+ String commandErrorSt = COLOR_RED + "Error executing command " + command.scope() + ":" + INTENSITY_BOLD + command.name() + INTENSITY_NORMAL + COLOR_DEFAULT + ": ";
+ for (Iterator<Object> it = params.iterator(); it.hasNext(); ) {
+ Object param = it.next();
+ if (HelpOption.HELP.name().equals(param)) {
+ int termWidth = session.getTerminal() != null ? session.getTerminal().getWidth() : 80;
+ boolean globalScope = NameScoping.isGlobalScope(session, command.scope());
+ printUsage(action, options, arguments, System.out, globalScope, termWidth);
+ return false;
+ }
+ }
+
+ // Populate
+ Map<Option, Object> optionValues = new HashMap<Option, Object>();
+ Map<Argument, Object> argumentValues = new HashMap<Argument, Object>();
+ boolean processOptions = true;
+ int argIndex = 0;
+ for (Iterator<Object> it = params.iterator(); it.hasNext(); ) {
+ Object param = it.next();
+
+ if (processOptions && param instanceof String && ((String) param).startsWith("-")) {
+ boolean isKeyValuePair = ((String) param).indexOf('=') != -1;
+ String name;
+ Object value = null;
+ if (isKeyValuePair) {
+ name = ((String) param).substring(0, ((String) param).indexOf('='));
+ value = ((String) param).substring(((String) param).indexOf('=') + 1);
+ } else {
+ name = (String) param;
+ }
+ Option option = null;
+ for (Option opt : options.keySet()) {
+ if (name.equals(opt.name()) || Arrays.asList(opt.aliases()).contains(name)) {
+ option = opt;
+ break;
+ }
+ }
+ if (option == null) {
+ throw new CommandException(commandErrorSt
+ + "undefined option " + INTENSITY_BOLD + param + INTENSITY_NORMAL + "\n"
+ + "Try <command> --help' for more information.",
+ "Undefined option: " + param);
+ }
+ Field field = options.get(option);
+ if (value == null && (field.getType() == boolean.class || field.getType() == Boolean.class)) {
+ value = Boolean.TRUE;
+ }
+ if (value == null && it.hasNext()) {
+ value = it.next();
+ }
+ if (value == null) {
+ throw new CommandException(commandErrorSt
+ + "missing value for option " + INTENSITY_BOLD + param + INTENSITY_NORMAL,
+ "Missing value for option: " + param
+ );
+ }
+ if (option.multiValued()) {
+ @SuppressWarnings("unchecked")
+ List<Object> l = (List<Object>) optionValues.get(option);
+ if (l == null) {
+ l = new ArrayList<Object>();
+ optionValues.put(option, l);
+ }
+ l.add(value);
+ } else {
+ optionValues.put(option, value);
+ }
+ } else {
+ processOptions = false;
+ if (argIndex >= orderedArguments.size()) {
+ throw new CommandException(commandErrorSt +
+ "too many arguments specified",
+ "Too many arguments specified"
+ );
+ }
+ Argument argument = orderedArguments.get(argIndex);
+ if (!argument.multiValued()) {
+ argIndex++;
+ }
+ if (argument.multiValued()) {
+ @SuppressWarnings("unchecked")
+ List<Object> l = (List<Object>) argumentValues.get(argument);
+ if (l == null) {
+ l = new ArrayList<Object>();
+ argumentValues.put(argument, l);
+ }
+ l.add(param);
+ } else {
+ argumentValues.put(argument, param);
+ }
+ }
+ }
+ // Check required arguments / options
+ for (Option option : options.keySet()) {
+ if (option.required() && optionValues.get(option) == null) {
+ throw new CommandException(commandErrorSt +
+ "option " + INTENSITY_BOLD + option.name() + INTENSITY_NORMAL + " is required",
+ "Option " + option.name() + " is required"
+ );
+ }
+ }
+ for (Argument argument : orderedArguments) {
+ if (argument.required() && argumentValues.get(argument) == null) {
+ throw new CommandException(commandErrorSt +
+ "argument " + INTENSITY_BOLD + argument.name() + INTENSITY_NORMAL + " is required",
+ "Argument " + argument.name() + " is required"
+ );
+ }
+ }
+
+ // Convert and inject values
+ for (Map.Entry<Option, Object> entry : optionValues.entrySet()) {
+ Field field = options.get(entry.getKey());
+ Object value;
+ try {
+ value = convert(action, entry.getValue(), field.getGenericType());
+ } catch (Exception e) {
+ throw new CommandException(commandErrorSt +
+ "unable to convert option " + INTENSITY_BOLD + entry.getKey().name() + INTENSITY_NORMAL + " with value '"
+ + entry.getValue() + "' to type " + new GenericType(field.getGenericType()).toString(),
+ "Unable to convert option " + entry.getKey().name() + " with value '"
+ + entry.getValue() + "' to type " + new GenericType(field.getGenericType()).toString(),
+ e
+ );
+ }
+ field.setAccessible(true);
+ field.set(action, value);
+ }
+ for (Map.Entry<Argument, Object> entry : argumentValues.entrySet()) {
+ Field field = arguments.get(entry.getKey());
+ Object value;
+ try {
+ value = convert(action, entry.getValue(), field.getGenericType());
+ } catch (Exception e) {
+ throw new CommandException(commandErrorSt +
+ "unable to convert argument " + INTENSITY_BOLD + entry.getKey().name() + INTENSITY_NORMAL + " with value '"
+ + entry.getValue() + "' to type " + new GenericType(field.getGenericType()).toString(),
+ "Unable to convert argument " + entry.getKey().name() + " with value '"
+ + entry.getValue() + "' to type " + new GenericType(field.getGenericType()).toString(),
+ e
+ );
+ }
+ field.setAccessible(true);
+ field.set(action, value);
+ }
+ return true;
+ }
+
+ protected Object convert(Action action, Object value, Type toType) throws Exception {
+ if (toType == String.class) {
+ return value != null ? value.toString() : null;
+ }
+ return new DefaultConverter(action.getClass().getClassLoader()).convert(value, toType);
+ }
+
+ private Argument replaceDefaultArgument(Field field, Argument argument) {
+ if (Argument.DEFAULT.equals(argument.name())) {
+ final Argument delegate = argument;
+ final String name = field.getName();
+ argument = new Argument() {
+ public String name() {
+ return name;
+ }
+
+ public String description() {
+ return delegate.description();
+ }
+
+ public boolean required() {
+ return delegate.required();
+ }
+
+ public int index() {
+ return delegate.index();
+ }
+
+ public boolean multiValued() {
+ return delegate.multiValued();
+ }
+
+ public String valueToShowInHelp() {
+ return delegate.valueToShowInHelp();
+ }
+
+ public Class<? extends Annotation> annotationType() {
+ return delegate.annotationType();
+ }
+ };
+ }
+ return argument;
+ }
+
+ private void assertIndexesAreCorrect(Class<? extends Action> actionClass, List<Argument> orderedArguments) {
+ for (int i = 0; i < orderedArguments.size(); i++) {
+ if (orderedArguments.get(i) == null) {
+ throw new IllegalArgumentException("Missing argument for index: " + i + " on Action " + actionClass.getName());
+ }
+ }
+ }
+
+ public void printUsage(Action action, Map<Option, Field> options, Map<Argument, Field> arguments, PrintStream out, boolean globalScope, int termWidth) {
+ Command command = action.getClass().getAnnotation(Command.class);
+ if (command != null) {
+ List<Argument> argumentsSet = new ArrayList<Argument>(arguments.keySet());
+ Collections.sort(argumentsSet, new Comparator<Argument>() {
+ public int compare(Argument o1, Argument o2) {
+ return Integer.valueOf(o1.index()).compareTo(Integer.valueOf(o2.index()));
+ }
+ });
+ Set<Option> optionsSet = new HashSet<Option>(options.keySet());
+ optionsSet.add(HelpOption.HELP);
+ if (command != null && (command.description() != null || command.name() != null)) {
+ out.println(INTENSITY_BOLD + "DESCRIPTION" + INTENSITY_NORMAL);
+ out.print(" ");
+ if (command.name() != null) {
+ if (globalScope) {
+ out.println(INTENSITY_BOLD + command.name() + INTENSITY_NORMAL);
+ } else {
+ out.println(command.scope() + ":" + INTENSITY_BOLD + command.name() + INTENSITY_NORMAL);
+ }
+ out.println();
+ }
+ out.print("\t");
+ out.println(command.description());
+ out.println();
+ }
+ StringBuffer syntax = new StringBuffer();
+ if (command != null) {
+ if (globalScope) {
+ syntax.append(command.name());
+ } else {
+ syntax.append(String.format("%s:%s", command.scope(), command.name()));
+ }
+ }
+ if (options.size() > 0) {
+ syntax.append(" [options]");
+ }
+ if (arguments.size() > 0) {
+ syntax.append(' ');
+ for (Argument argument : argumentsSet) {
+ if (!argument.required()) {
+ syntax.append(String.format("[%s] ", argument.name()));
+ } else {
+ syntax.append(String.format("%s ", argument.name()));
+ }
+ }
+ }
+
+ out.println(INTENSITY_BOLD + "SYNTAX" + INTENSITY_NORMAL);
+ out.print(" ");
+ out.println(syntax.toString());
+ out.println();
+ if (arguments.size() > 0) {
+ out.println(INTENSITY_BOLD + "ARGUMENTS" + INTENSITY_NORMAL);
+ for (Argument argument : argumentsSet) {
+ out.print(" ");
+ out.println(INTENSITY_BOLD + argument.name() + INTENSITY_NORMAL);
+ printFormatted(" ", argument.description(), termWidth, out, true);
+ if (!argument.required()) {
+ if (argument.valueToShowInHelp() != null && argument.valueToShowInHelp().length() != 0) {
+ if (Argument.DEFAULT_STRING.equals(argument.valueToShowInHelp())) {
+ Object o = getDefaultValue(action, arguments.get(argument));
+ String defaultValue = getDefaultValueString(o);
+ if (defaultValue != null) {
+ printDefaultsTo(out, defaultValue);
+ }
+ } else {
+ printDefaultsTo(out, argument.valueToShowInHelp());
+ }
+ }
+ }
+ }
+ out.println();
+ }
+ if (options.size() > 0) {
+ out.println(INTENSITY_BOLD + "OPTIONS" + INTENSITY_NORMAL);
+ for (Option option : optionsSet) {
+ String opt = option.name();
+ for (String alias : option.aliases()) {
+ opt += ", " + alias;
+ }
+ out.print(" ");
+ out.println(INTENSITY_BOLD + opt + INTENSITY_NORMAL);
+ printFormatted(" ", option.description(), termWidth, out, true);
+ if (option.valueToShowInHelp() != null && option.valueToShowInHelp().length() != 0) {
+ if (Option.DEFAULT_STRING.equals(option.valueToShowInHelp())) {
+ Object o = getDefaultValue(action, options.get(option));
+ String defaultValue = getDefaultValueString(o);
+ if (defaultValue != null) {
+ printDefaultsTo(out, defaultValue);
+ }
+ } else {
+ printDefaultsTo(out, option.valueToShowInHelp());
+ }
+ }
+ }
+ out.println();
+ }
+ if (command.detailedDescription().length() > 0) {
+ out.println(INTENSITY_BOLD + "DETAILS" + INTENSITY_NORMAL);
+ String desc = loadDescription(action.getClass(), command.detailedDescription());
+ printFormatted(" ", desc, termWidth, out, true);
+ }
+ }
+ }
+
+ public Object getDefaultValue(Action action, Field field) {
+ try {
+ field.setAccessible(true);
+ return field.get(action);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private String loadDescription(Class<?> clazz, String desc) {
+ if (desc != null && desc.startsWith("classpath:")) {
+ desc = loadClassPathResource(clazz, desc.substring("classpath:".length()));
+ }
+ return desc;
+ }
+
+ public String getDefaultValueString(Object o) {
+ if (o != null
+ && (!(o instanceof Boolean) || ((Boolean) o))
+ && (!(o instanceof Number) || ((Number) o).doubleValue() != 0.0)) {
+ return o.toString();
+ } else {
+ return null;
+ }
+ }
+
+ private void printDefaultsTo(PrintStream out, String value) {
+ out.println(" (defaults to " + value + ")");
+ }
+
+ static void printFormatted(String prefix, String str, int termWidth, PrintStream out, boolean prefixFirstLine) {
+ int pfxLen = prefix.length();
+ int maxwidth = termWidth - pfxLen;
+ Pattern wrap = Pattern.compile("(\\S\\S{" + maxwidth + ",}|.{1," + maxwidth + "})(\\s+|$)");
+ int cur = 0;
+ while (cur >= 0) {
+ int lst = str.indexOf('\n', cur);
+ String s = (lst >= 0) ? str.substring(cur, lst) : str.substring(cur);
+ if (s.length() == 0) {
+ out.println();
+ } else {
+ Matcher m = wrap.matcher(s);
+ while (m.find()) {
+ if (cur > 0 || prefixFirstLine) {
+ out.print(prefix);
+ }
+ out.println(m.group());
+ }
+ }
+ if (lst >= 0) {
+ cur = lst + 1;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private String loadClassPathResource(Class<?> clazz, String path) {
+ InputStream is = clazz.getResourceAsStream(path);
+ if (is == null) {
+ is = clazz.getClassLoader().getResourceAsStream(path);
+ }
+ if (is == null) {
+ return "Unable to load description from " + path;
+ }
+
+ try {
+ Reader r = new InputStreamReader(is);
+ StringWriter sw = new StringWriter();
+ int c;
+ while ((c = r.read()) != -1) {
+ sw.append((char) c);
+ }
+ return sw.toString();
+ } catch (IOException e) {
+ return "Unable to load description from " + path;
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
new file mode 100644
index 0000000..ec0b1a8
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
@@ -0,0 +1,57 @@
+/*
+ * 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.karaf.shell.impl.action.command;
+
+import java.lang.annotation.Annotation;
+
+import org.apache.karaf.shell.api.action.Option;
+
+
+public class HelpOption {
+
+ public static final Option HELP = new Option() {
+ public String name() {
+ return "--help";
+ }
+
+ public String[] aliases() {
+ return new String[]{};
+ }
+
+ public String description() {
+ return "Display this help message";
+ }
+
+ public boolean required() {
+ return false;
+ }
+
+ public boolean multiValued() {
+ return false;
+ }
+
+ public String valueToShowInHelp() {
+ return Option.DEFAULT_STRING;
+ }
+
+ public Class<? extends Annotation> annotationType() {
+ return Option.class;
+ }
+ };
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java
new file mode 100644
index 0000000..611ecab
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java
@@ -0,0 +1,168 @@
+/*
+ * 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.karaf.shell.impl.action.command;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Destroy;
+import org.apache.karaf.shell.api.action.lifecycle.Init;
+import org.apache.karaf.shell.api.action.lifecycle.Manager;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.support.converter.GenericType;
+
+public class ManagerImpl implements Manager {
+
+ private final Registry dependencies;
+ private final Registry registrations;
+ private final Map<Class<?>, Object> instances = new HashMap<Class<?>, Object>();
+ private final boolean allowCustomServices;
+
+ public ManagerImpl(Registry dependencies, Registry registrations) {
+ this(dependencies, registrations, false);
+ }
+
+ public ManagerImpl(Registry dependencies, Registry registrations, boolean allowCustomServices) {
+ this.dependencies = dependencies;
+ this.registrations = registrations;
+ this.allowCustomServices = allowCustomServices;
+ }
+
+ public <T> T instantiate(Class<? extends T> clazz) throws Exception {
+ return instantiate(clazz, dependencies);
+ }
+
+ public <T> T instantiate(Class<? extends T> clazz, Registry registry) throws Exception {
+ if (!allowCustomServices) {
+ Service reg = clazz.getAnnotation(Service.class);
+ if (reg == null) {
+ throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Service");
+ }
+ }
+ T instance = clazz.newInstance();
+ // Inject services
+ for (Class<?> cl = clazz; cl != Object.class; cl = cl.getSuperclass()) {
+ for (Field field : cl.getDeclaredFields()) {
+ if (field.getAnnotation(Reference.class) != null) {
+ GenericType type = new GenericType(field.getGenericType());
+ Object value;
+ if (type.getRawClass() == List.class) {
+ value = registry.getServices(type.getActualTypeArgument(0).getRawClass());
+ if (value == null && registry != this.dependencies) {
+ value = this.dependencies.getServices(type.getActualTypeArgument(0).getRawClass());
+ }
+ } else {
+ value = registry.getService(type.getRawClass());
+ if (value == null && registry != this.dependencies) {
+ value = this.dependencies.getService(type.getRawClass());
+ }
+ }
+ if (!allowCustomServices && value == null) {
+ throw new RuntimeException("No service matching " + field.getType().getName());
+ }
+ field.setAccessible(true);
+ field.set(instance, value);
+ }
+ }
+ }
+ for (Method method : clazz.getDeclaredMethods()) {
+ Init ann = method.getAnnotation(Init.class);
+ if (ann != null && method.getParameterTypes().length == 0 && method.getReturnType() == void.class) {
+ method.setAccessible(true);
+ method.invoke(instance);
+ }
+ }
+ return instance;
+ }
+
+ public void release(Object instance) throws Exception {
+ Class<?> clazz = instance.getClass();
+ if (!allowCustomServices) {
+ Service reg = clazz.getAnnotation(Service.class);
+ if (reg == null) {
+ throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Service");
+ }
+ }
+ for (Method method : clazz.getDeclaredMethods()) {
+ Destroy ann = method.getAnnotation(Destroy.class);
+ if (ann != null && method.getParameterTypes().length == 0 && method.getReturnType() == void.class) {
+ method.setAccessible(true);
+ method.invoke(instance);
+ }
+ }
+ }
+
+ @Override
+ public void register(Class<?> clazz) {
+ if (!allowCustomServices) {
+ Service reg = clazz.getAnnotation(Service.class);
+ if (reg == null ) {
+ throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Service");
+ }
+ }
+ if (Action.class.isAssignableFrom(clazz)) {
+ final Command cmd = clazz.getAnnotation(Command.class);
+ if (cmd == null) {
+ throw new IllegalArgumentException("Command " + clazz.getName() + " is not annotated with @Command");
+ }
+ Object command = new ActionCommand(this, (Class<? extends Action>) clazz);
+ registrations.register(command);
+ }
+ if (allowCustomServices || Completer.class.isAssignableFrom(clazz)) {
+ try {
+ // Create completer
+ Object completer = instantiate(clazz);
+ synchronized (instances) {
+ instances.put(clazz, completer);
+ }
+ registrations.register(completer);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public void unregister(Class<?> clazz) {
+ Object object;
+ synchronized (instances) {
+ object = instances.remove(clazz);
+ }
+ if (object != null) {
+ registrations.unregister(object);
+ if (object instanceof Completer) {
+ try {
+ release(object);
+ } catch (Exception e) {
+ // TODO: log exception
+ }
+ }
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtender.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtender.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtender.java
new file mode 100644
index 0000000..25bcae7
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtender.java
@@ -0,0 +1,94 @@
+/*
+ * 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.karaf.shell.impl.action.osgi;
+
+import org.apache.felix.utils.extender.AbstractExtender;
+import org.apache.felix.utils.extender.Extension;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.impl.action.command.ManagerImpl;
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bundle extender scanning for command classes.
+ */
+public class CommandExtender extends AbstractExtender {
+
+ public static final String KARAF_COMMANDS = "Karaf-Commands";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CommandExtender.class);
+
+ //
+ // Adapt BundleActivator to make it blueprint friendly
+ //
+
+ private Registry registry;
+
+ public CommandExtender(Registry registry) {
+ this.registry = registry;
+ this.registry.register(new ManagerImpl(this.registry, this.registry));
+ }
+
+ //
+ // Extender implementation
+ //
+
+ @Override
+ protected Extension doCreateExtension(Bundle bundle) throws Exception {
+ if (bundle.getHeaders().get(KARAF_COMMANDS) != null) {
+ return new CommandExtension(bundle, registry);
+ }
+ return null;
+ }
+
+ @Override
+ protected void debug(Bundle bundle, String msg) {
+ StringBuilder buf = new StringBuilder();
+ if ( bundle != null )
+ {
+ buf.append( bundle.getSymbolicName() );
+ buf.append( " (" );
+ buf.append( bundle.getBundleId() );
+ buf.append( "): " );
+ }
+ buf.append(msg);
+ LOGGER.debug(buf.toString());
+ }
+
+ @Override
+ protected void warn(Bundle bundle, String msg, Throwable t) {
+ StringBuilder buf = new StringBuilder();
+ if ( bundle != null )
+ {
+ buf.append( bundle.getSymbolicName() );
+ buf.append( " (" );
+ buf.append( bundle.getBundleId() );
+ buf.append( "): " );
+ }
+ buf.append(msg);
+ LOGGER.warn(buf.toString(), t);
+ }
+
+ @Override
+ protected void error(String msg, Throwable t) {
+ LOGGER.error(msg, t);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java
new file mode 100644
index 0000000..b1a953b
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java
@@ -0,0 +1,202 @@
+/*
+ * 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.karaf.shell.impl.action.osgi;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.felix.utils.extender.Extension;
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.manifest.Parser;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.History;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.impl.action.command.ManagerImpl;
+import org.apache.karaf.shell.support.converter.GenericType;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleWiring;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Commands extension
+ */
+public class CommandExtension implements Extension, Satisfiable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CommandExtension.class);
+
+ private final Bundle bundle;
+ private final ManagerImpl manager;
+ private final Registry registry;
+ private final CountDownLatch started;
+ private final MultiServiceTracker tracker;
+ private final List<Satisfiable> satisfiables = new ArrayList<Satisfiable>();
+
+
+ public CommandExtension(Bundle bundle, Registry registry) {
+ this.bundle = bundle;
+ this.registry = new RegistryImpl(registry);
+ this.registry.register(bundle.getBundleContext());
+ this.manager = new ManagerImpl(this.registry, registry);
+ this.registry.register(this.manager);
+ this.started = new CountDownLatch(1);
+ this.tracker = new MultiServiceTracker(bundle.getBundleContext(), this);
+ }
+
+ @Override
+ public void found() {
+ for (Satisfiable s : satisfiables) {
+ s.found();
+ }
+ }
+
+ @Override
+ public void updated() {
+ for (Satisfiable s : satisfiables) {
+ s.updated();
+ }
+ }
+
+ @Override
+ public void lost() {
+ for (Satisfiable s : satisfiables) {
+ s.lost();
+ }
+ }
+
+ public void start() throws Exception {
+ try {
+ String header = bundle.getHeaders().get(CommandExtender.KARAF_COMMANDS);
+ Clause[] clauses = Parser.parseHeader(header);
+ BundleWiring wiring = bundle.adapt(BundleWiring.class);
+ for (Clause clause : clauses) {
+ String name = clause.getName();
+ int options = BundleWiring.LISTRESOURCES_LOCAL;
+ name = name.replace('.', '/');
+ if (name.endsWith("*")) {
+ options |= BundleWiring.LISTRESOURCES_RECURSE;
+ name = name.substring(0, name.length() - 1);
+ }
+ if (!name.startsWith("/")) {
+ name = "/" + name;
+ }
+ if (name.endsWith("/")) {
+ name = name.substring(0, name.length() - 1);
+ }
+ Collection<String> classes = wiring.listResources(name, "*.class", options);
+ for (String className : classes) {
+ className = className.replace('/', '.').replace(".class", "");
+ inspectClass(bundle.loadClass(className));
+ }
+ }
+ tracker.open();
+ if (!tracker.isSatisfied()) {
+ LOGGER.info("Command registration delayed. Missing dependencies: " + tracker.getMissingServices());
+ }
+ } finally {
+ started.countDown();
+ }
+ }
+
+ public void destroy() {
+ try {
+ started.await(5000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ LOGGER.warn("The wait for bundle being started before destruction has been interrupted.", e);
+ }
+ tracker.close();
+ }
+
+ private void inspectClass(final Class<?> clazz) throws Exception {
+ Service reg = clazz.getAnnotation(Service.class);
+ if (reg == null) {
+ return;
+ }
+ // Create trackers
+ for (Class<?> cl = clazz; cl != Object.class; cl = cl.getSuperclass()) {
+ for (Field field : cl.getDeclaredFields()) {
+ if (field.getAnnotation(Reference.class) != null) {
+ GenericType type = new GenericType(field.getType());
+ Class clazzRef = type.getRawClass() == List.class ? type.getActualTypeArgument(0).getRawClass() : type.getRawClass();
+ if (clazzRef != BundleContext.class
+ && clazzRef != Session.class
+ && clazzRef != Terminal.class
+ && clazzRef != History.class
+ && clazzRef != Registry.class
+ && clazzRef != SessionFactory.class
+ && !registry.hasService(clazzRef)) {
+ track(clazzRef);
+ }
+ }
+ }
+ }
+ satisfiables.add(new AutoRegister(clazz));
+ }
+
+ protected void track(final Class clazzRef) {
+ tracker.track(clazzRef);
+ registry.register(new Callable() {
+ @Override
+ public Object call() throws Exception {
+ return tracker.getService(clazzRef);
+ }
+ }, clazzRef);
+ }
+
+ public class AutoRegister implements Satisfiable {
+
+ private final Class<?> clazz;
+
+ public AutoRegister(Class<?> clazz) {
+ this.clazz = clazz;
+ }
+
+ @Override
+ public void found() {
+ try {
+ manager.register(clazz);
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to create service " + clazz.getName(), e);
+ }
+ }
+
+ @Override
+ public void updated() {
+ lost();
+ found();
+ }
+
+ @Override
+ public void lost() {
+ manager.unregister(clazz);
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/MultiServiceTracker.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/MultiServiceTracker.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/MultiServiceTracker.java
new file mode 100644
index 0000000..a762957
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/MultiServiceTracker.java
@@ -0,0 +1,106 @@
+/*
+ * 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.karaf.shell.impl.action.osgi;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.osgi.framework.BundleContext;
+
+/**
+ * Track multiple services by their type
+ */
+public class MultiServiceTracker implements Satisfiable {
+
+ private final BundleContext bundleContext;
+ private final Satisfiable satisfiable;
+ private final ConcurrentMap<Class, SingleServiceTracker> trackers = new ConcurrentHashMap<Class, SingleServiceTracker>();
+ private final AtomicInteger count = new AtomicInteger(-1);
+
+ public MultiServiceTracker(BundleContext bundleContext, Satisfiable satisfiable) {
+ this.bundleContext = bundleContext;
+ this.satisfiable = satisfiable;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void track(Class service) {
+ if (trackers.get(service) == null) {
+ SingleServiceTracker tracker = new SingleServiceTracker(bundleContext, service, this);
+ trackers.putIfAbsent(service, tracker);
+ }
+ }
+
+ public <T> T getService(Class<T> clazz) {
+ SingleServiceTracker tracker = trackers.get(clazz);
+ return tracker != null ? clazz.cast(tracker.getService()) : null;
+ }
+
+ public void open() {
+ for (SingleServiceTracker tracker : trackers.values()) {
+ tracker.open();
+ }
+ found();
+ }
+
+ public void close() {
+ lost();
+ for (SingleServiceTracker tracker : trackers.values()) {
+ tracker.close();
+ }
+ }
+
+ public boolean isSatisfied() {
+ return count.get() == trackers.size();
+ }
+
+ public List<String> getMissingServices() {
+ List<String> missing = new ArrayList<String>();
+ for (SingleServiceTracker tracker : trackers.values()) {
+ if (!tracker.isSatisfied()) {
+ missing.add(tracker.getClassName());
+ }
+ }
+ return missing;
+ }
+
+ @Override
+ public void found() {
+ if (count.incrementAndGet() == trackers.size()) {
+ satisfiable.found();
+ }
+ }
+
+ @Override
+ public void updated() {
+ if (count.get() == trackers.size()) {
+ satisfiable.updated();
+ }
+ }
+
+ @Override
+ public void lost() {
+ if (count.getAndDecrement() == trackers.size()) {
+ satisfiable.lost();
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/RegistryImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/RegistryImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/RegistryImpl.java
new file mode 100644
index 0000000..38b56ed
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/RegistryImpl.java
@@ -0,0 +1,159 @@
+/*
+ * 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.karaf.shell.impl.action.osgi;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Registry;
+
+public class RegistryImpl implements Registry {
+
+ private final Registry parent;
+ private final Map<Object, Object> services = new LinkedHashMap<Object, Object>();
+
+ public RegistryImpl(Registry parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public List<Command> getCommands() {
+ return getServices(Command.class);
+ }
+
+ @Override
+ public <T> void register(Callable<T> factory, Class<T> clazz) {
+ synchronized (services) {
+ services.put(factory, new Factory<T>(clazz, factory));
+ }
+ }
+
+ @Override
+ public void register(Object service) {
+ synchronized (services) {
+ services.put(service, service);
+ }
+ }
+
+ @Override
+ public void unregister(Object service) {
+ synchronized (services) {
+ services.remove(service);
+ }
+ }
+
+ @Override
+ public <T> T getService(Class<T> clazz) {
+ synchronized (services) {
+ for (Object service : services.values()) {
+ if (service instanceof Factory) {
+ if (clazz.isAssignableFrom(((Factory) service).clazz)) {
+ if (isVisible(service)) {
+ try {
+ return clazz.cast(((Factory) service).callable.call());
+ } catch (Exception e) {
+ // TODO: log exception
+ }
+ }
+ }
+ } else if (clazz.isInstance(service)) {
+ if (isVisible(service)) {
+ return clazz.cast(service);
+ }
+ }
+ }
+ }
+ if (parent != null) {
+ return parent.getService(clazz);
+ }
+ return null;
+ }
+
+ @Override
+ public <T> List<T> getServices(Class<T> clazz) {
+ List<T> list = new ArrayList<T>();
+ synchronized (services) {
+ for (Object service : services.values()) {
+ if (service instanceof Factory) {
+ if (clazz.isAssignableFrom(((Factory) service).clazz)) {
+ if (isVisible(service)) {
+ try {
+ list.add(clazz.cast(((Factory) service).callable.call()));
+ } catch (Exception e) {
+ // TODO: log exception
+ }
+ }
+ }
+ } else if (clazz.isInstance(service)) {
+ if (isVisible(service)) {
+ list.add(clazz.cast(service));
+ }
+ }
+ }
+ }
+ if (parent != null) {
+ list.addAll(parent.getServices(clazz));
+ }
+ return list;
+ }
+
+ @Override
+ public boolean hasService(Class<?> clazz) {
+ synchronized (services) {
+ for (Object service : services.values()) {
+ if (service instanceof Factory) {
+ if (clazz.isAssignableFrom(((Factory) service).clazz)) {
+ if (isVisible(service)) {
+ return true;
+ }
+ }
+ } else if (clazz.isInstance(service)) {
+ if (isVisible(service)) {
+ return true;
+ }
+ }
+ }
+ }
+ if (parent != null) {
+ return parent.hasService(clazz);
+ }
+ return false;
+ }
+
+ protected boolean isVisible(Object service) {
+ return true;
+ }
+
+ static class Factory<T> {
+
+ final Class<T> clazz;
+ final Callable<T> callable;
+
+ Factory(Class<T> clazz, Callable<T> callable) {
+ this.clazz = clazz;
+ this.callable = callable;
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/Satisfiable.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/Satisfiable.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/Satisfiable.java
new file mode 100644
index 0000000..90545aa
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/Satisfiable.java
@@ -0,0 +1,30 @@
+/*
+ * 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.karaf.shell.impl.action.osgi;
+
+/**
+ * Interface to be called with a boolean satisfaction status.
+ */
+public interface Satisfiable {
+
+ void found();
+ void updated();
+ void lost();
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/SingleServiceTracker.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/SingleServiceTracker.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/SingleServiceTracker.java
new file mode 100644
index 0000000..039fe48
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/SingleServiceTracker.java
@@ -0,0 +1,168 @@
+/*
+ * 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.karaf.shell.impl.action.osgi;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Track a single service by its type.
+ *
+ * @param <T>
+ */
+public final class SingleServiceTracker<T> {
+
+ private final BundleContext ctx;
+ private final String className;
+ private final AtomicReference<T> service = new AtomicReference<T>();
+ private final AtomicReference<ServiceReference> ref = new AtomicReference<ServiceReference>();
+ private final AtomicBoolean open = new AtomicBoolean(false);
+ private final Satisfiable serviceListener;
+ private Filter filter;
+
+ private final ServiceListener listener = new ServiceListener() {
+ public void serviceChanged(ServiceEvent event) {
+ if (open.get()) {
+ if (event.getType() == ServiceEvent.UNREGISTERING) {
+ ServiceReference deadRef = event.getServiceReference();
+ if (deadRef.equals(ref.get())) {
+ findMatchingReference(deadRef);
+ }
+ } else if (event.getType() == ServiceEvent.REGISTERED && ref.get() == null) {
+ findMatchingReference(null);
+ }
+ }
+ }
+ };
+
+ public SingleServiceTracker(BundleContext context, Class<T> clazz, Satisfiable sl) {
+ ctx = context;
+ this.className = clazz.getName();
+ serviceListener = sl;
+ }
+
+ public T getService() {
+ return service.get();
+ }
+
+ public ServiceReference getServiceReference() {
+ return ref.get();
+ }
+
+ public void open() {
+ if (open.compareAndSet(false, true)) {
+ try {
+ String filterString = '(' + Constants.OBJECTCLASS + '=' + className + ')';
+ if (filter != null) filterString = "(&" + filterString + filter + ')';
+ ctx.addServiceListener(listener, filterString);
+ findMatchingReference(null);
+ } catch (InvalidSyntaxException e) {
+ // this can never happen. (famous last words :)
+ }
+ }
+ }
+
+ private void findMatchingReference(ServiceReference original) {
+ boolean clear = true;
+ ServiceReference ref = ctx.getServiceReference(className);
+ if (ref != null && (filter == null || filter.match(ref))) {
+ @SuppressWarnings("unchecked")
+ T service = (T) ctx.getService(ref);
+ if (service != null) {
+ clear = false;
+
+ // We do the unget out of the lock so we don't exit this class while holding a lock.
+ if (!!!update(original, ref, service)) {
+ ctx.ungetService(ref);
+ }
+ }
+ } else if (original == null) {
+ clear = false;
+ }
+
+ if (clear) {
+ update(original, null, null);
+ }
+ }
+
+ private boolean update(ServiceReference deadRef, ServiceReference newRef, T service) {
+ boolean result = false;
+ int foundLostReplaced = -1;
+
+ // Make sure we don't try to get a lock on null
+ Object lock;
+
+ // we have to choose our lock.
+ if (newRef != null) lock = newRef;
+ else if (deadRef != null) lock = deadRef;
+ else lock = this;
+
+ // This lock is here to ensure that no two threads can set the ref and service
+ // at the same time.
+ synchronized (lock) {
+ if (open.get()) {
+ result = this.ref.compareAndSet(deadRef, newRef);
+ if (result) {
+ this.service.set(service);
+
+ if (deadRef == null && newRef != null) foundLostReplaced = 0;
+ if (deadRef != null && newRef == null) foundLostReplaced = 1;
+ if (deadRef != null && newRef != null) foundLostReplaced = 2;
+ }
+ }
+ }
+
+ if (serviceListener != null) {
+ if (foundLostReplaced == 0) serviceListener.found();
+ else if (foundLostReplaced == 1) serviceListener.lost();
+ else if (foundLostReplaced == 2) serviceListener.updated();
+ }
+
+ return result;
+ }
+
+ public void close() {
+ if (open.compareAndSet(true, false)) {
+ ctx.removeServiceListener(listener);
+
+ synchronized (this) {
+ ServiceReference deadRef = ref.getAndSet(null);
+ service.set(null);
+ if (deadRef != null) ctx.ungetService(deadRef);
+ }
+ }
+ }
+
+ public boolean isSatisfied() {
+ return service.get() != null;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/Branding.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/Branding.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/Branding.java
new file mode 100644
index 0000000..bae8f2a
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/Branding.java
@@ -0,0 +1,72 @@
+/*
+ * 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.karaf.shell.impl.console;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import org.apache.karaf.shell.api.console.Terminal;
+
+
+public final class Branding {
+
+ private Branding() { }
+
+ public static Properties loadBrandingProperties() {
+ Properties props = new Properties();
+ loadProps(props, "org/apache/karaf/shell/console/branding.properties");
+ loadProps(props, "org/apache/karaf/branding/branding.properties");
+ return props;
+ }
+
+ public static Properties loadBrandingProperties(Terminal terminal) {
+ Properties props = new Properties();
+ if (terminal != null && terminal.getClass().getName().endsWith("SshTerminal")) {
+ //it's a ssh client, so load branding seperately
+ loadProps(props, "org/apache/karaf/shell/console/branding-ssh.properties");
+ } else {
+ loadProps(props, "org/apache/karaf/shell/console/branding.properties");
+ }
+
+ loadProps(props, "org/apache/karaf/branding/branding.properties");
+ return props;
+ }
+
+ protected static void loadProps(Properties props, String resource) {
+ InputStream is = null;
+ try {
+ is = Branding.class.getClassLoader().getResourceAsStream(resource);
+ if (is != null) {
+ props.load(is);
+ }
+ } catch (IOException e) {
+ // ignore
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandNamesCompleter.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandNamesCompleter.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandNamesCompleter.java
new file mode 100644
index 0000000..377bdcc
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandNamesCompleter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.karaf.shell.impl.console;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+
+public class CommandNamesCompleter extends org.apache.karaf.shell.support.completers.CommandNamesCompleter {
+
+ @Override
+ public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+ // TODO: optimize
+ List<Command> list = session.getRegistry().getCommands();
+ Set<String> names = new HashSet<String>();
+ for (Command command : list) {
+ names.add(command.getScope() + ":" + command.getName());
+ names.add(command.getName());
+ }
+ int res = new StringsCompleter(names).complete(session, commandLine, candidates);
+ Collections.sort(candidates);
+ return res;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandWrapper.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandWrapper.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandWrapper.java
new file mode 100644
index 0000000..9488d8f
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandWrapper.java
@@ -0,0 +1,61 @@
+/*
+ * 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.karaf.shell.impl.console;
+
+import java.util.List;
+
+import org.apache.felix.gogo.runtime.Closure;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Function;
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Session;
+
+public class CommandWrapper implements Function {
+
+ private final Command command;
+
+ public CommandWrapper(Command command) {
+ this.command = command;
+ }
+
+ public Command getCommand() {
+ return command;
+ }
+
+ @Override
+ public Object execute(final CommandSession commandSession, List<Object> arguments) throws Exception {
+ // TODO: remove the hack for .session
+ Session session = (Session) commandSession.get(".session");
+ // When need to translate closures to a compatible type for the command
+ for (int i = 0; i < arguments.size(); i++) {
+ Object v = arguments.get(i);
+ if (v instanceof Closure) {
+ final Closure closure = (Closure) v;
+ arguments.set(i, new org.apache.karaf.shell.api.console.Function() {
+ @Override
+ public Object execute(Session session, List<Object> arguments) throws Exception {
+ return closure.execute(commandSession, arguments);
+ }
+ });
+ }
+ }
+ return command.execute(session, arguments);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandsCompleter.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandsCompleter.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandsCompleter.java
new file mode 100644
index 0000000..d73af34
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CommandsCompleter.java
@@ -0,0 +1,256 @@
+/*
+ * 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.karaf.shell.impl.console;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.support.completers.AggregateCompleter;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+
+/**
+ * Overall command line completer.
+ */
+public class CommandsCompleter extends org.apache.karaf.shell.support.completers.CommandsCompleter {
+
+ private final SessionFactory factory;
+ private final Map<String, Completer> globalCompleters = new HashMap<String, Completer>();
+ private final Map<String, Completer> localCompleters = new HashMap<String, Completer>();
+ private final List<Command> commands = new ArrayList<Command>();
+
+ public CommandsCompleter(SessionFactory factory) {
+ this.factory = factory;
+ }
+
+ public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+ Map<String, Completer>[] allCompleters = checkData();
+
+ List<String> scopes = getCurrentScopes(session);
+ sort(allCompleters, scopes);
+
+ String subShell = getCurrentSubShell(session);
+ String completion = getCompletionType(session);
+
+ // SUBSHELL mode
+ if (Session.COMPLETION_MODE_SUBSHELL.equalsIgnoreCase(completion)) {
+ if (subShell.isEmpty()) {
+ subShell = Session.SCOPE_GLOBAL;
+ }
+ List<Completer> completers = new ArrayList<Completer>();
+ for (String name : allCompleters[1].keySet()) {
+ if (name.startsWith(subShell)) {
+ completers.add(allCompleters[1].get(name));
+ }
+ }
+ if (!subShell.equals(Session.SCOPE_GLOBAL)) {
+ completers.add(new StringsCompleter(new String[] { "exit" }));
+ }
+ int res = new AggregateCompleter(completers).complete(session, commandLine, candidates);
+ Collections.sort(candidates);
+ return res;
+ }
+
+ if (Session.COMPLETION_MODE_FIRST.equalsIgnoreCase(completion)) {
+ if (!subShell.isEmpty()) {
+ List<Completer> completers = new ArrayList<Completer>();
+ for (String name : allCompleters[1].keySet()) {
+ if (name.startsWith(subShell)) {
+ completers.add(allCompleters[1].get(name));
+ }
+ }
+ int res = new AggregateCompleter(completers).complete(session, commandLine, candidates);
+ if (!candidates.isEmpty()) {
+ Collections.sort(candidates);
+ return res;
+ }
+ }
+ List<Completer> compl = new ArrayList<Completer>();
+ compl.add(new StringsCompleter(getAliases(session)));
+ compl.addAll(allCompleters[0].values());
+ int res = new AggregateCompleter(compl).complete(session, commandLine, candidates);
+ Collections.sort(candidates);
+ return res;
+ }
+
+ List<Completer> compl = new ArrayList<Completer>();
+ compl.add(new StringsCompleter(getAliases(session)));
+ compl.addAll(allCompleters[0].values());
+ int res = new AggregateCompleter(compl).complete(session, commandLine, candidates);
+ Collections.sort(candidates);
+ return res;
+ }
+
+ protected void sort(Map<String, Completer>[] completers, List<String> scopes) {
+ ScopeComparator comparator = new ScopeComparator(scopes);
+ for (int i = 0; i < completers.length; i++) {
+ Map<String, Completer> map = new TreeMap<String, Completer>(comparator);
+ map.putAll(completers[i]);
+ completers[i] = map;
+ }
+ }
+
+ protected static class ScopeComparator implements Comparator<String> {
+ private final List<String> scopes;
+ public ScopeComparator(List<String> scopes) {
+ this.scopes = scopes;
+ }
+ @Override
+ public int compare(String o1, String o2) {
+ String[] p1 = o1.split(":");
+ String[] p2 = o2.split(":");
+ int p = 0;
+ while (p < p1.length && p < p2.length) {
+ int i1 = scopes.indexOf(p1[p]);
+ int i2 = scopes.indexOf(p2[p]);
+ if (i1 < 0) {
+ if (i2 < 0) {
+ int c = p1[p].compareTo(p2[p]);
+ if (c != 0) {
+ return c;
+ } else {
+ p++;
+ }
+ } else {
+ return +1;
+ }
+ } else if (i2 < 0) {
+ return -1;
+ } else if (i1 < i2) {
+ return -1;
+ } else if (i1 > i2) {
+ return +1;
+ } else {
+ p++;
+ }
+ }
+ return 0;
+ }
+ }
+
+ protected List<String> getCurrentScopes(Session session) {
+ String scopes = (String) session.get(Session.SCOPE);
+ return Arrays.asList(scopes.split(":"));
+ }
+
+ protected String getCurrentSubShell(Session session) {
+ String s = (String) session.get(Session.SUBSHELL);
+ if (s == null) {
+ s = "";
+ }
+ return s;
+ }
+
+ protected String getCompletionType(Session session) {
+ String completion = (String) session.get(Session.COMPLETION_MODE);
+ if (completion == null) {
+ completion = Session.COMPLETION_MODE_GLOBAL;
+ }
+ return completion;
+ }
+
+ protected String stripScope(String name) {
+ int index = name.indexOf(":");
+ return index > 0 ? name.substring(index + 1) : name;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected Map<String, Completer>[] checkData() {
+ // Copy the set to avoid concurrent modification exceptions
+ // TODO: fix that in gogo instead
+ Collection<Command> commands;
+ boolean update;
+ synchronized (this) {
+ commands = factory.getRegistry().getCommands();
+ update = !commands.equals(this.commands);
+ }
+ if (update) {
+ // get command aliases
+ Map<String, Completer> global = new HashMap<String, Completer>();
+ Map<String, Completer> local = new HashMap<String, Completer>();
+
+ // add argument completers for each command
+ for (Command command : commands) {
+ String key = command.getScope() + ":" + command.getName();
+ Completer cg = command.getCompleter(false);
+ Completer cl = command.getCompleter(true);
+ if (cg == null) {
+ if (Session.SCOPE_GLOBAL.equals(command.getScope())) {
+ cg = new StringsCompleter(new String[] { command.getName() });
+ } else {
+ cg = new StringsCompleter(new String[] { key, command.getName() });
+ }
+ }
+ if (cl == null) {
+ cl = new StringsCompleter(new String[] { command.getName() });
+ }
+ global.put(key, cg);
+ local.put(key, cl);
+ }
+
+ synchronized (this) {
+ this.commands.clear();
+ this.globalCompleters.clear();
+ this.localCompleters.clear();
+ this.commands.addAll(commands);
+ this.globalCompleters.putAll(global);
+ this.localCompleters.putAll(local);
+ }
+ }
+ synchronized (this) {
+ return new Map[] {
+ new HashMap<String, Completer>(this.globalCompleters),
+ new HashMap<String, Completer>(this.localCompleters)
+ };
+ }
+ }
+
+ /**
+ * Get the aliases defined in the console session.
+ *
+ * @return the aliases set
+ */
+ @SuppressWarnings("unchecked")
+ private Set<String> getAliases(Session session) {
+ Set<String> vars = ((Set<String>) session.get(null));
+ Set<String> aliases = new HashSet<String>();
+ for (String var : vars) {
+ Object content = session.get(var);
+ if (content != null && "org.apache.felix.gogo.runtime.Closure".equals(content.getClass().getName())) {
+ aliases.add(var);
+ }
+ }
+ return aliases;
+ }
+
+}
+
http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CompleterAsCompletor.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CompleterAsCompletor.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CompleterAsCompletor.java
new file mode 100644
index 0000000..fb8ebfb
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/CompleterAsCompletor.java
@@ -0,0 +1,41 @@
+/*
+ * 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.karaf.shell.impl.console;
+
+import java.util.List;
+
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.impl.console.parsing.CommandLineImpl;
+
+public class CompleterAsCompletor implements jline.console.completer.Completer {
+
+ private final Session session;
+ private final Completer completer;
+
+ public CompleterAsCompletor(Session session, Completer completer) {
+ this.session = session;
+ this.completer = completer;
+ }
+
+ public int complete(String buffer, int cursor, List candidates) {
+ return completer.complete(session, CommandLineImpl.build(buffer, cursor), candidates);
+ }
+
+}