killbill-aplcache

osgi: switch to whiteboard pattern to register HttpServlets Bundles

2/8/2013 10:49:06 PM

Details

diff --git a/api/src/main/java/com/ning/billing/osgi/api/http/ServletRouter.java b/api/src/main/java/com/ning/billing/osgi/api/http/ServletRouter.java
index ef4d3bb..7a77de6 100644
--- a/api/src/main/java/com/ning/billing/osgi/api/http/ServletRouter.java
+++ b/api/src/main/java/com/ning/billing/osgi/api/http/ServletRouter.java
@@ -22,5 +22,7 @@ public interface ServletRouter {
 
     void registerServlet(String pluginName, HttpServlet httpServlet);
 
+    void unregisterServlet(String pluginName);
+
     HttpServlet getServletForPlugin(String pluginName);
 }
diff --git a/api/src/main/java/com/ning/billing/osgi/api/OSGIKillbill.java b/api/src/main/java/com/ning/billing/osgi/api/OSGIKillbill.java
index 4212b9b..b9a7adc 100644
--- a/api/src/main/java/com/ning/billing/osgi/api/OSGIKillbill.java
+++ b/api/src/main/java/com/ning/billing/osgi/api/OSGIKillbill.java
@@ -16,7 +16,6 @@
 
 package com.ning.billing.osgi.api;
 
-import javax.servlet.http.HttpServlet;
 import javax.sql.DataSource;
 
 import com.ning.billing.account.api.AccountUserApi;
@@ -92,12 +91,4 @@ public interface OSGIKillbill {
      * @return the dataSource for the OSGI bundles
      */
     public DataSource getDataSource();
-
-    /**
-     * Register a servlet
-     *
-     * @param pluginName plugin name
-     * @param pluginServlet servlet from the bundle
-     */
-    public void registerServlet(String pluginName, HttpServlet pluginServlet);
 }
diff --git a/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java b/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java
index dbcb4bc..08e2d04 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java
@@ -18,7 +18,6 @@ package com.ning.billing.osgi;
 
 import javax.inject.Inject;
 import javax.inject.Named;
-import javax.servlet.http.HttpServlet;
 import javax.sql.DataSource;
 
 import com.ning.billing.account.api.AccountUserApi;
@@ -232,9 +231,4 @@ public class DefaultOSGIKillbill implements OSGIKillbill {
     public DataSource getDataSource() {
         return dataSource;
     }
-
-    @Override
-    public void registerServlet(final String pluginName, final HttpServlet pluginServlet) {
-        servletRouter.registerServlet(pluginName, pluginServlet);
-    }
 }
diff --git a/osgi/src/main/java/com/ning/billing/osgi/http/DefaultServletRouter.java b/osgi/src/main/java/com/ning/billing/osgi/http/DefaultServletRouter.java
index dea87a9..a4795d5 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/http/DefaultServletRouter.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/http/DefaultServletRouter.java
@@ -16,8 +16,8 @@
 
 package com.ning.billing.osgi.http;
 
-import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.inject.Singleton;
 import javax.servlet.http.HttpServlet;
