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);
+    }
+
+}