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/02/18 09:28:30 UTC

[6/6] git commit: [KARAF-2763] Provide a standalone extender to support the new annotations

[KARAF-2763] Provide a standalone extender to support the new annotations

Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/2bd28679
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/2bd28679
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/2bd28679

Branch: refs/heads/master
Commit: 2bd28679e01f3800a2d686fcc214c850f0a3c2c5
Parents: e4f7bc4
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Tue Feb 18 09:24:43 2014 +0100
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Tue Feb 18 09:24:43 2014 +0100

----------------------------------------------------------------------
 shell/console/pom.xml                           |   9 +
 .../shell/inject/impl/InjectionExtender.java    | 100 ++++++
 .../shell/inject/impl/InjectionExtension.java   | 350 +++++++++++++++++++
 .../shell/inject/impl/MultiServiceTracker.java  |  89 +++++
 .../karaf/shell/inject/impl/Satisfiable.java    |  30 ++
 .../shell/inject/impl/SingleServiceTracker.java | 160 +++++++++
 .../OSGI-INF/blueprint/karaf-console.xml        |   7 +
 7 files changed, 745 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/2bd28679/shell/console/pom.xml
----------------------------------------------------------------------
diff --git a/shell/console/pom.xml b/shell/console/pom.xml
index 9a04677..0e39177 100644
--- a/shell/console/pom.xml
+++ b/shell/console/pom.xml
@@ -82,6 +82,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.utils</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.sshd</groupId>
             <artifactId>sshd-core</artifactId>
         </dependency>
@@ -129,6 +134,7 @@
                     <instructions>
                         <Import-Package>
                             !org.apache.felix.gogo.runtime.*,
+                            !org.apache.karaf.shell.inject.impl,
                             org.osgi.service.event;resolution:=optional,
                             org.apache.karaf.branding;resolution:=optional,
                             org.apache.sshd.agent*;resolution:=optional,
@@ -159,6 +165,9 @@
                         <Private-Package>
                         	org.apache.karaf.shell.console.impl*,
                             org.apache.karaf.shell.security.impl*,
+                            org.apache.karaf.shell.inject.impl*,
+                            org.apache.felix.utils.extender,
+                            org.apache.felix.utils.manifest
                         </Private-Package>
                         <Main-Class>
                             org.apache.karaf.shell.console.impl.Main

