FELIX-4137 Support Web Application Bundles

Apply patch by Dominique Pfister (thanks alot)

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1496453 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/http/jetty/pom.xml b/http/jetty/pom.xml
index 7ebb950..fcff4a9 100644
--- a/http/jetty/pom.xml
+++ b/http/jetty/pom.xml
@@ -71,11 +71,13 @@
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.core</artifactId>
+            <version>4.2.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.compendium</artifactId>
+            <version>4.2.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -108,6 +110,11 @@
             <version>7.6.3.v20120416</version>
         </dependency>
         <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-webapp</artifactId>
+            <version>7.6.3.v20120416</version>
+        </dependency>
+        <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.http.api</artifactId>
             <version>2.2.0</version>
diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
index 8091832..cbac529 100644
--- a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
+++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
@@ -16,10 +16,13 @@
  */
 package org.apache.felix.http.jetty.internal;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Properties;
 
@@ -95,6 +98,9 @@
     /** Felix specific property to set the servlet context path of the Http Service */
     public static final String FELIX_HTTP_CONTEXT_PATH = "org.apache.felix.http.context_path";
 
+    /** Felix specific property to set the list of path exclusions for Web Application Bundles */
+    public static final String FELIX_HTTP_PATH_EXCLUSIONS = "org.apache.felix.http.path_exclusions";
+
     private final BundleContext context;
     private boolean debug;
     private String host;
@@ -116,6 +122,7 @@
     private int requestBufferSize;
     private int responseBufferSize;
     private String contextPath;
+    private String[] pathExclusions;
 
     /**
      * Properties from the configuration not matching any of the
@@ -247,6 +254,11 @@
         return contextPath;
     }
 
+    public String[] getPathExclusions()
+    {
+        return this.pathExclusions;
+    }
+
     public void reset()
     {
         update(null);
@@ -278,6 +290,7 @@
         this.requestBufferSize = getIntProperty(FELIX_JETTY_REQUEST_BUFFER_SIZE, 8 * 014);
         this.responseBufferSize = getIntProperty(FELIX_JETTY_RESPONSE_BUFFER_SIZE, 24 * 1024);
         this.contextPath = validateContextPath(getProperty(props, FELIX_HTTP_CONTEXT_PATH, null));
+        this.pathExclusions = getStringArrayProperty(props, FELIX_HTTP_PATH_EXCLUSIONS, new String[] { "/system" });
 
         // copy rest of the properties
         Enumeration keys = props.keys();
@@ -319,6 +332,41 @@
         }
     }
 
+    private String[] getStringArrayProperty(Dictionary props, String name, String[] defValue)
+    {
+        Object value = props.remove(name);
+        if (value == null)
+        {
+            value = this.context.getProperty(name);
+        }
+        if (value instanceof String)
+        {
+            return new String[]
+                { (String) value };
+        }
+        else if (value instanceof String[])
+        {
+            return (String[]) value;
+        }
+        else if (value instanceof Collection)
+        {
+            ArrayList<String> conv = new ArrayList<String>();
+            for (Iterator<?> vi = ((Collection<?>) value).iterator(); vi.hasNext();)
+            {
+                Object object = vi.next();
+                if (object != null)
+                {
+                    conv.add(String.valueOf(object));
+                }
+            }
+            return conv.toArray(new String[conv.size()]);
+        }
+        else
+        {
+            return defValue;
+        }
+    }
+
     private static String validateContextPath(String ctxPath)
     {
         // undefined, empty, or root context path
diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
index f78fe2d..4dc0a36 100644
--- a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
+++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
@@ -25,8 +25,14 @@
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
 
 import org.apache.felix.http.base.internal.DispatcherServlet;
 import org.apache.felix.http.base.internal.EventDispatcher;
@@ -37,6 +43,7 @@
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.SessionManager;
 import org.eclipse.jetty.server.bio.SocketConnector;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.StatisticsHandler;
 import org.eclipse.jetty.server.nio.SelectChannelConnector;
 import org.eclipse.jetty.server.ssl.SslConnector;
@@ -44,12 +51,26 @@
 import org.eclipse.jetty.server.ssl.SslSocketConnector;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
 import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+import javax.servlet.ServletContext;
 
 public final class JettyService
-    implements Runnable
+        extends AbstractLifeCycle.AbstractLifeCycleListener
+        implements BundleTrackerCustomizer, ServiceTrackerCustomizer
 {
     /** PID for configuration of the HTTP service. */
     private static final String PID = "org.apache.felix.http";
