killbill-aplcache

osgi: install the Felix Web Console The Felix Web Console

2/14/2013 11:09:17 PM

Details

diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java
index 9fee49d..651eed8 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java
@@ -18,10 +18,14 @@ package com.ning.billing.jaxrs.resources;
 
 import java.io.IOException;
 
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.HEAD;
@@ -29,7 +33,6 @@ import javax.ws.rs.OPTIONS;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
 import javax.ws.rs.core.Response;
 
 import com.ning.billing.jaxrs.util.Context;
@@ -43,7 +46,7 @@ import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 
 @Singleton
-@Path(JaxrsResource.PLUGINS_PATH)
+@Path(JaxrsResource.PLUGINS_PATH + "{subResources:.*}")
 public class PluginResource extends JaxRsResourceBase {
 
     private final HttpServlet osgiServlet;
@@ -60,65 +63,97 @@ public class PluginResource extends JaxRsResourceBase {
     }
 
     @DELETE
-    @Path("/{pluginName:" + STRING_PATTERN + "}/{rest:.+}")
-    public Response doDELETE(@PathParam("pluginName") final String pluginName,
-                             @javax.ws.rs.core.Context final HttpServletRequest request,
-                             @javax.ws.rs.core.Context final HttpServletResponse response) throws ServletException, IOException {
-        return serviceViaOSGIPlugin(pluginName, request, response);
+    public Response doDELETE(@javax.ws.rs.core.Context final HttpServletRequest request,
+                             @javax.ws.rs.core.Context final HttpServletResponse response,
+                             @javax.ws.rs.core.Context final ServletContext servletContext,
+                             @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
     }
 
     @GET
-    @Path("/{pluginName:" + STRING_PATTERN + "}/{rest:.+}")
-    public Response doGET(@PathParam("pluginName") final String pluginName,
-                          @javax.ws.rs.core.Context final HttpServletRequest request,
-                          @javax.ws.rs.core.Context final HttpServletResponse response) throws ServletException, IOException {
-        return serviceViaOSGIPlugin(pluginName, request, response);
+    public Response doGET(@javax.ws.rs.core.Context final HttpServletRequest request,
+                          @javax.ws.rs.core.Context final HttpServletResponse response,
+                          @javax.ws.rs.core.Context final ServletContext servletContext,
+                          @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
     }
 
     @OPTIONS
-    @Path("/{pluginName:" + STRING_PATTERN + "}/{rest:.+}")
-    public Response doOPTIONS(@PathParam("pluginName") final String pluginName,
-                              @javax.ws.rs.core.Context final HttpServletRequest request,
-                              @javax.ws.rs.core.Context final HttpServletResponse response) throws ServletException, IOException {
-        return serviceViaOSGIPlugin(pluginName, request, response);
+    public Response doOPTIONS(@javax.ws.rs.core.Context final HttpServletRequest request,
+                              @javax.ws.rs.core.Context final HttpServletResponse response,
+                              @javax.ws.rs.core.Context final ServletContext servletContext,
+                              @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
     }
 
     @POST
-    @Path("/{pluginName:" + STRING_PATTERN + "}/{rest:.+}")
-    public Response doPOST(@PathParam("pluginName") final String pluginName,
-                           @javax.ws.rs.core.Context final HttpServletRequest request,
-                           @javax.ws.rs.core.Context final HttpServletResponse response) throws ServletException, IOException {
-        return serviceViaOSGIPlugin(pluginName, request, response);
+    public Response doPOST(@javax.ws.rs.core.Context final HttpServletRequest request,
+                           @javax.ws.rs.core.Context final HttpServletResponse response,
+                           @javax.ws.rs.core.Context final ServletContext servletContext,
+                           @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
     }
 
     @PUT
-    @Path("/{pluginName:" + STRING_PATTERN + "}/{rest:.+}")
-    public Response doPUT(@PathParam("pluginName") final String pluginName,
-                          @javax.ws.rs.core.Context final HttpServletRequest request,
-                          @javax.ws.rs.core.Context final HttpServletResponse response) throws ServletException, IOException {
-        return serviceViaOSGIPlugin(pluginName, request, response);
+    public Response doPUT(@javax.ws.rs.core.Context final HttpServletRequest request,
+                          @javax.ws.rs.core.Context final HttpServletResponse response,
+                          @javax.ws.rs.core.Context final ServletContext servletContext,
+                          @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
     }
 
     @HEAD
-    @Path("/{pluginName:" + STRING_PATTERN + "}/{rest:.+}")
-    public Response doHEAD(@PathParam("pluginName") final String pluginName,
-                           @javax.ws.rs.core.Context final HttpServletRequest request,
-                           @javax.ws.rs.core.Context final HttpServletResponse response) throws ServletException, IOException {
-        prepareOSGIRequest(pluginName, request);
+    public Response doHEAD(@javax.ws.rs.core.Context final HttpServletRequest request,
+                           @javax.ws.rs.core.Context final HttpServletResponse response,
+                           @javax.ws.rs.core.Context final ServletContext servletContext,
+                           @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+        prepareOSGIRequest(request, servletContext, servletConfig);
         osgiServlet.service(request, response);
 
         // Make sure to return 204
         return Response.noContent().build();
     }
 
-    private Response serviceViaOSGIPlugin(final String pluginName, final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
-        prepareOSGIRequest(pluginName, request);
-        osgiServlet.service(request, response);
+    private Response serviceViaOSGIPlugin(final HttpServletRequest request, final HttpServletResponse response,
+                                          final ServletContext servletContext, final ServletConfig servletConfig) throws ServletException, IOException {
+        prepareOSGIRequest(request, servletContext, servletConfig);
+        osgiServlet.service(new OSGIServletRequestWrapper(request), new OSGIServletResponseWrapper(response));
 
         return Response.status(response.getStatus()).build();
     }
 
-    private void prepareOSGIRequest(final String pluginName, final HttpServletRequest request) {
-        request.setAttribute("killbill.osgi.pluginName", pluginName);
+    private void prepareOSGIRequest(final HttpServletRequest request, final ServletContext servletContext, final ServletConfig servletConfig) {
+        request.setAttribute("killbill.osgi.servletContext", servletContext);
+        request.setAttribute("killbill.osgi.servletConfig", servletConfig);
+    }
+
+    // Request wrapper to hide the /plugins prefix to OSGI bundles
+    private static final class OSGIServletRequestWrapper extends HttpServletRequestWrapper {
+
+        public OSGIServletRequestWrapper(final HttpServletRequest request) {
+            super(request);
+        }
+
+        @Override
+        public String getPathInfo() {
+            return super.getPathInfo().replace(JaxrsResource.PLUGINS_PATH, "");
+        }
+
+        @Override
+        public String getContextPath() {
+            return JaxrsResource.PLUGINS_PATH;
+        }
+
+        @Override
+        public String getServletPath() {
+            return super.getServletPath().replace(JaxrsResource.PLUGINS_PATH, "");
+        }
+    }
+
+    private static final class OSGIServletResponseWrapper extends HttpServletResponseWrapper {
+
+        public OSGIServletResponseWrapper(final HttpServletResponse response) {
+            super(response);
+        }
     }
 }

osgi/pom.xml 51(+51 -0)

diff --git a/osgi/pom.xml b/osgi/pom.xml
index 76f1bef..e9386f8 100644
--- a/osgi/pom.xml
+++ b/osgi/pom.xml
@@ -32,6 +32,57 @@
             <artifactId>org.apache.felix.framework</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.configadmin</artifactId>
+            <version>1.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.log</artifactId>
+            <version>1.0.1</version>
+        </dependency>
+
+        <!-- TODO PIERRE Dependencies for the Felix Web Console.
+             In 4.0.1-SNAPSHOT, these are provided by the bundle,
+             see https://issues.apache.org/jira/browse/FELIX-3778.
+        -->
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.webconsole</artifactId>
+            <version>4.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+            <version>1.2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20070829</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.shell</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.osgi.service.obr</artifactId>
+            <version>1.0.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>4.3.1</version>
+        </dependency>
+
+        <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
diff --git a/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIService.java b/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIService.java
index 49d1289..068a91b 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIService.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIService.java
@@ -24,13 +24,14 @@ import java.util.Map;
 
 import javax.inject.Inject;
 
+import org.apache.felix.cm.impl.ConfigurationManager;
 import org.apache.felix.framework.Felix;
 import org.apache.felix.framework.util.FelixConstants;
+import org.apache.felix.webconsole.internal.OsgiManagerActivator;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
-import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.launch.Framework;
 import org.slf4j.Logger;
@@ -39,9 +40,7 @@ import org.slf4j.osgi.logservice.impl.Activator;
 
 import com.ning.billing.lifecycle.LifecycleHandlerType;
 import com.ning.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
-import com.ning.billing.osgi.api.OSGIPluginProperties;
 import com.ning.billing.osgi.api.OSGIService;
-import com.ning.billing.osgi.api.OSGIServiceRegistration;
 import com.ning.billing.osgi.api.config.PluginConfigServiceApi;
 import com.ning.billing.osgi.api.config.PluginJavaConfig;
 import com.ning.billing.osgi.api.config.PluginRubyConfig;
@@ -52,7 +51,6 @@ import com.ning.billing.payment.plugin.api.PaymentPluginApi;
 import com.ning.billing.util.config.OSGIConfig;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 
 public class DefaultOSGIService implements OSGIService {
 
@@ -60,7 +58,6 @@ public class DefaultOSGIService implements OSGIService {
 
     private static final Logger logger = LoggerFactory.getLogger(DefaultOSGIService.class);
 
-
     private final OSGIConfig osgiConfig;
     private final PluginFinder pluginFinder;
     private final PluginConfigServiceApi pluginConfigServiceApi;
@@ -115,7 +112,6 @@ public class DefaultOSGIService implements OSGIService {
     public void startFramework() {
     }
 
-
     @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
     public void stop() {
         try {
@@ -186,8 +182,19 @@ public class DefaultOSGIService implements OSGIService {
         final Map<Object, Object> felixConfig = new HashMap<Object, Object>();
         felixConfig.putAll(config);
 
-        // Install default bundles: killbill and slf4j ones
-        felixConfig.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, ImmutableList.<BundleActivator>of(killbillActivator, new Activator()));
+        // Install default bundles
+        // TODO PIERRE Should the Felix Web Console (and its dependencies) be rather installed at deploy time?
+        felixConfig.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP,
+                        ImmutableList.<BundleActivator>of(killbillActivator,
+                                                          // SLF4J LogService
+                                                          new Activator(),
+                                                          // Felix Web Console
+                                                          new OsgiManagerActivator(),
+                                                          // Felix Log Service (installed for the Web Console)
+                                                          // TODO PIERRE Does it conflict with the SLF4J one?
+                                                          new org.apache.felix.log.Activator(),
+                                                          // Felix Configuration Admin Service (installed for the Web Console)
+                                                          new ConfigurationManager()));
 
         final Framework felix = new Felix(felixConfig);
         felix.init();
diff --git a/osgi/src/main/java/com/ning/billing/osgi/glue/DefaultOSGIModule.java b/osgi/src/main/java/com/ning/billing/osgi/glue/DefaultOSGIModule.java
index 701e8dc..6b9ff07 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/glue/DefaultOSGIModule.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/glue/DefaultOSGIModule.java
@@ -16,9 +16,11 @@
 
 package com.ning.billing.osgi.glue;
 
+import javax.servlet.Servlet;
 import javax.servlet.http.HttpServlet;
 import javax.sql.DataSource;
 
+import org.osgi.service.http.HttpService;
 import org.skife.config.ConfigurationObjectFactory;
 
 import com.ning.billing.osgi.DefaultOSGIKillbill;
@@ -28,6 +30,7 @@ import com.ning.billing.osgi.api.OSGIKillbill;
 import com.ning.billing.osgi.api.OSGIService;
 import com.ning.billing.osgi.api.OSGIServiceRegistration;
 import com.ning.billing.osgi.api.config.PluginConfigServiceApi;
+import com.ning.billing.osgi.http.DefaultHttpService;
 import com.ning.billing.osgi.http.DefaultServletRouter;
 import com.ning.billing.osgi.http.OSGIServlet;
 import com.ning.billing.osgi.pluginconf.DefaultPluginConfigServiceApi;
@@ -51,14 +54,19 @@ public class DefaultOSGIModule extends AbstractModule {
     }
 
     protected void installOSGIServlet() {
-        bind(new TypeLiteral<OSGIServiceRegistration<HttpServlet>>() {}).to(DefaultServletRouter.class).asEagerSingleton();
+        bind(new TypeLiteral<OSGIServiceRegistration<Servlet>>() {}).to(DefaultServletRouter.class).asEagerSingleton();
         bind(HttpServlet.class).annotatedWith(Names.named(OSGI_NAMED)).to(OSGIServlet.class).asEagerSingleton();
     }
 
+    protected void installHttpService() {
+        bind(HttpService.class).to(DefaultHttpService.class).asEagerSingleton();
+    }
+
     @Override
     protected void configure() {
         installConfig();
         installOSGIServlet();
+        installHttpService();
 
         bind(OSGIService.class).to(DefaultOSGIService.class).asEagerSingleton();
 
diff --git a/osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpContext.java b/osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpContext.java
new file mode 100644
index 0000000..9ded549
--- /dev/null
+++ b/osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpContext.java
@@ -0,0 +1,45 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.net.URL;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.osgi.service.http.HttpContext;
+
+public class DefaultHttpContext implements HttpContext {
+
+    @Override
+    public boolean handleSecurity(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+        // Security should have already been handled by Shiro
+        return true;
+    }
+
+    @Override
+    public URL getResource(final String name) {
+        // Maybe it's in our classpath?
+        return DefaultHttpContext.class.getClassLoader().getResource(name);
+    }
+
+    @Override
+    public String getMimeType(final String name) {
+        return null;
+    }
+}
diff --git a/osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpService.java b/osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpService.java
new file mode 100644
index 0000000..a5d606c
--- /dev/null
+++ b/osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpService.java
@@ -0,0 +1,71 @@
+/*
+ * 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.http;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+
+@Singleton
+public class DefaultHttpService implements HttpService {
+
+    private final DefaultServletRouter servletRouter;
+
+    @Inject
+    public DefaultHttpService(final DefaultServletRouter servletRouter) {
+        this.servletRouter = servletRouter;
+    }
+
+    @Override
+    public void registerServlet(final String alias, final Servlet servlet, final Dictionary initparams, final HttpContext httpContext) throws ServletException, NamespaceException {
+        if (alias == null) {
+            throw new IllegalArgumentException("Invalid alias (null)");
+        } else if (servlet == null) {
+            throw new IllegalArgumentException("Invalid servlet (null)");
+        }
+
+        servletRouter.registerService(alias, servlet);
+    }
+
+    @Override
+    public void registerResources(final String alias, final String name, final HttpContext httpContext) throws NamespaceException {
+        final Servlet staticServlet = new StaticServlet(httpContext);
+        try {
+            registerServlet(alias, staticServlet, new Hashtable(), httpContext);
+        } catch (ServletException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    @Override
+    public void unregister(final String alias) {
+        servletRouter.unregisterService(alias);
+    }
+
+    @Override
+    public HttpContext createDefaultHttpContext() {
+        return new DefaultHttpContext();
+    }
+}
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 fc36943..8196e1b 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
@@ -21,28 +21,37 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import javax.inject.Singleton;
-import javax.servlet.http.HttpServlet;
+import javax.servlet.Servlet;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.ning.billing.osgi.api.OSGIServiceRegistration;
 
 @Singleton
-public class DefaultServletRouter implements OSGIServiceRegistration<HttpServlet> {
+public class DefaultServletRouter implements OSGIServiceRegistration<Servlet> {
+
+    private static final Logger logger = LoggerFactory.getLogger(DefaultServletRouter.class);
 
-    private final Map<String, HttpServlet> pluginServlets = new ConcurrentHashMap<String, HttpServlet>();
+    // Internal Servlet routing table: map of plugin prefixes to servlet instances.
+    // A plugin prefix can be foo, foo/bar, foo/bar/baz, ... and is mounted on /plugins/<pluginPrefix>
+    private final Map<String, Servlet> pluginServlets = new ConcurrentHashMap<String, Servlet>();
 
     @Override
-    public void registerService(final String pluginName, final HttpServlet httpServlet) {
-        pluginServlets.put(pluginName, httpServlet);
+    public void registerService(final String pathPrefix, final Servlet httpServlet) {
+        logger.info("Registering OSGI servlet at " + pathPrefix);
+        pluginServlets.put(pathPrefix, httpServlet);
     }
 
     @Override
-    public void unregisterService(final String pluginName) {
-        pluginServlets.remove(pluginName);
+    public void unregisterService(final String pathPrefix) {
+        logger.info("Unregistering OSGI servlet at " + pathPrefix);
+        pluginServlets.remove(pathPrefix);
     }
 
     @Override
-    public HttpServlet getServiceForPluginName(final String pluginName) {
-        return pluginServlets.get(pluginName);
+    public Servlet getServiceForPluginName(final String pathPrefix) {
+        return getServletForPathPrefix(pathPrefix);
     }
 
     @Override
@@ -51,7 +60,23 @@ public class DefaultServletRouter implements OSGIServiceRegistration<HttpServlet
     }
 
     @Override
-    public Class<HttpServlet> getServiceType() {
-        return HttpServlet.class;
+    public Class<Servlet> getServiceType() {
+        return Servlet.class;
+    }
+
+    // TODO PIERRE Naive implementation - we should rather switch to e.g. heap tree
+    public String getPluginPrefixForPath(final String pathPrefix) {
+        String bestMatch = null;
+        for (final String potentialMatch : pluginServlets.keySet()) {
+            if (pathPrefix.startsWith(potentialMatch) && (bestMatch == null || bestMatch.length() < potentialMatch.length())) {
+                bestMatch = potentialMatch;
+            }
+        }
+        return bestMatch;
+    }
+
+    private Servlet getServletForPathPrefix(final String pathPrefix) {
+        final String bestMatch = getPluginPrefixForPath(pathPrefix);
+        return bestMatch == null ? null : pluginServlets.get(bestMatch);
     }
 }
diff --git a/osgi/src/main/java/com/ning/billing/osgi/http/OSGIServlet.java b/osgi/src/main/java/com/ning/billing/osgi/http/OSGIServlet.java
index 182d142..a9af086 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/http/OSGIServlet.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/http/OSGIServlet.java
@@ -17,21 +17,26 @@
 package com.ning.billing.osgi.http;
 
 import java.io.IOException;
+import java.util.Vector;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
 
-import com.ning.billing.osgi.api.OSGIServiceRegistration;
-
 @Singleton
 public class OSGIServlet extends HttpServlet {
 
+    private final Vector<Servlet> initializedServlets = new Vector<Servlet>();
+    private final Object servletsMonitor = new Object();
+
     @Inject
-    private OSGIServiceRegistration<HttpServlet> servletRouter;
+    private DefaultServletRouter servletRouter;
 
     @Override
     protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
@@ -64,16 +69,57 @@ public class OSGIServlet extends HttpServlet {
     }
 
     private void serviceViaPlugin(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
-        final HttpServlet pluginServlet = getPluginServlet(req);
+        // requestPath is the full path minus the JAX-RS prefix (/plugins)
+        final String requestPath = req.getServletPath() + req.getPathInfo();
+
+        final Servlet pluginServlet = getPluginServlet(requestPath);
         if (pluginServlet != null) {
-            pluginServlet.service(req, resp);
+            initializeServletIfNeeded(req, pluginServlet);
+            final OSGIServletRequestWrapper requestWrapper = new OSGIServletRequestWrapper(req, servletRouter.getPluginPrefixForPath(requestPath));
+            pluginServlet.service(requestWrapper, resp);
         } else {
             resp.sendError(404);
         }
     }
 
-    private HttpServlet getPluginServlet(final HttpServletRequest req) {
-        final String pluginName = (String) req.getAttribute("killbill.osgi.pluginName");
+    // Request wrapper to hide the plugin prefix to OSGI servlets (the plugin prefix serves as a servlet path)
+    private static final class OSGIServletRequestWrapper extends HttpServletRequestWrapper {
+
+        private final String pluginPrefix;
+
+        public OSGIServletRequestWrapper(final HttpServletRequest request, final String pluginPrefix) {
+            super(request);
+            this.pluginPrefix = pluginPrefix;
+        }
+
+        @Override
+        public String getPathInfo() {
+            return super.getPathInfo().replace(pluginPrefix, "");
+        }
+
+        @Override
+        public String getContextPath() {
+            return super.getContextPath() + pluginPrefix;
+        }
+    }
+
+    // Hack to bridge the gap between the web container and the OSGI servlets
+    private void initializeServletIfNeeded(final HttpServletRequest req, final Servlet pluginServlet) throws ServletException {
+        if (!initializedServlets.contains(pluginServlet)) {
+            synchronized (servletsMonitor) {
+                if (!initializedServlets.contains(pluginServlet)) {
+                    final ServletConfig servletConfig = (ServletConfig) req.getAttribute("killbill.osgi.servletConfig");
+                    if (servletConfig != null) {
+                        // TODO PIERRE The servlet will never be destroyed!
+                        pluginServlet.init(servletConfig);
+                        initializedServlets.add(pluginServlet);
+                    }
+                }
+            }
+        }
+    }
+
+    private Servlet getPluginServlet(final String pluginName) {
         if (pluginName != null) {
             return servletRouter.getServiceForPluginName(pluginName);
         } else {
diff --git a/osgi/src/main/java/com/ning/billing/osgi/http/StaticServlet.java b/osgi/src/main/java/com/ning/billing/osgi/http/StaticServlet.java
new file mode 100644
index 0000000..9f8d5ac
--- /dev/null
+++ b/osgi/src/main/java/com/ning/billing/osgi/http/StaticServlet.java
@@ -0,0 +1,86 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.net.URL;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.osgi.service.http.HttpContext;
+
+import com.google.common.io.Resources;
+
+// Simple servlet to serve OSGI resources
+public class StaticServlet extends HttpServlet {
+
+    private final HttpContext httpContext;
+
+    public StaticServlet(final HttpContext httpContext) {
+        this.httpContext = httpContext;
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        final URL url = findResourceURL(req);
+        if (url != null) {
+            Resources.copy(url, resp.getOutputStream());
+            resp.setStatus(200);
+            return;
+        }
+
+        // If we can't find it, the container might
+        final RequestDispatcher rd = getServletContext().getNamedDispatcher("default");
+        final HttpServletRequest wrapped = new HttpServletRequestWrapper(req) {
+            public String getServletPath() { return ""; }
+        };
+        rd.forward(wrapped, resp);
+    }
+
+    // TODO PIERRE HUGE HACK
+    // We don't really know at this point the resource path to look for
+    // e.g. if the request is for /plugins/foo/bar/baz/qux.css, should
+    // we look for /qux.css? /baz/qux.css? /bar/baz/qux.css? /foo/bar/baz/qux.css?
+    private URL findResourceURL(final HttpServletRequest request) {
+        final String url = request.getRequestURI();
+        for (int i = 0; i < url.lastIndexOf('/'); i++) {
+            final int idx = url.indexOf('/', i);
+            if (idx > -1) {
+                final String resourceName = url.substring(idx);
+                final URL match = findResourceURL(resourceName);
+                if (match != null) {
+                    return match;
+                }
+            }
+        }
+        return null;
+    }
+
+    private URL findResourceURL(final String resourceName) {
+        URL url = httpContext.getResource(resourceName);
+        if (url == null) {
+            // Look into the OSGI bundle JAR
+            url = httpContext.getClass().getResource(resourceName);
+        }
+        return url;
+    }
+}
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 33b320f..51b84d6 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java
@@ -19,7 +19,7 @@ package com.ning.billing.osgi;
 import java.util.List;
 
 import javax.inject.Inject;
-import javax.servlet.http.HttpServlet;
+import javax.servlet.Servlet;
 
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
@@ -27,6 +27,7 @@ import org.osgi.framework.ServiceEvent;
 import org.osgi.framework.ServiceListener;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.http.HttpService;
 
 import com.ning.billing.osgi.api.OSGIKillbill;
 import com.ning.billing.osgi.api.OSGIPluginProperties;
@@ -38,6 +39,7 @@ import com.google.common.collect.ImmutableList;
 public class KillbillActivator implements BundleActivator, ServiceListener {
 
     private final OSGIKillbill osgiKillbill;
+    private final HttpService defaultHttpService;
     private final List<OSGIServiceRegistration> allRegistrationHandlers;
 
     private volatile ServiceRegistration osgiKillbillRegistration;
@@ -46,9 +48,11 @@ public class KillbillActivator implements BundleActivator, ServiceListener {
 
     @Inject
     public KillbillActivator(final OSGIKillbill osgiKillbill,
-                             final OSGIServiceRegistration<HttpServlet> servletRouter,
+                             final HttpService defaultHttpService,
+                             final OSGIServiceRegistration<Servlet> servletRouter,
                              final OSGIServiceRegistration<PaymentPluginApi> paymentProviderPluginRegistry) {
         this.osgiKillbill = osgiKillbill;
+        this.defaultHttpService = defaultHttpService;
         this.allRegistrationHandlers = ImmutableList.<OSGIServiceRegistration>of(servletRouter, paymentProviderPluginRegistry);
     }
 
@@ -117,9 +121,10 @@ public class KillbillActivator implements BundleActivator, ServiceListener {
         return true;
     }
 
-
     private void registerServices(final BundleContext context) {
         osgiKillbillRegistration = context.registerService(OSGIKillbill.class.getName(), osgiKillbill, null);
+
+        context.registerService(HttpService.class.getName(), defaultHttpService, null);
     }
 
     private void unregisterServices() {