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"/>