@@ -57,16 +78,27 @@
     /** Endpoint service registration property from RFC 189 */
     private static final String REG_PROPERTY_ENDPOINTS = "osgi.http.service.endpoints";
 
+    private static final String HEADER_WEB_CONTEXT_PATH = "Web-ContextPath";
+    private static final String HEADER_ACTIVATION_POLICY = "Bundle-ActivationPolicy";
+    private static final String WEB_SYMBOLIC_NAME = "osgi.web.symbolicname";
+    private static final String WEB_VERSION = "osgi.web.version";
+    private static final String WEB_CONTEXT_PATH = "osgi.web.contextpath";
+    private static final String OSGI_BUNDLE_CONTEXT = "osgi-bundlecontext";
+
     private final JettyConfig config;
     private final BundleContext context;
-    private boolean running;
-    private Thread thread;
     private ServiceRegistration configServiceReg;
+    private ExecutorService executor;
     private Server server;
+    private ContextHandlerCollection parent;
     private DispatcherServlet dispatcher;
     private EventDispatcher eventDispatcher;
     private final HttpServiceController controller;
     private MBeanServerTracker mbeanServerTracker;
+    private BundleTracker bundleTracker;
+    private ServiceTracker serviceTracker;
+    private EventAdmin eventAdmin;
+    private Map<String, Deployment> deployments = new LinkedHashMap<String, Deployment>();
 
     public JettyService(BundleContext context, DispatcherServlet dispatcher, EventDispatcher eventDispatcher,
         HttpServiceController controller)
@@ -86,24 +118,48 @@
         this.configServiceReg = this.context.registerService("org.osgi.service.cm.ManagedService",
             new JettyManagedService(this), props);
 
-        this.thread = new Thread(this, "Jetty HTTP Service");
-        this.thread.start();
+        this.executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+           public Thread newThread(Runnable runnable)
+           {
+                Thread t = new Thread(runnable);
+                t.setName("Jetty HTTP Service");
+                return t;
+            }
+        });
+        this.executor.submit(new JettyOperation() {
+            @Override
+            protected void doExecute() throws Exception {
+                startJetty();
+            }
+        });
+
+        this.serviceTracker = new ServiceTracker(this.context, EventAdmin.class.getName(), this);
+        this.serviceTracker.open();
+
+        this.bundleTracker = new BundleTracker(this.context, Bundle.ACTIVE | Bundle.STARTING, this);
+        this.bundleTracker.open();
     }
 
     public void stop()
         throws Exception
     {
+        if (this.executor != null && !this.executor.isShutdown()) {
+            this.executor.submit(new JettyOperation() {
+                @Override
+                protected void doExecute() throws Exception {
+                    stopJetty();
+                }
+            });
+            this.executor.shutdown();
+        }
         if (this.configServiceReg != null) {
             this.configServiceReg.unregister();
         }
-
-        this.running = false;
-        this.thread.interrupt();
-
-        try {
-            this.thread.join(3000);
-        } catch (InterruptedException e) {
-            // Do nothing
+        if (this.bundleTracker != null) {
+            this.bundleTracker.close();
+        }
+        if (this.serviceTracker != null) {
+            this.serviceTracker.close();
         }
     }
 
@@ -119,8 +175,14 @@
     {
         this.config.update(props);
 
-        if (this.running && (this.thread != null)) {
-            this.thread.interrupt();
+        if (this.executor != null  && !this.executor.isShutdown()) {
+            this.executor.submit(new JettyOperation() {
+                @Override
+                protected void doExecute() throws Exception {
+                    stopJetty();
+                    startJetty();
+                }
+            });
         }
     }
 
@@ -163,6 +225,7 @@
             StringBuffer message = new StringBuffer("Started jetty ").append(Server.getVersion()).append(" at port(s)");
             HashLoginService realm = new HashLoginService("OSGi HTTP Service Realm");
             this.server = new Server();
+            this.server.addLifeCycleListener(this);
 
             // HTTP/1.1 requires Date header if possible (it is)
             this.server.setSendDateHeader(true);
@@ -181,7 +244,10 @@
                 message.append(" HTTPS:").append(this.config.getHttpsPort());
             }
 
-            ServletContextHandler context = new ServletContextHandler(this.server, this.config.getContextPath(), ServletContextHandler.SESSIONS);
+            this.parent = new ContextHandlerCollection();
+
+            ServletContextHandler context = new ServletContextHandler(this.parent,
+                    this.config.getContextPath(), ServletContextHandler.SESSIONS);
 
             message.append(" on context path ").append(this.config.getContextPath());
             configureSessionManager(context);
@@ -196,6 +262,7 @@
                 context.addBean(new StatisticsHandler());
             }
 