@@ -27,7 +27,7 @@ import com.ning.billing.osgi.api.http.ServletRouter;
 @Singleton
 public class DefaultServletRouter implements ServletRouter {
 
-    private final Map<String, HttpServlet> pluginServlets = new HashMap<String, HttpServlet>();
+    private final Map<String, HttpServlet> pluginServlets = new ConcurrentHashMap<String, HttpServlet>();
 
     @Override
     public void registerServlet(final String pluginName, final HttpServlet httpServlet) {
@@ -35,6 +35,11 @@ public class DefaultServletRouter implements ServletRouter {
     }
 
     @Override
+    public void unregisterServlet(final String pluginName) {
+        pluginServlets.remove(pluginName);
+    }
+
+    @Override
     public HttpServlet getServletForPlugin(final String pluginName) {
         return pluginServlets.get(pluginName);
     }
diff --git a/osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java b/osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java
index 5b83eeb..8025980 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java
@@ -16,47 +16,96 @@
 
 package com.ning.billing.osgi;
 
-import java.util.List;
-
 import javax.inject.Inject;
+import javax.servlet.http.HttpServlet;
 
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 
 import com.ning.billing.osgi.api.OSGIKillbill;
-import com.ning.billing.payment.plugin.api.PaymentPluginApi;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
+import com.ning.billing.osgi.api.http.ServletRouter;
 
-public class KillbillActivator implements BundleActivator {
+public class KillbillActivator implements BundleActivator, ServiceListener {
 
     private final OSGIKillbill osgiKillbill;
+    private final ServletRouter servletRouter;
 
     private volatile ServiceRegistration osgiKillbillRegistration;
+
+    private BundleContext context = null;
+
     @Inject
-    public KillbillActivator(final OSGIKillbill osgiKillbill) {
+    public KillbillActivator(final OSGIKillbill osgiKillbill,
+                             final ServletRouter servletRouter) {
         this.osgiKillbill = osgiKillbill;
+        this.servletRouter = servletRouter;
     }
 
     @Override
     public void start(final BundleContext context) throws Exception {
+        this.context = context;
+
+        context.addServiceListener(this);
         registerServices(context);
     }
 
+    @Override
+    public void stop(final BundleContext context) throws Exception {
+        this.context = null;
+
+        context.removeServiceListener(this);
+        unregisterServices();
+    }
+
+    @Override
+    public void serviceChanged(final ServiceEvent event) {
+        listenForServlets(event);
+    }
+
+    private void listenForServlets(final ServiceEvent event) {
+        if (event.getType() != ServiceEvent.REGISTERED && event.getType() != ServiceEvent.UNREGISTERING) {
+            // Servlets can only be added or removed, not modified
+            return;
+        }
+        final ServiceReference serviceReference = event.getServiceReference();
+
+        // Make sure we can retrieve the plugin name
+        final String pluginName = (String) serviceReference.getProperty("killbill.pluginName");
+        if (pluginName == null) {
+            return;
+        }
+
+        // Make sure this event is for a servlet
+        HttpServlet httpServlet = null;
+        final String[] objectClass = (String[]) event.getServiceReference().getProperty("objectClass");
+        if (context != null && objectClass != null && objectClass.length > 0 && HttpServlet.class.getName().equals(objectClass[0])) {
+            final Object service = context.getService(serviceReference);
+            httpServlet = (HttpServlet) service;
+        }
+
+        if (httpServlet == null) {
+            return;
+        }
+
+        if (event.getType() == ServiceEvent.REGISTERED) {
+            servletRouter.registerServlet(pluginName, httpServlet);
+        } else if (event.getType() == ServiceEvent.UNREGISTERING) {
+            servletRouter.unregisterServlet(pluginName);
+        }
+    }
+
     private void registerServices(final BundleContext context) {
         osgiKillbillRegistration = context.registerService(OSGIKillbill.class.getName(), osgiKillbill, null);
     }
 
-    @Override
-    public void stop(final BundleContext context) throws Exception {
+    private void unregisterServices() {
         if (osgiKillbillRegistration != null) {
             osgiKillbillRegistration.unregister();
             osgiKillbillRegistration = null;
         }
     }
-
 }
diff --git a/osgi-bundles/jruby/pom.xml b/osgi-bundles/jruby/pom.xml
index 18b9486..7166849 100644
--- a/osgi-bundles/jruby/pom.xml
+++ b/osgi-bundles/jruby/pom.xml
@@ -73,9 +73,10 @@
                 <configuration>
                     <instructions>
                         <Bundle-Activator>com.ning.billing.osgi.bundles.jruby.Activator</Bundle-Activator>
-                        <Import-Package>
-                            *;resolution:=optional,org.osgi.service.log;resolution:=optional,org.jruby;resolution:=optional;version="[1.7,2)",javax.management,javax.crypto,javax.net.ssl,javax.security.auth.x500;resolution:=optional
-                        </Import-Package>
+                        <Export-Package></Export-Package>
+                        <Private-Package>com.ning.billing.osgi.bundles.jruby.*</Private-Package>
+                        <!-- Optional resolution because exported by the Felix system bundle -->
+                        <Import-Package>*;resolution:=optional,javax.management;javax.management.*;javax.crypto;javax.crypto.*;javax.net;javax.net.*;javax.security;javax.security.*;resolution:=optional</Import-Package>
                     </instructions>
                 </configuration>
                 <executions>
diff --git a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/Activator.java b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/Activator.java
index 1d3ac3f..ab65cb6 100644
--- a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/Activator.java
+++ b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/Activator.java
@@ -54,9 +54,9 @@ public class Activator implements BundleActivator {
         // Setup JRuby
         final ScriptingContainer scriptingContainer = setupScriptingContainer(rubyConfig);
         if (PluginType.NOTIFICATION.equals(rubyConfig.getPluginType())) {
-            plugin = new JRubyNotificationPlugin(rubyConfig, scriptingContainer, osgiKillbill, logger);
+            plugin = new JRubyNotificationPlugin(rubyConfig, scriptingContainer, context, logger);
         } else if (PluginType.PAYMENT.equals(rubyConfig.getPluginType())) {
-            plugin = new JRubyPaymentPlugin(rubyConfig, scriptingContainer, osgiKillbill, logger);
+            plugin = new JRubyPaymentPlugin(rubyConfig, scriptingContainer, context, logger);
         }
 
         // Validate and instantiate the plugin
diff --git a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyHttpServlet.java b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyHttpServlet.java
new file mode 100644
index 0000000..7e576cc
--- /dev/null
+++ b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyHttpServlet.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.osgi.bundles.jruby;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.jruby.runtime.builtin.IRubyObject;
+
+public class JRubyHttpServlet extends HttpServlet {
+
+    private final HttpServlet delegate;
+
+    public JRubyHttpServlet(final IRubyObject rubyObject) {
+        delegate = (HttpServlet) rubyObject.toJava(HttpServlet.class);
+    }
+
+    @Override
+    protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        delegate.service(req, resp);
+    }
+}
diff --git a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
index 9ac561a..3887a80 100644
--- a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
+++ b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
@@ -26,7 +26,6 @@ import org.osgi.service.log.LogService;
 
 import com.ning.billing.beatrix.bus.api.ExtBusEvent;
 import com.ning.billing.beatrix.bus.api.ExternalBus;
-import com.ning.billing.osgi.api.OSGIKillbill;
 import com.ning.billing.osgi.api.config.PluginRubyConfig;
 
 import com.google.common.eventbus.Subscribe;
@@ -34,8 +33,8 @@ import com.google.common.eventbus.Subscribe;
 public class JRubyNotificationPlugin extends JRubyPlugin {
 
     public JRubyNotificationPlugin(final PluginRubyConfig config, final ScriptingContainer container,
-                                   final OSGIKillbill osgiKillbill, @Nullable final LogService logger) {
-        super(config, container, osgiKillbill, logger);
+                                   final BundleContext bundleContext, @Nullable final LogService logger) {
+        super(config, container, bundleContext, logger);
     }
 
     @Override
diff --git a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
index 1071a79..623d746 100644
--- a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
+++ b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
@@ -30,7 +30,6 @@ import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.log.LogService;
 
-import com.ning.billing.osgi.api.OSGIKillbill;
 import com.ning.billing.osgi.api.config.PluginRubyConfig;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
 import com.ning.billing.payment.plugin.api.PaymentInfoPlugin;
@@ -45,8 +44,8 @@ public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi 
     private volatile ServiceRegistration<PaymentPluginApi> paymentInfoPluginRegistration;
 
     public JRubyPaymentPlugin(final PluginRubyConfig config, final ScriptingContainer container,
-                              final OSGIKillbill osgiKillbill, @Nullable final LogService logger) {
-        super(config, container, osgiKillbill, logger);
+                              final BundleContext bundleContext, @Nullable final LogService logger) {
+        super(config, container, bundleContext, logger);
     }
 
     @Override
diff --git a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
index 9228928..84f75a9 100644
--- a/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
+++ b/osgi-bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java
@@ -17,6 +17,7 @@
 package com.ning.billing.osgi.bundles.jruby;
 
 import java.util.Arrays;
+import java.util.Hashtable;
 import java.util.Map;
 
 import javax.annotation.Nullable;
@@ -28,9 +29,9 @@ import org.jruby.embed.EvalFailedException;
 import org.jruby.embed.ScriptingContainer;
 import org.jruby.runtime.builtin.IRubyObject;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.log.LogService;
 
-import com.ning.billing.osgi.api.OSGIKillbill;
 import com.ning.billing.osgi.api.config.PluginRubyConfig;
 
 // Bridge between the OSGI bundle and the ruby plugin
@@ -46,7 +47,7 @@ public abstract class JRubyPlugin {
     private static final String ACTIVE = "@active";
 
     protected final LogService logger;
-    protected final OSGIKillbill osgiKillbill;
+    protected final BundleContext bundleContext;
     protected final String pluginGemName;
     protected final String rubyRequire;
     protected final String pluginMainClass;
@@ -55,12 +56,13 @@ public abstract class JRubyPlugin {
 
     protected RubyObject pluginInstance;
 
+    private ServiceRegistration httpServletServiceRegistration = null;
     private String cachedRequireLine = null;
 
     public JRubyPlugin(final PluginRubyConfig config, final ScriptingContainer container,
-                       final OSGIKillbill osgiKillbill, @Nullable final LogService logger) {
+                       final BundleContext bundleContext, @Nullable final LogService logger) {
         this.logger = logger;
-        this.osgiKillbill = osgiKillbill;
+        this.bundleContext = bundleContext;
         this.pluginGemName = config.getPluginName();
         this.rubyRequire = config.getRubyRequire();
         this.pluginMainClass = config.getRubyMainClass();
@@ -92,26 +94,41 @@ public abstract class JRubyPlugin {
 
         // Start the plugin
         pluginInstance = (RubyObject) container.runScriptlet(pluginMainClass + ".new(" + JAVA_APIS + ")");
-
-        // Register the rack handler
-        final IRubyObject rackHandler = pluginInstance.callMethod("rack_handler");
-        if (!rackHandler.isNil()) {
-            osgiKillbill.registerServlet(pluginGemName, (HttpServlet) rackHandler.toJava(HttpServlet.class));
-        }
     }
 
     public void startPlugin(final BundleContext context) {
         checkPluginIsStopped();
         pluginInstance.callMethod("start_plugin");
         checkPluginIsRunning();
+        registerHttpServlet();
     }
 
     public void stopPlugin(final BundleContext context) {
         checkPluginIsRunning();
+        unregisterHttpServlet();
         pluginInstance.callMethod("stop_plugin");
         checkPluginIsStopped();
     }
 
+    private void registerHttpServlet() {
+        // Register the rack handler
+        final IRubyObject rackHandler = pluginInstance.callMethod("rack_handler");
+        if (!rackHandler.isNil()) {
+            log(LogService.LOG_INFO, String.format("Using %s as rack handler", rackHandler.getMetaClass()));
+
+            final JRubyHttpServlet jRubyHttpServlet = new JRubyHttpServlet(rackHandler);
+            final Hashtable<String, String> properties = new Hashtable<>();
+            properties.put("killbill.pluginName", pluginGemName);
+            httpServletServiceRegistration = bundleContext.registerService(HttpServlet.class.getName(), jRubyHttpServlet, properties);
+        }
+    }
+
+    private void unregisterHttpServlet() {
+        if (httpServletServiceRegistration != null) {
+            httpServletServiceRegistration.unregister();
+        }
+    }
+
     protected void checkPluginIsRunning() {
         if (pluginInstance == null || !pluginInstance.getInstanceVariable(ACTIVE).isTrue()) {
             throw new IllegalStateException(String.format("Plugin %s didn't start properly", pluginMainClass));

pom.xml 7(+7 -0)

diff --git a/pom.xml b/pom.xml
index d9b4619..dc27391 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,6 +92,7 @@
                 <groupId>javax.servlet</groupId>
                 <artifactId>javax.servlet-api</artifactId>
                 <version>3.0.1</version>
+                <scope>provided</scope>
             </dependency>
             <dependency>
                 <groupId>com.ning.billing</groupId>
@@ -316,6 +317,12 @@
                 <scope>provided</scope>
             </dependency>
             <dependency>
+                <groupId>com.google.code.findbugs</groupId>
+                <artifactId>jsr305</artifactId>
+                <version>1.3.9</version>
+                <scope>provided</scope>
+            </dependency>
+            <dependency>
                 <groupId>com.google.inject</groupId>
                 <artifactId>guice</artifactId>
                 <version>3.0</version>