http://git-wip-us.apache.org/repos/asf/karaf/blob/2bd28679/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtender.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtender.java b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtender.java
new file mode 100644
index 0000000..6177390
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtender.java
@@ -0,0 +1,100 @@
+/*
+ * 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.inject.impl;
+
+import org.apache.felix.utils.extender.AbstractExtender;
+import org.apache.felix.utils.extender.Extension;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bundle extender scanning for command classes.
+ */
+public class InjectionExtender extends AbstractExtender {
+
+    public static final String KARAF_COMMANDS = "Karaf-Injection";
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(InjectionExtender.class);
+
+    //
+    // Adapt BundleActivator to make it blueprint friendly
+    //
+
+    private BundleContext bundleContext;
+
+    public void setBundleContext(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+
+    public void init() throws Exception {
+        start(bundleContext);
+    }
+
+    public void destroy() throws Exception {
+        stop(bundleContext);
+    }
+
+    //
+    // Extender implementation
+    //
+
+    @Override
+    protected Extension doCreateExtension(Bundle bundle) throws Exception {
+        if (bundle.getHeaders().get(KARAF_COMMANDS) != null) {
+            return new InjectionExtension(bundle);
+        }
+        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/2bd28679/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtension.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtension.java b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtension.java
new file mode 100644
index 0000000..fd807a3
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/InjectionExtension.java
@@ -0,0 +1,350 @@
+/*
+ * 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.inject.impl;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.felix.gogo.commands.Action;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.Function;
+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.commands.Command;
+import org.apache.karaf.shell.commands.CommandWithAction;
+import org.apache.karaf.shell.inject.Destroy;
+import org.apache.karaf.shell.inject.Init;
+import org.apache.karaf.shell.inject.Reference;
+import org.apache.karaf.shell.inject.Service;
+import org.apache.karaf.shell.commands.basic.AbstractCommand;
+import org.apache.karaf.shell.console.BundleContextAware;
+import org.apache.karaf.shell.console.CompletableFunction;
+import org.apache.karaf.shell.console.Completer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.wiring.BundleWiring;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Commands extension
+ */
+public class InjectionExtension implements Extension, Satisfiable {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(InjectionExtension.class);
+
+    private final Bundle bundle;
+    private final CountDownLatch started;
+    private final MultiServiceTracker tracker;
+    private final List<Satisfiable> satisfiables = new ArrayList<Satisfiable>();
+
+
+    public InjectionExtension(Bundle bundle) {
+        this.bundle = bundle;
+        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(InjectionExtender.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();
+        } 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;
+        }
+        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");
+            }
+            // Create trackers
+            for (Class<?> cl = clazz; cl != Object.class; cl = cl.getSuperclass()) {
+                for (Field field : cl.getDeclaredFields()) {
+                    if (field.getAnnotation(Reference.class) != null) {
+                        if (field.getType() != BundleContext.class) {
+                            tracker.track(field.getType());
+                        }
+                    }
+                }
+            }
+            satisfiables.add(new AutoRegisterCommand((Class<? extends Action>) clazz));
+        }
+        if (Completer.class.isAssignableFrom(clazz)) {
+            // Create trackers
+            for (Class<?> cl = clazz; cl != Object.class; cl = cl.getSuperclass()) {
+                for (Field field : cl.getDeclaredFields()) {
+                    if (field.getAnnotation(Reference.class) != null) {
+                        if (field.getType() != BundleContext.class) {
+                            tracker.track(field.getType());
+                        }
+                    }
+                }
+            }
+            satisfiables.add(new AutoRegisterService(clazz));
+        }
+    }
+
+    public class AutoRegisterService implements Satisfiable {
+
+        private final Class<?> clazz;
+        private Object service;
+        private ServiceRegistration registration;
+
+        public AutoRegisterService(Class<?> clazz) {
+            this.clazz = clazz;
+        }
+
+        @Override
+        public void found() {
+            try {
+                // Create completer
+                service = clazz.newInstance();
+                Set<String> classes = new HashSet<String>();
+                // Inject services
+                for (Class<?> cl = clazz; cl != Object.class; cl = cl.getSuperclass()) {
+                    classes.add(cl.getName());
+                    for (Class c : cl.getInterfaces()) {
+                        classes.add(c.getName());
+                    }
+                    for (Field field : cl.getDeclaredFields()) {
+                        if (field.getAnnotation(Reference.class) != null) {
+                            Object value;
+                            if (field.getType() == BundleContext.class) {
+                                value = InjectionExtension.this.bundle.getBundleContext();
+                            } else {
+                                value = InjectionExtension.this.tracker.getService(field.getType());
+                            }
+                            if (value == null) {
+                                throw new RuntimeException("No OSGi service matching " + field.getType().getName());
+                            }
+                            field.setAccessible(true);
+                            field.set(service, 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(service);
+                    }
+                }
+                Hashtable<String, String> props = new Hashtable<String, String>();
+                registration = bundle.getBundleContext().registerService(classes.toArray(new String[classes.size()]), service, props);
+            } catch (Exception e) {
+                throw new RuntimeException("Unable to creation service " + clazz.getName(), e);
+            }
+        }
+
+        @Override
+        public void updated() {
+            lost();
+            found();
+        }
+
+        @Override
+        public void lost() {
+            if (registration != null) {
+                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);
+                        try {
+                            method.invoke(service);
+                        } catch (Exception e) {
+                            LOGGER.warn("Error destroying service", e);
+                        }
+                    }
+                }
+                registration.unregister();
+                registration = null;
+            }
+        }
+
+    }
+
+    public class AutoRegisterCommand extends AbstractCommand implements Satisfiable, CompletableFunction {
+
+        private final Class<? extends Action> actionClass;
+        private ServiceRegistration registration;
+
+        public AutoRegisterCommand(Class<? extends Action> actionClass) {
+            this.actionClass = actionClass;
+        }
+
+        @Override
+        public void found() {
+            // Register command
+            Command cmd = actionClass.getAnnotation(Command.class);
+            Hashtable<String, String> props = new Hashtable<String, String>();
+            props.put(CommandProcessor.COMMAND_SCOPE, cmd.scope());
+            props.put(CommandProcessor.COMMAND_FUNCTION, cmd.name());
+            String[] classes = {
+                    Function.class.getName(),
+                    CompletableFunction.class.getName(),
+                    CommandWithAction.class.getName(),
+                    AbstractCommand.class.getName()
+            };
+            registration = bundle.getBundleContext().registerService(classes, this, props);
+        }
+
+        @Override
+        public void updated() {
+
+        }
+
+        @Override
+        public void lost() {
+            if (registration != null) {
+                registration.unregister();
+                registration = null;
+            }
+        }
+
+        @Override
+        public Action createNewAction() {
+            try {
+                Action action = actionClass.newInstance();
+                // Inject services
+                for (Class<?> cl = actionClass; cl != Object.class; cl = cl.getSuperclass()) {
+                    for (Field field : cl.getDeclaredFields()) {
+                        if (field.getAnnotation(Reference.class) != null) {
+                            Object value;
+                            if (field.getType() == BundleContext.class) {
+                                value = InjectionExtension.this.bundle.getBundleContext();
+                            } else {
+                                value = InjectionExtension.this.tracker.getService(field.getType());
+                            }
+                            if (value == null) {
+                                throw new RuntimeException("No OSGi service matching " + field.getType().getName());
+                            }
+                            field.setAccessible(true);
+                            field.set(action, value);
+                        }
+                    }
+                }
+                if (action instanceof BundleContextAware) {
+                    ((BundleContextAware) action).setBundleContext(bundle.getBundleContext());
+                }
+                for (Method method : actionClass.getDeclaredMethods()) {
+                    Init ann = method.getAnnotation(Init.class);
+                    if (ann != null && method.getParameterTypes().length == 0 && method.getReturnType() == void.class) {
+                        method.setAccessible(true);
+                        method.invoke(action);
+                    }
+                }
+                return action;
+            } catch (Exception e) {
+                throw new RuntimeException("Unable to creation command action " + actionClass.getName(), e);
+            }
+        }
+
+        @Override
+        public void releaseAction(Action action) throws Exception {
+            for (Method method : actionClass.getDeclaredMethods()) {
+                Destroy ann = method.getAnnotation(Destroy.class);
+                if (ann != null && method.getParameterTypes().length == 0 && method.getReturnType() == void.class) {
+                    method.setAccessible(true);
+                    method.invoke(action);
+                }
+            }
+            super.releaseAction(action);
+        }
+
+        @Override
+        public List<Completer> getCompleters() {
+            return null;
+        }
+
+        @Override
+        public Map<String, Completer> getOptionalCompleters() {
+            return null;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/2bd28679/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/MultiServiceTracker.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/MultiServiceTracker.java b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/MultiServiceTracker.java
new file mode 100644
index 0000000..5394dca
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/MultiServiceTracker.java
@@ -0,0 +1,89 @@
+/*
+ * 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.inject.impl;
+
+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 {
+
+    private final BundleContext bundleContext;
+    private final Satisfiable satisfiable;
+    private final ConcurrentMap<Class, SingleServiceTracker> trackers = new ConcurrentHashMap<Class, SingleServiceTracker>();
+    private final AtomicInteger count = new AtomicInteger();
+
+    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, new Satisfiable() {
+                @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();
+                    }
+                }
+
+            });
+            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();
+        }
+    }
+
+    public void close() {
+        for (SingleServiceTracker tracker : trackers.values()) {
+            tracker.close();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/2bd28679/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/Satisfiable.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/Satisfiable.java b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/Satisfiable.java
new file mode 100644
index 0000000..ed5ceb6
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/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.inject.impl;
+
+/**
+ * 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/2bd28679/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/SingleServiceTracker.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/SingleServiceTracker.java b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/SingleServiceTracker.java
new file mode 100644
index 0000000..9ecec4d
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/inject/impl/SingleServiceTracker.java
@@ -0,0 +1,160 @@
+/*
+ * 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.inject.impl;
+
+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);
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/2bd28679/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
----------------------------------------------------------------------
diff --git a/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml b/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
index bbe0c5b..1abeb1e 100644
--- a/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
+++ b/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
@@ -82,4 +82,11 @@
     </bean>
     <service ref="secureCommandConfigTransformer" interface="org.osgi.service.cm.ConfigurationListener"/>
 
+    <!-- Console commands extender -->
+    <bean id="consoleExtender"
+          class="org.apache.karaf.shell.inject.impl.InjectionExtender"
+          init-method="init" destroy-method="destroy">
+        <property name="bundleContext" ref="blueprintBundleContext"/>
+    </bean>
+
 </blueprint>