+            this.server.setHandler(this.parent);
             this.server.start();
             SystemLogger.info(message.toString());
         }
@@ -342,31 +409,6 @@
         manager.setMaxCookieAge(this.config.getIntProperty(SessionManager.__MaxAgeProperty, -1));
     }
 
-    public void run()
-    {
-        this.running = true;
-        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
-
-        while (this.running) {
-            startJetty();
-
-            synchronized (this)
-            {
-                try
-                {
-                    wait();
-                }
-                catch (InterruptedException e)
-                {
-                    // we will definitely be interrupted
-                }
-            }
-
-            stopJetty();
-        }
-    }
-
-
     private String getEndpoint(final Connector listener, final InetAddress ia)
     {
         if (ia.isLoopbackAddress())
@@ -491,4 +533,266 @@
         }
         props.put(REG_PROPERTY_ENDPOINTS, endpoints.toArray(new String[endpoints.size()]));
     }
+
+    private Deployment startWebAppBundle(Bundle bundle, String contextPath)
+    {
+        postEvent(WebEvent.DEPLOYING(bundle, this.context.getBundle()));
+
+        // check existing deployments
+        Deployment deployment = this.deployments.get(contextPath);
+        if (deployment != null) {
+            SystemLogger.warning(String.format(
+                    "Web application bundle %s has context path %s which is already registered",
+                    bundle.getSymbolicName(), contextPath), null);
+            postEvent(WebEvent.FAILED(bundle, this.context.getBundle(), null, contextPath,
+                    deployment.getBundle().getBundleId()));
+            return null;
+        }
+
+        // check context path belonging to Http Service implementation
+        if (contextPath.equals("/")) {
+            SystemLogger.warning(String.format(
+                    "Web application bundle %s has context path %s which is reserved",
+                    bundle.getSymbolicName(), contextPath), null);
+            postEvent(WebEvent.FAILED(bundle, this.context.getBundle(), null, contextPath,
+                    this.context.getBundle().getBundleId()));
+            return null;
+        }
+
+        // check against excluded paths
+        for (String path : this.config.getPathExclusions()) {
+            if (contextPath.startsWith(path)) {
+                SystemLogger.warning(String.format(
+                        "Web application bundle %s has context path %s which clashes with excluded path prefix %s",
+                        bundle.getSymbolicName(), contextPath, path), null);
+                postEvent(WebEvent.FAILED(bundle, this.context.getBundle(), null, path, null));
+                return null;
+            }
+        }
+
+        deployment = new Deployment(contextPath, bundle);
+        this.deployments.put(contextPath, deployment);
+
+        WebAppBundleContext context = new WebAppBundleContext(contextPath, bundle, this.getClass().getClassLoader());
+        deploy(deployment, context);
+        return deployment;
+    }
+
+    public void deploy(final Deployment deployment, final WebAppBundleContext context)
+    {
+        if (this.executor != null  && !this.executor.isShutdown()) {
+            this.executor.submit(new JettyOperation() {
+                @Override
+                protected void doExecute() {
+                    final Bundle webAppBundle = deployment.getBundle();
+                    final Bundle extenderBundle = JettyService.this.context.getBundle();
+
+                    try {
+                        JettyService.this.parent.addHandler(context);
+                        context.start();
+
+                        Dictionary<String, Object> props = new Hashtable<String, Object>();
+                        props.put(WEB_SYMBOLIC_NAME, webAppBundle.getSymbolicName());
+                        props.put(WEB_VERSION, webAppBundle.getVersion());
+                        props.put(WEB_CONTEXT_PATH, deployment.getContextPath());
+                        deployment.setRegistration(webAppBundle.getBundleContext().registerService(
+                                ServletContext.class.getName(), context.getServletContext(), props));
+
+                        context.getServletContext().setAttribute(OSGI_BUNDLE_CONTEXT, webAppBundle.getBundleContext());
+
+                        postEvent(WebEvent.DEPLOYED(webAppBundle, extenderBundle));
+                    } catch (Exception e) {
+                        SystemLogger.error(String.format("Deploying web application bundle %s failed.", webAppBundle.getSymbolicName()), e);
+                        postEvent(WebEvent.FAILED(webAppBundle, extenderBundle, e, null, null));
+                        deployment.setContext(null);
+                    }
+                }
+            });
+            deployment.setContext(context);
+        }
+    }
+
+    public void undeploy(final Deployment deployment, final WebAppBundleContext context)
+    {
+        if (this.executor != null  && !this.executor.isShutdown()) {
+            this.executor.submit(new JettyOperation(){
+                @Override
+                protected void doExecute() {
+                    final Bundle webAppBundle = deployment.getBundle();
+                    final Bundle extenderBundle = JettyService.this.context.getBundle();
+
+                    try {
+                        postEvent(WebEvent.UNDEPLOYING(webAppBundle, extenderBundle));
+
+                        context.getServletContext().removeAttribute(OSGI_BUNDLE_CONTEXT);
+
+                        ServiceRegistration registration = deployment.getRegistration();
+                        if (registration != null) {
+                            registration.unregister();
+                        }
+                        deployment.setRegistration(null);
+                        context.stop();
+                    } catch (Exception e) {
+                        SystemLogger.error(String.format("Undeploying web application bundle %s failed.", webAppBundle.getSymbolicName()), e);
+                    } finally {
+                        postEvent(WebEvent.UNDEPLOYED(webAppBundle, extenderBundle));
+                    }
+                }
+            });
+        }
+        deployment.setContext(null);
+    }
+
+    public Object addingBundle(Bundle bundle, BundleEvent event)
+    {
+        return detectWebAppBundle(bundle);
+    }
+
+    public void modifiedBundle(Bundle bundle, BundleEvent event, Object object)
+    {
+        detectWebAppBundle(bundle);
+    }
+
+    private Object detectWebAppBundle(Bundle bundle)
+    {
+        if (bundle.getState() == Bundle.ACTIVE || (bundle.getState() == Bundle.STARTING &&
+                "Lazy".equals(bundle.getHeaders().get(HEADER_ACTIVATION_POLICY)))) {
+
+            String contextPath = (String) bundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH);
+            if (contextPath != null) {
+                return startWebAppBundle(bundle, contextPath);
+            }
+        }
+        return null;
+    }
+
+    public void removedBundle(Bundle bundle, BundleEvent event, Object object)
+    {
+        String contextPath = (String) bundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH);
+        if (contextPath == null) {
+            return;
+        }
+
+        Deployment deployment = this.deployments.remove(contextPath);
+        if (deployment != null && deployment.getContext() != null) {
+            // remove registration, since bundle is already stopping
+            deployment.setRegistration(null);
+            undeploy(deployment, deployment.getContext());
+        }
+    }
+
+    public Object addingService(ServiceReference reference)
+    {
+        Object service = this.context.getService(reference);
+        modifiedService(reference, service);
+        return service;
+    }
+
+    public void modifiedService(ServiceReference reference, Object service)
+    {
+        this.eventAdmin = (EventAdmin) service;
+    }
+
+    public void removedService(ServiceReference reference, Object service)
+    {
+        this.context.ungetService(reference);
+        this.eventAdmin = null;
+    }
+
+    private void postEvent(Event event)
+    {
+        if (this.eventAdmin != null) {
+            this.eventAdmin.postEvent(event);
+        }
+    }
+
+    public void lifeCycleStarted(LifeCycle event)
+    {
+        for (Deployment deployment : this.deployments.values()) {
+            if (deployment.getContext() == null) {
+                postEvent(WebEvent.DEPLOYING(deployment.getBundle(), this.context.getBundle()));
+                WebAppBundleContext context = new WebAppBundleContext(deployment.getContextPath(),
+                        deployment.getBundle(), this.getClass().getClassLoader());
+                deploy(deployment, context);
+            }
+        }
+    }
+
+    public void lifeCycleStopping(LifeCycle event)
+    {
+        for (Deployment deployment : this.deployments.values()) {
+            if (deployment.getContext() != null) {
+                undeploy(deployment, deployment.getContext());
+            }
+        }
+    }
+
+    /**
+     * A deployment represents a web application bundle that may or may not be deployed.
+     */
+    static class Deployment
+    {
+        private String contextPath;
+        private Bundle bundle;
+        private WebAppBundleContext context;
+        private ServiceRegistration registration;
+
+        public Deployment(String contextPath, Bundle bundle)
+        {
+            this.contextPath = contextPath;
+            this.bundle = bundle;
+        }
+
+        public Bundle getBundle()
+        {
+            return this.bundle;
+        }
+
+        public String getContextPath()
+        {
+            return this.contextPath;
+        }
+
+        public WebAppBundleContext getContext()
+        {
+            return this.context;
+        }
+
+        public void setContext(WebAppBundleContext context)
+        {
+            this.context = context;
+        }
+
+        public ServiceRegistration getRegistration()
+        {
+            return this.registration;
+        }
+
+        public void setRegistration(ServiceRegistration registration)
+        {
+            this.registration = registration;
+        }
+    }
+
+    /**
+     * A Jetty operation is executed with the context class loader set to this class's
+     * class loader.
+     */
+    abstract static class JettyOperation implements Callable<Void>
+    {
+        public Void call() throws Exception
+        {
+            ClassLoader cl = Thread.currentThread().getContextClassLoader();
+
+            try {
+                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
+                doExecute();
+                return null;
+            } finally {
+                Thread.currentThread().setContextClassLoader(cl);
+            }
+        }
+
+        protected abstract void doExecute() throws Exception;
+    }
 }
diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebAppBundleContext.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebAppBundleContext.java
new file mode 100644
index 0000000..0c50c60
--- /dev/null
+++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebAppBundleContext.java
@@ -0,0 +1,127 @@
+/*
+ * 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.felix.http.jetty.internal;
+
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.URLResource;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.osgi.framework.Bundle;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Enumeration;
+
+class WebAppBundleContext extends WebAppContext
+{
+    public WebAppBundleContext(String contextPath, final Bundle bundle, final ClassLoader parent)
+    {
+        super(null, contextPath.substring(1), contextPath);
+
+        this.setBaseResource(new BundleURLResource(bundle.getEntry("/")));
+        this.setClassLoader(new ClassLoader(parent) {
+            @Override
+            protected Class<?> findClass(String s) throws ClassNotFoundException {
+                // Don't try to load classes from the bundle when it is not active
+                if (bundle.getState() == Bundle.ACTIVE) {
+                    try {
+                        return bundle.loadClass(s);
+                    } catch (ClassNotFoundException e) {
+                    }
+                }
+                return super.findClass(s);
+            }
+
+            @Override
+            protected URL findResource(String name) {
+                // Don't try to load resources from the bundle when it is not active
+                if (bundle.getState() == Bundle.ACTIVE) {
+                    URL url = bundle.getResource(name);
+                    if (url != null) {
+                        return url;
+                    }
+                }
+                return super.findResource(name);
+            }
+
+            @Override
+            @SuppressWarnings({"unchecked"})
+            protected Enumeration<URL> findResources(String name) throws IOException {
+                // Don't try to load resources from the bundle when it is not active
+                if (bundle.getState() == Bundle.ACTIVE) {
+                    Enumeration<URL> urls = (Enumeration<URL>) bundle.getResources(name);
+                    if (urls != null) {
+                        return urls;
+                    }
+                }
+                return super.findResources(name);
+            }
+        });
+        this.setThrowUnavailableOnStartupException(true);
+    }
+
+    @Override
+    public Resource newResource(URL url) throws IOException
+    {
+        if (url == null) {
+            return null;
+        }
+        return new BundleURLResource(url);
+    }
+
+    static class BundleURLResource extends URLResource
+    {
+        BundleURLResource(URL url)
+        {
+            super(url, null);
+        }
+
+        @Override
+        public synchronized void release()
+        {
+            if (this._in != null) {
+                // Do not close this input stream: it would invalidate
+                // the associated zipfile's inflater and every future access
+                // to some bundle entry leads to an NPE with message
+                // "Inflater has been closed"
+                this._in = null;
+            }
+            super.release();
+        }
+
+        @Override
+        public Resource addPath(String path) throws MalformedURLException
+        {
+            if (path == null) {
+                return null;
+            }
+            path = URIUtil.canonicalPath(path);
+
+            URL url = new URL(URIUtil.addPaths(this._url.toExternalForm(), path));
+            return new BundleURLResource(url);
+        }
+
+        @Override
+        public File getFile() throws IOException
+        {
+            // not available as a file
+            return null;
+        }
+    }
+}
diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebEvent.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebEvent.java
new file mode 100644
index 0000000..b5e2cc0
--- /dev/null
+++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebEvent.java
@@ -0,0 +1,98 @@
+/*
+ * 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.felix.http.jetty.internal;
+
+import org.osgi.framework.Bundle;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventConstants;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+public abstract class WebEvent
+{
+    private static final String TOPIC_WEB_EVENT = "org/osgi/service/web";
+    private static final String TOPIC_DEPLOYING = TOPIC_WEB_EVENT + "/DEPLOYING";
+    private static final String TOPIC_DEPLOYED = TOPIC_WEB_EVENT + "/DEPLOYED";
+    private static final String TOPIC_UNDEPLOYING = TOPIC_WEB_EVENT + "/UNDEPLOYING";
+    private static final String TOPIC_UNDEPLOYED = TOPIC_WEB_EVENT + "/UNDEPLOYED";
+    private static final String TOPIC_FAILED = TOPIC_WEB_EVENT + "/FAILED";
+
+    private static final String CONTEXT_PATH = "context.path";
+    private static final String EXCEPTION = "exception";
+    private static final String COLLISION = "collision";
+    private static final String COLLISION_BUNDLES = "collision.bundles";
+
+    private static final String EXTENDER_BUNDLE = "extender." + EventConstants.BUNDLE;
+    private static final String EXTENDER_BUNDLE_ID = "extender." + EventConstants.BUNDLE_ID;
+    private static final String EXTENDER_BUNDLE_VERSION = "extender." + EventConstants.BUNDLE_VERSION;
+    private static final String EXTENDER_BUNDLE_SYMBOLICNAME = "extender." + EventConstants.BUNDLE_SYMBOLICNAME;
+
+    private static final String HEADER_WEB_CONTEXT_PATH = "Web-ContextPath";
+
+    static Event DEPLOYING(Bundle webAppBundle, Bundle extenderBundle)
+    {
+        return new Event(TOPIC_DEPLOYING, createBaseProperties(webAppBundle, extenderBundle));
+    }
+
+    static Event DEPLOYED(Bundle webAppBundle, Bundle extenderBundle)
+    {
+        return new Event(TOPIC_DEPLOYED, createBaseProperties(webAppBundle, extenderBundle));
+    }
+
+    static Event UNDEPLOYING(Bundle webAppBundle, Bundle extenderBundle)
+    {
+        return new Event(TOPIC_UNDEPLOYING, createBaseProperties(webAppBundle, extenderBundle));
+    }
+
+    static Event UNDEPLOYED(Bundle webAppBundle, Bundle extenderBundle)
+    {
+        return new Event(TOPIC_UNDEPLOYED, createBaseProperties(webAppBundle, extenderBundle));
+    }
+
+    static Event FAILED(Bundle webAppBundle, Bundle extenderBundle, Throwable exception,
+                        String collision, Long collisionBundles)
+    {
+        Dictionary<String,Object> props = createBaseProperties(webAppBundle, extenderBundle);
+        if (exception != null) {
+            props.put(EXCEPTION, exception);
+        }
+        if (collision != null) {
+            props.put(COLLISION, collision);
+        }
+        if (collisionBundles != null) {
+            props.put(COLLISION_BUNDLES, collisionBundles);
+        }
+        return new Event(TOPIC_FAILED, props);
+    }
+
+    private static Dictionary<String,Object> createBaseProperties(Bundle webAppBundle, Bundle extenderBundle)
+    {
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(EventConstants.BUNDLE_SYMBOLICNAME, webAppBundle.getSymbolicName());
+        props.put(EventConstants.BUNDLE_ID, webAppBundle.getBundleId());
+        props.put(EventConstants.BUNDLE, webAppBundle);
+        props.put(EventConstants.BUNDLE_VERSION, webAppBundle.getVersion());
+        props.put(CONTEXT_PATH, webAppBundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH));
+        props.put(EventConstants.TIMESTAMP, System.currentTimeMillis());
+        props.put(EXTENDER_BUNDLE, extenderBundle);
+        props.put(EXTENDER_BUNDLE_ID, extenderBundle.getBundleId());
+        props.put(EXTENDER_BUNDLE_SYMBOLICNAME, extenderBundle.getSymbolicName());
+        props.put(EXTENDER_BUNDLE_VERSION, extenderBundle.getVersion());
+        return props;
+    }
+}
diff --git a/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.properties b/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.properties
index 67b0a34..3df364d 100644
--- a/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.properties
+++ b/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -122,4 +122,10 @@
 org.apache.felix.http.context_path.description = The Servlet Context Path \
  to use for the Http Service. If this property is not configured it \
  defaults to "/". This must be a valid path starting with a slash and not \
- ending with a slash (unless it is the root context).
\ No newline at end of file
+ ending with a slash (unless it is the root context).
+
+org.apache.felix.http.path_exclusions.name = Path Exclusions
+org.apache.felix.http.path_exclusions.description = Contains a list of \
+ context path prefixes. If a Web Application Bundle is started with a \
+ context path matching any of these prefixes, it will not be deployed \
+ in the servlet container.
\ No newline at end of file
diff --git a/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.xml b/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.xml
index b418876..227a9f6 100644
--- a/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.xml
+++ b/http/jetty/src/main/resources/OSGI-INF/metatype/metatype.xml
@@ -44,6 +44,7 @@
         <AD id="org.apache.felix.http.jetty.requestBufferSize" type="Integer" default="8192" name="%org.apache.felix.http.jetty.requestBufferSize.name" description="%org.apache.felix.http.jetty.requestBufferSize.description"/>
         <AD id="org.apache.felix.http.jetty.responseBufferSize" type="Integer" default="24576" name="%org.apache.felix.http.jetty.responseBufferSize.name" description="%org.apache.felix.http.jetty.responseBufferSize.description"/>
         <AD id="org.apache.felix.http.debug" type="Boolean" default="false" name="%org.apache.felix.http.debug.name" description="%org.apache.felix.http.debug.description"/>
+        <AD id="org.apache.felix.http.path_exclusions" type="String" default="/system" cardinality="2147483647" name="%org.apache.felix.http.path_exclusions.name" description="%org.apache.felix.http.path_exclusions.description"/>
     </OCD>
     <Designate pid="org.apache.felix.http">
         <Object ocdref="org.apache.felix.http"/>