FELIX-3226 Improve HttpContext whiteboard support
  - export constants
  - allow for sharing HttpContext services across bundles
    (must be declared)
  - delay Servlet/Filter registration as long as HttpContext
    service referred to is missing
  - add unit tests
FELIX-2882 service instance as key
  - replace the service instances by the service reference
    as the key of the mappings

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1210612 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/HttpWhiteboardConstants.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/HttpWhiteboardConstants.java
new file mode 100644
index 0000000..00521f7
--- /dev/null
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/HttpWhiteboardConstants.java
@@ -0,0 +1,128 @@
+/*
+ * 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.whiteboard;
+
+/**
+ * The <code>HttpWhiteboardConstants</code> defines constants for values
+ * used by the Http Whiteboard registration support.
+ *
+ * @since Http Whiteboard Bundle 2.3.0
+ */
+public class HttpWhiteboardConstants
+{
+
+    /**
+     * The service registration property indicating the name of a
+     * <code>HttpContext</code> service.
+     * <p>
+     * If the property is set to a non-empty string for an
+     * <code>HttpContext</code> service it indicates the name by which it may be
+     * referred to by <code>Servlet</code> and <code>Filter</code> services.
+     * This is also a required registration property for
+     * <code>HttpService</code> services to be accepted by the Http Whiteboard
+     * registration.
+     * <p>
+     * If the property is set for a <code>Servlet</code> or <code>Filter</code>
+     * services it indicates the name of a registered <code>HttpContext</code>
+     * which is to be used for the registration with the Http Service. If the
+     * property is not set for a <code>Servlet</code> or <code>Filter</code>
+     * services or its value is the empty string, a default HttpContext is used
+     * which does no security handling and has no MIME type support and which
+     * returns resources from the servlet's or the filter's bundle.
+     * <p>
+     * The value of this service registration property is a single string.
+     */
+    public static final String CONTEXT_ID = "contextId";
+
+    /**
+     * The service registration property indicating whether a
+     * <code>HttpContext</code> service registered with the {@link #CONTEXT_ID}
+     * service registration
+     * property is shared across bundles or not. By default
+     * <code>HttpContext</code> services are only available to
+     * <code>Servlet</code> and <code>Filter</code> services registered by the
+     * same bundle.
+     * <p>
+     * If this property is set to <code>true</code> for <code>HttpContext</code>
+     * service, it may be referred to by <code>Servlet</code> or
+     * <code>Filter</code> services from different bundles.
+     * <p>
+     * <b>Recommendation:</b> Shared <code>HttpContext</code> services should
+     * either not implement the <code>getResource</code> at all or be registered
+     * as service factories to ensure no access to foreign bundle resources is
+     * not allowed through this backdoor.
+     * <p>
+     * The value of this service registration is a single boolean or string.
+     * Only if the boolean value is <code>true</code> (either by
+     * <code>Boolean.booleanValue()</code> or by
+     * <code>Boolean.valueOf(String)</code>) will the <code>HttpContext</code>
+     * be shared.
+     */
+    public static final String CONTEXT_SHARED = "context.shared";
+
+    /**
+     * The service registration property indicating the registration alias
+     * for a <code>Servlet</code> service. This value is used as the
+     * alias parameter for the <code>HttpService.registerServlet</code> call.
+     * <p>
+     * A <code>Servlet</code> service registered with this service property may
+     * also provide a {@link #CONTEXT_ID} property which referrs to a
+     * <code>HttpContext</code> service. If such a service is not registered
+     * (yet), the servlet will not be registered with the Http Service. Once the
+     * <code>HttpContext</code> service becomes available, the servlet is
+     * registered.
+     * <p>
+     * The value of this service registration property is a single string
+     * starting with a slash.
+     */
+    public static final String ALIAS = "alias";
+
+    /**
+     * The service registration property indicating the URL patter
+     * for a <code>Filter</code> service. This value is used as the
+     * pattern parameter for the <code>ExtHttpService.registerFilter</code>
+     * call.
+     * <p>
+     * A <code>Filter</code> service registered with this service property may
+     * also provide a {@link #CONTEXT_ID} property which referrs to a
+     * <code>HttpContext</code> service. If such a service is not registered
+     * (yet), the filter will not be registered with the Http Service. Once the
+     * <code>HttpContext</code> service becomes available, the filter is
+     * registered.
+     * <p>
+     * The value of this service registration property is a single string being
+     * a regular expression.
+     * <p>
+     * <b>Note:</b> <code>Filter</code> services are only supported if the Http
+     * Service implements the
+     * <code>org.apache.felix.http.api.ExtHttpService</code> interface.
+     */
+    public static final String PATTERN = "pattern";
+
+    /**
+     * Prefix for service registration properties being used as init parameters
+     * for the <code>Servlet</code> and <code>Filter</code> initialization.
+     */
+    public static final String INIT_PREFIX = "init.";
+
+    // no instances
+    private HttpWhiteboardConstants()
+    {
+    }
+}
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/WhiteboardActivator.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/WhiteboardActivator.java
index e6dd0d4..7ab9ecb 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/WhiteboardActivator.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/WhiteboardActivator.java
@@ -23,7 +23,6 @@
 import org.apache.felix.http.whiteboard.internal.tracker.HttpContextTracker;
 import org.apache.felix.http.whiteboard.internal.tracker.ServletTracker;
 import org.apache.felix.http.whiteboard.internal.tracker.HttpServiceTracker;
-import org.apache.felix.http.whiteboard.internal.manager.ExtenderManagerImpl;
 import org.apache.felix.http.whiteboard.internal.manager.ExtenderManager;
 import org.apache.felix.http.whiteboard.internal.manager.HttpWhiteboardWebConsolePlugin;
 import org.apache.felix.http.base.internal.AbstractActivator;
@@ -46,13 +45,13 @@
     protected void doStart()
         throws Exception
     {
-        this.manager = new ExtenderManagerImpl();
+        this.manager = new ExtenderManager();
         addTracker(new HttpContextTracker(getBundleContext(), this.manager));
         addTracker(new FilterTracker(getBundleContext(), this.manager));
         addTracker(new ServletTracker(getBundleContext(), this.manager));
         addTracker(new HttpServiceTracker(getBundleContext(), this.manager));
 
-        HttpWhiteboardWebConsolePlugin plugin = new HttpWhiteboardWebConsolePlugin((ExtenderManagerImpl) this.manager);
+        HttpWhiteboardWebConsolePlugin plugin = new HttpWhiteboardWebConsolePlugin(this.manager);
         Hashtable<String, Object> props = new Hashtable<String, Object>();
         props.put("felix.webconsole.label", plugin.getLabel());
         props.put("felix.webconsole.title", plugin.getTitle());
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/AbstractMapping.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/AbstractMapping.java
index e503ff2..0ad4d97 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/AbstractMapping.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/AbstractMapping.java
@@ -16,19 +16,35 @@
  */
 package org.apache.felix.http.whiteboard.internal.manager;
 
-import org.osgi.service.http.HttpContext;
-import org.osgi.service.http.HttpService;
 import java.util.Hashtable;
 
-public abstract class AbstractMapping
-{
-    private final HttpContext context;
-    private final Hashtable<String, String> initParams;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
 
-    public AbstractMapping(HttpContext context)
+abstract class AbstractMapping
+{
+    private final Bundle bundle;
+    private HttpContext context;
+    private final Hashtable<String, String> initParams;
+    private boolean registered;
+
+    protected AbstractMapping(final Bundle bundle)
+    {
+        this.bundle = bundle;
+        this.context = null;
+        this.initParams = new Hashtable<String, String>();
+        this.registered = false;
+    }
+
+    public Bundle getBundle()
+    {
+        return bundle;
+    }
+
+    public void setContext(HttpContext context)
     {
         this.context = context;
-        this.initParams = new Hashtable<String, String>();
     }
 
     public final HttpContext getContext()
@@ -41,6 +57,16 @@
         return this.initParams;
     }
 
+    boolean isRegistered()
+    {
+        return registered;
+    }
+
+    void setRegistered(boolean registered)
+    {
+        this.registered = registered;
+    }
+
     public abstract void register(HttpService httpService);
 
     public abstract void unregister(HttpService httpService);
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManager.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManager.java
index 0e0378e..7adbbcf 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManager.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManager.java
@@ -16,30 +16,290 @@
  */
 package org.apache.felix.http.whiteboard.internal.manager;
 
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.Filter;
+import javax.servlet.Servlet;
+
+import org.apache.felix.http.api.ExtHttpService;
+import org.apache.felix.http.base.internal.logger.SystemLogger;
+import org.apache.felix.http.whiteboard.HttpWhiteboardConstants;
+import org.apache.felix.http.whiteboard.internal.manager.HttpContextManager.HttpContextHolder;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
 import org.osgi.service.http.HttpContext;
 import org.osgi.service.http.HttpService;
-import org.osgi.service.log.LogService;
-import org.osgi.framework.ServiceReference;
-import javax.servlet.Servlet;
-import javax.servlet.Filter;
 
-public interface ExtenderManager
+public final class ExtenderManager
 {
-    public void add(HttpContext service, ServiceReference ref);
+    private HttpService httpService;
+    private final HashMap<ServiceReference, AbstractMapping> mapping;
+    private final HttpContextManager contextManager;
 
-    public void remove(HttpContext service);
+    public ExtenderManager()
+    {
+        this.mapping = new HashMap<ServiceReference, AbstractMapping>();
+        this.contextManager = new HttpContextManager();
+    }
 
-    public void add(Filter service, ServiceReference ref);
+    static boolean isEmpty(final String value)
+    {
+        return value == null || value.length() == 0;
+    }
 
-    public void remove(Filter service);
+    private String getStringProperty(ServiceReference ref, String key)
+    {
+        Object value = ref.getProperty(key);
+        return (value instanceof String) ? (String)value : null;
+    }
 
-    public void add(Servlet service, ServiceReference ref);
+    private boolean getBooleanProperty(ServiceReference ref, String key)
+    {
+        Object value = ref.getProperty(key);
+        if (value instanceof String)
+        {
+            return Boolean.valueOf((String) value);
+        }
+        else if (value instanceof Boolean)
+        {
+            return ((Boolean) value).booleanValue();
+        }
+        return false;
+    }
 
-    public void remove(Servlet service);
+    private int getIntProperty(ServiceReference ref, String key, int defValue)
+    {
+        Object value = ref.getProperty(key);
+        if (value == null) {
+            return defValue;
+        }
 
-    public void setHttpService(HttpService service);
+        try {
+            return Integer.parseInt(value.toString());
+        } catch (Exception e) {
+            return defValue;
+        }
+    }
 
-    public void unsetHttpService();
+    private void addInitParams(ServiceReference ref, AbstractMapping mapping)
+    {
+        for (String key : ref.getPropertyKeys()) {
+            if (key.startsWith(HttpWhiteboardConstants.INIT_PREFIX)) {
+                String paramKey = key.substring(HttpWhiteboardConstants.INIT_PREFIX.length());
+                String paramValue = getStringProperty(ref, key);
 
-    public void unregisterAll();
+                if (paramValue != null) {
+                    mapping.getInitParams().put(paramKey, paramValue);
+                }
+            }
+        }
+    }
+
+    public void add(HttpContext service, ServiceReference ref)
+    {
+        String contextId = getStringProperty(ref, HttpWhiteboardConstants.CONTEXT_ID);
+        if (!isEmpty(contextId))
+        {
+            boolean shared = getBooleanProperty(ref, HttpWhiteboardConstants.CONTEXT_SHARED);
+            Bundle bundle = shared ? null : ref.getBundle();
+            Collection<AbstractMapping> mappings = this.contextManager.addHttpContext(bundle, contextId, service);
+            for (AbstractMapping mapping : mappings)
+            {
+                registerMapping(mapping);
+            }
+        }
+        else
+        {
+            SystemLogger.debug("Ignoring HttpContext Service " + ref + ", " + HttpWhiteboardConstants.CONTEXT_ID
+                + " is missing or empty");
+        }
+    }
+
+    public void remove(HttpContext service)
+    {
+        Collection<AbstractMapping> mappings = this.contextManager.removeHttpContext(service);
+        if (mappings != null)
+        {
+            for (AbstractMapping mapping : mappings)
+            {
+                unregisterMapping(mapping);
+            }
+        }
+    }
+
+    private void getHttpContext(AbstractMapping mapping, ServiceReference ref)
+    {
+        Bundle bundle = ref.getBundle();
+        String contextId = getStringProperty(ref, HttpWhiteboardConstants.CONTEXT_ID);
+        this.contextManager.getHttpContext(bundle, contextId, mapping);
+    }
+
+    private void ungetHttpContext(AbstractMapping mapping, ServiceReference ref)
+    {
+        Bundle bundle = ref.getBundle();
+        String contextId = getStringProperty(ref, HttpWhiteboardConstants.CONTEXT_ID);
+        this.contextManager.ungetHttpContext(bundle, contextId, mapping);
+    }
+
+    public void add(Filter service, ServiceReference ref)
+    {
+        int ranking = getIntProperty(ref, Constants.SERVICE_RANKING, 0);
+        String pattern = getStringProperty(ref, HttpWhiteboardConstants.PATTERN);
+
+        if (isEmpty(pattern)) {
+            SystemLogger.debug("Ignoring Filter Service " + ref + ", " + HttpWhiteboardConstants.PATTERN
+                + " is missing or empty");
+            return;
+        }
+
+        FilterMapping mapping = new FilterMapping(ref.getBundle(), service, pattern, ranking);
+        getHttpContext(mapping, ref);
+        addInitParams(ref, mapping);
+        addMapping(ref, mapping);
+    }
+
+    public void add(Servlet service, ServiceReference ref)
+    {
+        String alias = getStringProperty(ref, HttpWhiteboardConstants.ALIAS);
+        if (isEmpty(alias))
+        {
+            SystemLogger.debug("Ignoring Servlet Service " + ref + ", " + HttpWhiteboardConstants.ALIAS
+                + " is missing or empty");
+            return;
+        }
+
+        ServletMapping mapping = new ServletMapping(ref.getBundle(), service, alias);
+        getHttpContext(mapping, ref);
+        addInitParams(ref, mapping);
+        addMapping(ref, mapping);
+    }
+
+    public void remove(ServiceReference ref)
+    {
+        removeMapping(ref);
+    }
+
+    public synchronized void setHttpService(HttpService service)
+    {
+        this.httpService = service;
+        if (this.httpService instanceof ExtHttpService) {
+            SystemLogger.info("Detected extended HttpService. Filters enabled.");
+        } else {
+            SystemLogger.info("Detected standard HttpService. Filters disabled.");
+        }
+
+        registerAll();
+    }
+
+    public synchronized void unsetHttpService()
+    {
+        unregisterAll();
+        this.httpService = null;
+    }
+
+    public synchronized void unregisterAll()
+    {
+    	AbstractMapping[] mappings = null;
+    	HttpService service;
+    	synchronized (this) {
+			service = this.httpService;
+			if (service != null) {
+    			Collection<AbstractMapping> values = this.mapping.values();
+    			mappings = values.toArray(new AbstractMapping[values.size()]);
+    		}
+    	}
+    	if (mappings != null) {
+    		for (AbstractMapping mapping : mappings) {
+    			mapping.unregister(service);
+    		}
+    	}
+    }
+
+    private synchronized void registerAll()
+    {
+    	AbstractMapping[] mappings = null;
+    	HttpService service;
+    	synchronized (this) {
+			service = this.httpService;
+			if (service != null) {
+    			Collection<AbstractMapping> values = this.mapping.values();
+    			mappings = values.toArray(new AbstractMapping[values.size()]);
+    		}
+    	}
+    	if (mappings != null) {
+    		for (AbstractMapping mapping : mappings) {
+    			mapping.register(service);
+    		}
+    	}
+    }
+
+    private synchronized void addMapping(ServiceReference ref, AbstractMapping mapping)
+    {
+        this.mapping.put(ref, mapping);
+        this.registerMapping(mapping);
+    }
+
+    private synchronized void removeMapping(ServiceReference ref)
+    {
+        AbstractMapping mapping = this.mapping.remove(ref);
+        if (mapping != null)
+        {
+            ungetHttpContext(mapping, ref);
+            unregisterMapping(mapping);
+        }
+    }
+
+    private void registerMapping(AbstractMapping mapping)
+    {
+        HttpService httpService = this.httpService;
+        if (httpService != null)
+        {
+            mapping.register(httpService);
+        }
+    }
+
+    private void unregisterMapping(AbstractMapping mapping)
+    {
+        HttpService httpService = this.httpService;
+        if (httpService != null)
+        {
+            mapping.unregister(httpService);
+        }
+    }
+
+    /**
+     * Returns
+     * {@link org.apache.felix.http.whiteboard.internal.manager.HttpContextManager.HttpContextHolder}
+     * instances of HttpContext services.
+     *
+     * @return
+     */
+    Map<String, HttpContextHolder> getHttpContexts()
+    {
+        return this.contextManager.getHttpContexts();
+    }
+
+    /**
+     * Returns {@link AbstractMapping} instances for which there is no
+     * registered HttpContext as desired by the context ID.
+     */
+    Map<String, Set<AbstractMapping>> getOrphanMappings()
+    {
+        return this.contextManager.getOrphanMappings();
+    }
+
+    /**
+     * Returns mappings indexed by there owning OSGi service.
+     */
+    Map<Object, AbstractMapping> getMappings()
+    {
+        synchronized (this)
+        {
+            return new HashMap<Object, AbstractMapping>(this.mapping);
+        }
+    }
 }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManagerImpl.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManagerImpl.java
deleted file mode 100644
index 909ab85..0000000
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManagerImpl.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * 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.whiteboard.internal.manager;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.servlet.Filter;
-import javax.servlet.Servlet;
-
-import org.apache.felix.http.api.ExtHttpService;
-import org.apache.felix.http.base.internal.logger.SystemLogger;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.Constants;
-import org.osgi.framework.ServiceReference;
-import org.osgi.service.http.HttpContext;
-import org.osgi.service.http.HttpService;
-
-public final class ExtenderManagerImpl
-    implements ExtenderManager
-{
-    private final static String CONTEXT_ID_KEY = "contextId";
-    private final static String PATTERN_KEY = "pattern";
-    private final static String ALIAS_KEY = "alias";
-    private final static String INIT_KEY_PREFIX = "init.";
-
-    private HttpService httpService;
-    private final HashMap<Object, AbstractMapping> mapping;
-    private final HttpContextManager contextManager;
-
-    public ExtenderManagerImpl()
-    {
-        this.mapping = new HashMap<Object, AbstractMapping>();
-        this.contextManager = new HttpContextManager();
-    }
-
-    private String getStringProperty(ServiceReference ref, String key)
-    {
-        Object value = ref.getProperty(key);
-        return (value instanceof String) ? (String)value : null;
-    }
-
-    private int getIntProperty(ServiceReference ref, String key, int defValue)
-    {
-        Object value = ref.getProperty(key);
-        if (value == null) {
-            return defValue;
-        }
-
-        try {
-            return Integer.parseInt(value.toString());
-        } catch (Exception e) {
-            return defValue;
-        }
-    }
-
-    private void addInitParams(ServiceReference ref, AbstractMapping mapping)
-    {
-        for (String key : ref.getPropertyKeys()) {
-            if (key.startsWith(INIT_KEY_PREFIX)) {
-                String paramKey = key.substring(INIT_KEY_PREFIX.length());
-                String paramValue = getStringProperty(ref, key);
-
-                if (paramValue != null) {
-                    mapping.getInitParams().put(paramKey, paramValue);
-                }
-            }
-        }
-    }
-
-    public void add(HttpContext service, ServiceReference ref)
-    {
-        Bundle bundle = ref.getBundle();
-        String contextId = getStringProperty(ref, CONTEXT_ID_KEY);
-        if (contextId != null) {
-            this.contextManager.addHttpContext(bundle, contextId, service);
-        }
-    }
-
-    public void remove(HttpContext service)
-    {
-        this.contextManager.removeHttpContext(service);
-    }
-
-    private HttpContext getHttpContext(ServiceReference ref)
-    {
-        Bundle bundle = ref.getBundle();
-        String contextId = getStringProperty(ref, CONTEXT_ID_KEY);
-
-        if (contextId != null) {
-            return this.contextManager.getHttpContext(bundle, contextId);
-        } else {
-            return new DefaultHttpContext(bundle);
-        }
-    }
-
-    public void add(Filter service, ServiceReference ref)
-    {
-        int ranking = getIntProperty(ref, Constants.SERVICE_RANKING, 0);
-        String pattern = getStringProperty(ref, PATTERN_KEY);
-
-        if (pattern == null) {
-            return;
-        }
-
-        FilterMapping mapping = new FilterMapping(getHttpContext(ref), service, pattern, ranking);
-        addInitParams(ref, mapping);
-        addMapping(service, mapping);
-    }
-
-    public void remove(Filter service)
-    {
-        removeMapping(service);
-    }
-
-    public void add(Servlet service, ServiceReference ref)
-    {
-        String alias = getStringProperty(ref, ALIAS_KEY);
-        if (alias == null) {
-            return;
-        }
-
-        ServletMapping mapping = new ServletMapping(getHttpContext(ref), service, alias);
-        addInitParams(ref, mapping);
-        addMapping(service, mapping);
-    }
-
-    public void remove(Servlet service)
-    {
-        removeMapping(service);
-    }
-
-    public synchronized void setHttpService(HttpService service)
-    {
-        this.httpService = service;
-        if (this.httpService instanceof ExtHttpService) {
-            SystemLogger.info("Detected extended HttpService. Filters enabled.");
-        } else {
-            SystemLogger.info("Detected standard HttpService. Filters disabled.");
-        }
-
-        registerAll();
-    }
-
-    public synchronized void unsetHttpService()
-    {
-        unregisterAll();
-        this.httpService = null;
-    }
-
-    public synchronized void unregisterAll()
-    {
-    	AbstractMapping[] mappings = null;
-    	HttpService service;
-    	synchronized (this) {
-			service = this.httpService;
-			if (service != null) {
-    			Collection<AbstractMapping> values = this.mapping.values();
-    			mappings = values.toArray(new AbstractMapping[values.size()]);
-    		}
-    	}
-    	if (mappings != null) {
-    		for (AbstractMapping mapping : mappings) {
-    			mapping.unregister(service);
-    		}
-    	}
-    }
-
-    private synchronized void registerAll()
-    {
-    	AbstractMapping[] mappings = null;
-    	HttpService service;
-    	synchronized (this) {
-			service = this.httpService;
-			if (service != null) {
-    			Collection<AbstractMapping> values = this.mapping.values();
-    			mappings = values.toArray(new AbstractMapping[values.size()]);
-    		}
-    	}
-    	if (mappings != null) {
-    		for (AbstractMapping mapping : mappings) {
-    			mapping.register(service);
-    		}
-    	}
-    }
-
-    private synchronized void addMapping(Object key, AbstractMapping mapping)
-    {
-        this.mapping.put(key, mapping);
-        if (this.httpService != null) {
-            mapping.register(this.httpService);
-        }
-    }
-
-    private synchronized void removeMapping(Object key)
-    {
-        AbstractMapping mapping = this.mapping.remove(key);
-        if ((mapping != null) && (this.httpService != null)) {
-            mapping.unregister(this.httpService);
-        }
-    }
-
-    Map<String, HttpContext> getHttpContexts() {
-        return this.contextManager.getHttpContexts();
-    }
-
-    Map<Object, AbstractMapping> getMappings()
-    {
-        synchronized (this)
-        {
-            return new HashMap<Object, AbstractMapping>(this.mapping);
-        }
-    }
-}
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/FilterMapping.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/FilterMapping.java
index 275aa4f..961dcc5 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/FilterMapping.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/FilterMapping.java
@@ -16,12 +16,12 @@
  */
 package org.apache.felix.http.whiteboard.internal.manager;
 
-import org.osgi.service.http.HttpService;
-import org.osgi.service.http.HttpContext;
+import javax.servlet.Filter;
+
 import org.apache.felix.http.api.ExtHttpService;
 import org.apache.felix.http.base.internal.logger.SystemLogger;
-
-import javax.servlet.Filter;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpService;
 
 public final class FilterMapping
     extends AbstractMapping
@@ -30,9 +30,9 @@
     private final int ranking;
     private final String pattern;
 
-    public FilterMapping(HttpContext context, Filter filter, String pattern, int ranking)
+    public FilterMapping(Bundle bundle, Filter filter, String pattern, int ranking)
     {
-        super(context);
+        super(bundle);
         this.filter = filter;
         this.pattern = pattern;
         this.ranking = ranking;
@@ -62,17 +62,26 @@
 
     private void register(ExtHttpService httpService)
     {
-        try {
-            httpService.registerFilter(this.filter, this.pattern, getInitParams(), ranking, getContext());
-        } catch (Exception e) {
-            SystemLogger.error("Failed to register filter", e);
+        if (!this.isRegistered() && getContext() != null)
+        {
+            try
+            {
+                httpService.registerFilter(this.filter, this.pattern, getInitParams(), ranking, getContext());
+                setRegistered(true);
+            }
+            catch (Exception e)
+            {
+                SystemLogger.error("Failed to register filter", e);
+            }
         }
     }
 
     public void unregister(HttpService httpService)
     {
-        if (httpService instanceof ExtHttpService) {
-            unregister((ExtHttpService)httpService);
+        if (httpService instanceof ExtHttpService && this.isRegistered())
+        {
+            unregister((ExtHttpService) httpService);
+            setRegistered(false);
         }
     }
 
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManager.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManager.java
index 58d4f74..34b5fb2 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManager.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManager.java
@@ -1,12 +1,12 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
+ * 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
+ * the License. You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -19,59 +19,248 @@
 import org.osgi.framework.Bundle;
 import org.osgi.service.http.HttpContext;
 import org.apache.felix.http.base.internal.logger.SystemLogger;
+
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 
 public final class HttpContextManager
 {
-    private final HashMap<String, HttpContext> idMap;
+    /**
+     * HttpContextHolders indexed by context ID fully configured
+     * with an HttpContext and optional servlets and filters.
+     * <p>
+     * The context ID either includes the bundle ID as the first part of the
+     * name, such as <i>123-sample.context</i> in the case of non-shared
+     * contexts. IDs of shared contexts are prefixed with the fixed string
+     * <code>shared</code> to not mix them with per-bundle contexts.
+     */
+    private final HashMap<String, HttpContextHolder> idMap;
+
+    /**
+     * Reverse mapping of HttpContext services to the context ID with
+     * which they are registered.
+     */
     private final HashMap<HttpContext, String> contextMap;
 
+    /**
+     * Map of servlets and filters registered referring to unregistered
+     * contexts as of yet.
+     */
+    private final HashMap<String, Set<AbstractMapping>> orphanMappings;
+
     public HttpContextManager()
     {
-        this.idMap = new HashMap<String, HttpContext>();
+        this.idMap = new HashMap<String, HttpContextHolder>();
         this.contextMap = new HashMap<HttpContext, String>();
+        this.orphanMappings = new HashMap<String, Set<AbstractMapping>>();
     }
 
-    private String createId(Bundle bundle, String contextId)
+    private static String createId(Bundle bundle, String contextId)
     {
-        return bundle.getBundleId() + "-" + contextId;
-    }
-
-    public synchronized HttpContext getHttpContext(Bundle bundle, String contextId)
-    {
-        String id = createId(bundle, contextId);
-        HttpContext context = this.idMap.get(id);
-
-        if (context == null) {
-            context = new DefaultHttpContext(bundle);
-            this.idMap.put(id, context);
-            this.contextMap.put(context, id);
-            SystemLogger.debug("Added context with id [" + contextId + "]");
-        } else {
-            SystemLogger.debug("Reusing context with id [" + contextId + "]");
+        if (bundle != null)
+        {
+            return bundle.getBundleId() + "-" + ((contextId == null) ? "" : contextId);
         }
 
-        return context;
+        return createId(contextId);
     }
 
-    public synchronized void removeHttpContext(HttpContext context)
+    private static String createId(String contextId)
+    {
+        return "shared-" + ((contextId == null) ? "" : contextId);
+    }
+
+    private static String getContextId(String id)
+    {
+        final int dash = id.indexOf('-');
+        return (dash < 0) ? id : id.substring(dash + 1);
+    }
+
+    public synchronized HttpContext getHttpContext(Bundle bundle, String contextId, AbstractMapping mapping)
+    {
+        // per-bundle context
+        String id = createId(bundle, contextId);
+        HttpContextHolder holder = this.idMap.get(id);
+
+        // shared context
+        if (holder == null)
+        {
+            id = createId(contextId);
+            holder = this.idMap.get(id);
+        }
+
+        // no context yet, put the mapping on hold
+        if (holder == null)
+        {
+
+            // care for default context if no context ID
+            if (ExtenderManager.isEmpty(contextId))
+            {
+                addHttpContext(bundle, "", new DefaultHttpContext(bundle));
+                return getHttpContext(bundle, "", mapping);
+            }
+
+            // otherwise context is not here yet
+            Set<AbstractMapping> orphaned = this.orphanMappings.get(contextId);
+            if (orphaned == null)
+            {
+                orphaned = new HashSet<AbstractMapping>();
+                this.orphanMappings.put(contextId, orphaned);
+            }
+            SystemLogger.debug("Holding off mapping with unregistered context with id [" + contextId + "]");
+            orphaned.add(mapping);
+            return null;
+        }
+
+        // otherwise use the context
+        SystemLogger.debug("Reusing context with id [" + contextId + "]");
+        holder.addMapping(mapping);
+        return holder.getContext();
+    }
+
+    public synchronized void ungetHttpContext(Bundle bundle, String contextId, AbstractMapping mapping)
+    {
+        // per-bundle context
+        String id = createId(bundle, contextId);
+        HttpContextHolder context = this.idMap.get(id);
+
+        // shared context
+        if (context == null)
+        {
+            id = createId(contextId);
+            context = this.idMap.get(id);
+        }
+
+        // remove the mapping if there is a mapped context
+        if (context != null)
+        {
+            context.removeMapping(mapping);
+        }
+        else
+        {
+            Set<AbstractMapping> orphans = this.orphanMappings.get(contextId);
+            if (orphans != null)
+            {
+                orphans.remove(mapping);
+                if (orphans.isEmpty())
+                {
+                    this.orphanMappings.remove(contextId);
+                }
+            }
+
+            // it is not expected but make sure there is no reference
+            mapping.setContext(null);
+        }
+    }
+
+    public synchronized Collection<AbstractMapping> addHttpContext(Bundle bundle, String contextId, HttpContext context)
+    {
+        String id = createId(bundle, contextId);
+        HttpContextHolder holder = new HttpContextHolder(context);
+
+        Set<AbstractMapping> orphans = this.orphanMappings.remove(contextId);
+        if (orphans != null)
+        {
+            for (Iterator<AbstractMapping> mi = orphans.iterator(); mi.hasNext();)
+            {
+                AbstractMapping mapping = mi.next();
+                if (bundle == null || bundle.equals(mapping.getBundle()))
+                {
+                    holder.addMapping(mapping);
+                    mi.remove();
+                }
+            }
+
+            // put any remaining orphans back
+            if (!orphans.isEmpty())
+            {
+                this.orphanMappings.put(contextId, orphans);
+            }
+        }
+
+        this.idMap.put(id, holder);
+        this.contextMap.put(context, id);
+
+        return holder.getMappings();
+    }
+
+    public synchronized Collection<AbstractMapping> removeHttpContext(HttpContext context)
     {
         String id = this.contextMap.remove(context);
-        if (id != null) {
-            this.idMap.remove(id);
+        if (id != null)
+        {
+            HttpContextHolder holder = this.idMap.remove(id);
+            if (holder != null)
+            {
+                Set<AbstractMapping> mappings = holder.getMappings();
+                if (mappings != null && !mappings.isEmpty())
+                {
+                    // keep the orphans around
+                    final String contextId = getContextId(id);
+                    Set<AbstractMapping> orphans = this.orphanMappings.get(contextId);
+                    if (orphans == null)
+                    {
+                        orphans = new HashSet<AbstractMapping>();
+                        this.orphanMappings.put(getContextId(id), orphans);
+                    }
+
+                    for (AbstractMapping mapping : mappings)
+                    {
+                        mapping.setContext(null);
+                        orphans.add(mapping);
+                    }
+                }
+                return mappings;
+            }
         }
+        return null;
     }
 
-    public synchronized void addHttpContext(Bundle bundle, String contextId, HttpContext context)
+    synchronized Map<String, HttpContextHolder> getHttpContexts()
     {
-        String id = createId(bundle, contextId);
-        this.idMap.put(id, context);
-        this.contextMap.put(context, id);
+        return new HashMap<String, HttpContextHolder>(this.idMap);
     }
 
-    public synchronized Map<String, HttpContext> getHttpContexts()
+    synchronized Map<String, Set<AbstractMapping>> getOrphanMappings()
     {
-        return new HashMap<String, HttpContext>(this.idMap);
+        return new HashMap<String, Set<AbstractMapping>>(this.orphanMappings);
+    }
+
+    static class HttpContextHolder
+    {
+        private final HttpContext context;
+        private final Set<AbstractMapping> mappings;
+
+        HttpContextHolder(final HttpContext context)
+        {
+            this.context = context;
+            this.mappings = new HashSet<AbstractMapping>();
+        }
+
+        public HttpContext getContext()
+        {
+            return context;
+        }
+
+        void addMapping(AbstractMapping mapping)
+        {
+            this.mappings.add(mapping);
+            mapping.setContext(this.getContext());
+        }
+
+        void removeMapping(AbstractMapping mapping)
+        {
+            mapping.setContext(null);
+            this.mappings.remove(mapping);
+        }
+
+        public Set<AbstractMapping> getMappings()
+        {
+            return mappings;
+        }
     }
 }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpWhiteboardWebConsolePlugin.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpWhiteboardWebConsolePlugin.java
index 408f7e3..b261177 100755
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpWhiteboardWebConsolePlugin.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/HttpWhiteboardWebConsolePlugin.java
@@ -27,13 +27,13 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.osgi.service.http.HttpContext;
+import org.apache.felix.http.whiteboard.internal.manager.HttpContextManager.HttpContextHolder;
 
 @SuppressWarnings("serial")
 public class HttpWhiteboardWebConsolePlugin extends HttpServlet
 {
 
-    private final ExtenderManagerImpl extMgr;
+    private final ExtenderManager extMgr;
 
     public String getLabel()
     {
@@ -45,7 +45,7 @@
         return "Http Whiteboard";
     }
 
-    public HttpWhiteboardWebConsolePlugin(final ExtenderManagerImpl extMgr)
+    public HttpWhiteboardWebConsolePlugin(final ExtenderManager extMgr)
     {
         this.extMgr = extMgr;
     }
@@ -92,12 +92,12 @@
         pw.println("<th class='content' colspan='3'>HttpContext</td>");
         pw.println("</tr>");
 
-        final Map<String, HttpContext> contexts = extMgr.getHttpContexts();
-        for (Map.Entry<String, HttpContext> handler : contexts.entrySet())
+        final Map<String, HttpContextHolder> contexts = extMgr.getHttpContexts();
+        for (Map.Entry<String, HttpContextHolder> handler : contexts.entrySet())
         {
             pw.println("<tr class='content'>");
             pw.println("<td class='content'>" + handler.getKey() + "</td>");
-            pw.println("<td class='content' colspan='3'>" + handler.getValue() + "</td>");
+            pw.println("<td class='content' colspan='3'>" + handler.getValue().getContext() + "</td>");
             pw.println("</tr>");
         }
     }
@@ -167,10 +167,10 @@
     private void printHttpContextServicesTxt(PrintWriter pw)
     {
         pw.println("Registered HttpContext Services");
-        final Map<String, HttpContext> contexts = extMgr.getHttpContexts();
-        for (Map.Entry<String, HttpContext> handler : contexts.entrySet())
+        final Map<String, HttpContextHolder> contexts = extMgr.getHttpContexts();
+        for (Map.Entry<String, HttpContextHolder> handler : contexts.entrySet())
         {
-            pw.println("  " + handler.getKey() + " ==> " + handler.getValue() + "</td>");
+            pw.println("  " + handler.getKey() + " ==> " + handler.getValue().getContext() + "</td>");
         }
         pw.println();
     }
@@ -183,8 +183,8 @@
             if (handler.getValue() instanceof ServletMapping)
             {
                 ServletMapping sm = (ServletMapping) handler.getValue();
-                pw.println("  " + sm.getAlias() + " ==> " + sm.getServlet() + " (" + sm.getInitParams() + ", "
-                    + sm.getContext() + ")");
+                pw.printf("  %s ==> %s (%s, %s, %s)%n", sm.getAlias(), sm.getServlet(),
+                    sm.isRegistered() ? "registered" : "unregistered", sm.getInitParams(), sm.getContext());
             }
         }
         pw.println();
@@ -198,8 +198,9 @@
             if (handler.getValue() instanceof FilterMapping)
             {
                 FilterMapping fm = (FilterMapping) handler.getValue();
-                pw.println("  " + fm.getPattern() + " ==> " + fm.getFilter() + " (" + fm.getRanking() + ", "
-                    + fm.getInitParams() + ", " + fm.getContext() + ")");
+                pw.printf("  %s ==> %s (%s, %s, %s, %s)%n", fm.getPattern(), fm.getFilter(),
+                    fm.isRegistered() ? "registered" : "unregistered", fm.getRanking(), fm.getInitParams(),
+                    fm.getContext());
             }
         }
         pw.println();
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ServletMapping.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ServletMapping.java
index 362e582..67b8d8c 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ServletMapping.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/manager/ServletMapping.java
@@ -16,21 +16,21 @@
  */
 package org.apache.felix.http.whiteboard.internal.manager;
 
-import org.osgi.service.http.HttpService;
-import org.osgi.service.http.HttpContext;
-import org.apache.felix.http.base.internal.logger.SystemLogger;
-
 import javax.servlet.Servlet;
 
+import org.apache.felix.http.base.internal.logger.SystemLogger;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpService;
+
 public final class ServletMapping
     extends AbstractMapping
 {
     private final Servlet servlet;
     private final String alias;
 
-    public ServletMapping(HttpContext context, Servlet servlet, String alias)
+    public ServletMapping(Bundle bundle, Servlet servlet, String alias)
     {
-        super(context);
+        super(bundle);
         this.servlet = servlet;
         this.alias = alias;
     }
@@ -47,15 +47,26 @@
 
     public void register(HttpService httpService)
     {
-        try {
-            httpService.registerServlet(this.alias, this.servlet, getInitParams(), getContext());
-        } catch (Exception e) {
-            SystemLogger.error("Failed to register servlet", e);
+        if (!this.isRegistered() && getContext() != null)
+        {
+            try
+            {
+                httpService.registerServlet(this.alias, this.servlet, getInitParams(), getContext());
+                this.setRegistered(true);
+            }
+            catch (Exception e)
+            {
+                SystemLogger.error("Failed to register servlet", e);
+            }
         }
     }
 
     public void unregister(HttpService httpService)
     {
-        httpService.unregister(this.alias);
+        if (this.isRegistered())
+        {
+            httpService.unregister(this.alias);
+            this.setRegistered(false);
+        }
     }
 }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/AbstractTracker.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/AbstractTracker.java
index c2590bf..e8a1f5c 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/AbstractTracker.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/AbstractTracker.java
@@ -23,7 +23,7 @@
 public abstract class AbstractTracker<T>
     extends ServiceTracker
 {
-    public AbstractTracker(BundleContext context, Class clz)
+    public AbstractTracker(BundleContext context, Class<T> clz)
     {
         super(context, clz.getName(), null);
     }
@@ -49,12 +49,12 @@
     public final void removedService(ServiceReference ref, Object service)
     {
         super.removedService(ref, service);
-        removed((T)service);
+        removed((T) service, ref);
     }
 
     protected abstract void modified(T service, ServiceReference ref);
 
     protected abstract void added(T service, ServiceReference ref);
 
-    protected abstract void removed(T service);
+    protected abstract void removed(T service, ServiceReference ref);
 }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/FilterTracker.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/FilterTracker.java
index a2640dd..ba4fa58 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/FilterTracker.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/FilterTracker.java
@@ -39,12 +39,12 @@
 
     protected void modified(Filter service, ServiceReference ref)
     {
-        removed(service);
+        removed(service, ref);
         added(service, ref);
     }
 
-    protected void removed(Filter service)
+    protected void removed(Filter service, ServiceReference ref)
     {
-        this.manager.remove(service);
+        this.manager.remove(ref);
     }
 }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpContextTracker.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpContextTracker.java
index 60af219..9238d36 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpContextTracker.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpContextTracker.java
@@ -25,7 +25,7 @@
     extends AbstractTracker<HttpContext>
 {
     private final ExtenderManager manager;
-    
+
     public HttpContextTracker(BundleContext context, ExtenderManager manager)
     {
         super(context, HttpContext.class);
@@ -39,11 +39,11 @@
 
     protected void modified(HttpContext service, ServiceReference ref)
     {
-        removed(service);
+        removed(service, ref);
         added(service, ref);
     }
-    
-    protected void removed(HttpContext service)
+
+    protected void removed(HttpContext service, ServiceReference ref)
     {
         this.manager.remove(service);
     }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpServiceTracker.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpServiceTracker.java
index af61564..4188a7a 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpServiceTracker.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/HttpServiceTracker.java
@@ -42,7 +42,7 @@
         // Do nothing
     }
 
-    protected void removed(HttpService service)
+    protected void removed(HttpService service, ServiceReference ref)
     {
         this.manager.unsetHttpService();
     }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/ServletTracker.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/ServletTracker.java
index 2dcae99..7853c0d 100644
--- a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/ServletTracker.java
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/internal/tracker/ServletTracker.java
@@ -39,12 +39,12 @@
 
     protected void modified(Servlet service, ServiceReference ref)
     {
-        removed(service);
+        removed(service, ref);
         added(service, ref);
     }
-    
-    protected void removed(Servlet service)
+
+    protected void removed(Servlet service, ServiceReference ref)
     {
-        this.manager.remove(service);
+        this.manager.remove(ref);
     }
 }
diff --git a/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/package-info.java b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/package-info.java
new file mode 100644
index 0000000..c213e31
--- /dev/null
+++ b/http/whiteboard/src/main/java/org/apache/felix/http/whiteboard/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+/**
+ * @see: @TODO ref to whiteboard page on felix.apache.org
+ */
+@Version("1.0")
+@Export(optional = "provide:=true")
+package org.apache.felix.http.whiteboard;
+
+import aQute.bnd.annotation.Export;
+import aQute.bnd.annotation.Version;
+
diff --git a/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManagerTest.java b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManagerTest.java
new file mode 100644
index 0000000..a8ad61b
--- /dev/null
+++ b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/ExtenderManagerTest.java
@@ -0,0 +1,876 @@
+/*
+ * 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.whiteboard.internal.manager;
+
+import static org.mockito.Mockito.when;
+
+import java.util.Dictionary;
+
+import javax.servlet.Filter;
+import javax.servlet.Servlet;
+
+import junit.framework.TestCase;
+
+import org.apache.commons.collections.BidiMap;
+import org.apache.commons.collections.bidimap.DualHashBidiMap;
+import org.apache.felix.http.api.ExtHttpService;
+import org.apache.felix.http.whiteboard.HttpWhiteboardConstants;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpContext;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExtenderManagerTest
+{
+
+    private static final String SAMPLE_CONTEXT_ID = "some.context.id";
+
+    private static final long BUNDLE_1_ID = 1L;
+
+    private static final long BUNDLE_2_ID = 2L;
+
+    private static final String SERVLET_1_ALIAS = "/servet1";
+
+    private static final String SERVLET_1_1_ALIAS = "/servet1_1";
+
+    private static final String SERVLET_2_ALIAS = "/servet2";
+
+    private MockExtHttpService httpService;
+
+    @Mock
+    private HttpContext sampleContext;
+
+    @Mock
+    private Bundle bundle1;
+
+    @Mock
+    private Bundle bundle2;
+
+    @Mock
+    private ExtServlet servlet1;
+
+    @Mock
+    private ExtServlet servlet1_1;
+
+    @Mock
+    private ExtServlet servlet2;
+
+    @Mock
+    private ExtFilter filter1;
+
+    @Mock
+    private ExtFilter filter1_1;
+
+
+    @Mock
+    private ExtFilter filter2;
+
+    @Mock
+    private ServiceReference servlet1Reference;
+
+    @Mock
+    private ServiceReference servlet1_1Reference;
+
+    @Mock
+    private ServiceReference servlet2Reference;
+
+    @Mock
+    private ServiceReference filter1Reference;
+
+    @Mock
+    private ServiceReference filter1_1Reference;
+
+    @Mock
+    private ServiceReference filter2Reference;
+
+    @Mock
+    private ServiceReference httpContextReference;
+
+    @Before
+    public void setup()
+    {
+        when(bundle1.getBundleId()).thenReturn(BUNDLE_1_ID);
+        when(bundle2.getBundleId()).thenReturn(BUNDLE_2_ID);
+        when(httpContextReference.getBundle()).thenReturn(bundle1);
+
+        when(servlet1Reference.getBundle()).thenReturn(bundle1);
+        when(servlet1Reference.getPropertyKeys()).thenReturn(new String[0]);
+        when(servlet1Reference.getProperty(HttpWhiteboardConstants.ALIAS)).thenReturn(SERVLET_1_ALIAS);
+
+        when(servlet1_1Reference.getBundle()).thenReturn(bundle1);
+        when(servlet1_1Reference.getPropertyKeys()).thenReturn(new String[0]);
+        when(servlet1_1Reference.getProperty(HttpWhiteboardConstants.ALIAS)).thenReturn(SERVLET_1_1_ALIAS);
+
+        when(servlet2Reference.getBundle()).thenReturn(bundle2);
+        when(servlet2Reference.getPropertyKeys()).thenReturn(new String[0]);
+        when(servlet2Reference.getProperty(HttpWhiteboardConstants.ALIAS)).thenReturn(SERVLET_2_ALIAS);
+        when(servlet2Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+
+        when(filter1Reference.getBundle()).thenReturn(bundle1);
+        when(filter1Reference.getPropertyKeys()).thenReturn(new String[0]);
+        when(filter1Reference.getProperty(HttpWhiteboardConstants.PATTERN)).thenReturn(SERVLET_1_ALIAS);
+
+        when(filter1_1Reference.getBundle()).thenReturn(bundle1);
+        when(filter1_1Reference.getPropertyKeys()).thenReturn(new String[0]);
+        when(filter1_1Reference.getProperty(HttpWhiteboardConstants.PATTERN)).thenReturn(SERVLET_1_1_ALIAS);
+
+        when(filter2Reference.getBundle()).thenReturn(bundle2);
+        when(filter2Reference.getPropertyKeys()).thenReturn(new String[0]);
+        when(filter2Reference.getProperty(HttpWhiteboardConstants.PATTERN)).thenReturn(SERVLET_2_ALIAS);
+        when(filter2Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+
+        this.httpService = new MockExtHttpService();
+    }
+
+    @After
+    public void tearDown()
+    {
+        this.httpService = null;
+    }
+
+    @Test
+    public void test_no_servlets_no_filters()
+    {
+        ExtenderManager em = new ExtenderManager();
+
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        em.setHttpService(null);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertTrue(em.getMappings().isEmpty());
+    }
+
+    @Test
+    public void test_servlet_per_bundle()
+    {
+        ExtenderManager em = new ExtenderManager();
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // setup a context without context ID
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        em.remove(sampleContext);
+
+        // set up a context with context ID and not shared
+        final String id = HttpContextManagerTest.createId(bundle1, SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // register servlet1 from bundle1
+        when(servlet1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(servlet1, servlet1Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, servlet1.getHttpContext());
+
+        // register servlet2 from bundle2
+        em.add(servlet2, servlet2Reference);
+
+        TestCase.assertEquals(2, em.getMappings().size());
+        TestCase.assertSame(servlet2, ((ServletMapping) em.getMappings().get(servlet2Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertNull(this.httpService.getServlets().get(SERVLET_2_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(servlet2Reference)));
+
+        // unregister servlet2
+        em.remove(servlet2Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister servlet1
+        em.remove(servlet1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().get(id).getMappings().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_servlet_shared()
+    {
+        ExtenderManager em = new ExtenderManager();
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // set up a context with context ID and shared
+        final String id = HttpContextManagerTest.createId(SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_SHARED)).thenReturn("true");
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // register servlet1 from bundle1
+        when(servlet1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(servlet1, servlet1Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, servlet1.getHttpContext());
+
+        // register servlet2 from bundle2
+        em.add(servlet2, servlet2Reference);
+
+        TestCase.assertEquals(2, em.getMappings().size());
+        TestCase.assertSame(servlet2, ((ServletMapping) em.getMappings().get(servlet2Reference)).getServlet());
+        TestCase.assertEquals(2, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(2, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet2, this.httpService.getServlets().get(SERVLET_2_ALIAS));
+        TestCase.assertEquals(0, em.getOrphanMappings().size());
+        TestCase.assertSame(sampleContext, servlet2.getHttpContext());
+
+        // unregister servlet2
+        em.remove(servlet2Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister servlet1
+        em.remove(servlet1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().get(id).getMappings().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_servlet_no_context_id()
+    {
+        ExtenderManager em = new ExtenderManager();
+        final String id1 = HttpContextManagerTest.createId(bundle1, null);
+        final String id2 = HttpContextManagerTest.createId(bundle2, null);
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+
+        // register servlet1 from bundle1
+        em.add(servlet1, servlet1Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id1).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(DefaultHttpContext.class, servlet1.getHttpContext().getClass());
+
+        // register servlet2 from bundle2
+        when(servlet2Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn("");
+        em.add(servlet2, servlet2Reference);
+
+        TestCase.assertEquals(2, em.getMappings().size());
+        TestCase.assertSame(servlet2, ((ServletMapping) em.getMappings().get(servlet2Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id2).getMappings().size());
+        TestCase.assertEquals(2, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet2, this.httpService.getServlets().get(SERVLET_2_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(DefaultHttpContext.class, servlet2.getHttpContext().getClass());
+
+        // different HttpContext instances per servlet/per bundle
+        TestCase.assertNotSame(servlet1.getHttpContext(), servlet2.getHttpContext());
+
+        // register servlet 1_1 from bundle 1
+        em.add(servlet1_1, servlet1_1Reference);
+
+        TestCase.assertEquals(3, em.getMappings().size());
+        TestCase.assertSame(servlet1_1, ((ServletMapping) em.getMappings().get(servlet1_1Reference)).getServlet());
+        TestCase.assertEquals(2, em.getHttpContexts().get(id1).getMappings().size());
+        TestCase.assertEquals(3, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1_1, this.httpService.getServlets().get(SERVLET_1_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(DefaultHttpContext.class, servlet1_1.getHttpContext().getClass());
+
+        // same HttpContext instances per servlet in same bundle
+        TestCase.assertSame(servlet1.getHttpContext(), servlet1_1.getHttpContext());
+    }
+
+    @Test
+    public void test_servlet_before_context_per_bundle()
+    {
+        ExtenderManager em = new ExtenderManager();
+        final String id = HttpContextManagerTest.createId(bundle1, SAMPLE_CONTEXT_ID);
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // register servlet1 from bundle1
+        when(servlet1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(servlet1, servlet1Reference);
+
+        // servlet not registered with HttpService yet
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertNull(this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(servlet1Reference)));
+
+        // set up a context with context ID and not shared
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // servlet registered with HttpService
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, servlet1.getHttpContext());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertNull(this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(servlet1Reference)));
+
+        // unregister servlet1
+        em.remove(servlet1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_servlet_before_context_shared()
+    {
+        ExtenderManager em = new ExtenderManager();
+        final String id = HttpContextManagerTest.createId(SAMPLE_CONTEXT_ID);
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // register servlet1 from bundle1
+        when(servlet1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(servlet1, servlet1Reference);
+
+        // servlet not registered with HttpService yet
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertNull(this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(servlet1Reference)));
+
+        // set up a context with context ID and not shared
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_SHARED)).thenReturn(true);
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // servlet registered with HttpService
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet1, this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, servlet1.getHttpContext());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(servlet1, ((ServletMapping) em.getMappings().get(servlet1Reference)).getServlet());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertNull(this.httpService.getServlets().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(servlet1Reference)));
+
+        // unregister servlet1
+        em.remove(servlet1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_filter_per_bundle()
+    {
+        ExtenderManager em = new ExtenderManager();
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // setup a context without context ID
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        em.remove(sampleContext);
+
+        // set up a context with context ID and not shared
+        final String id = HttpContextManagerTest.createId(bundle1, SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // register filter1 from bundle1
+        when(filter1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(filter1, filter1Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, filter1.getHttpContext());
+
+        // register filter2 from bundle2
+        em.add(filter2, filter2Reference);
+
+        TestCase.assertEquals(2, em.getMappings().size());
+        TestCase.assertSame(filter2, ((FilterMapping) em.getMappings().get(filter2Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertNull(this.httpService.getFilters().get(SERVLET_2_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(filter2Reference)));
+
+        // unregister filter2
+        em.remove(filter2Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister filter1
+        em.remove(filter1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().get(id).getMappings().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_filter_shared()
+    {
+        ExtenderManager em = new ExtenderManager();
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // set up a context with context ID and shared
+        final String id = HttpContextManagerTest.createId(SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_SHARED)).thenReturn("true");
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // register filter1 from bundle1
+        when(filter1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(filter1, filter1Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, filter1.getHttpContext());
+
+        // register filter2 from bundle2
+        em.add(filter2, filter2Reference);
+
+        TestCase.assertEquals(2, em.getMappings().size());
+        TestCase.assertSame(filter2, ((FilterMapping) em.getMappings().get(filter2Reference)).getFilter());
+        TestCase.assertEquals(2, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(2, this.httpService.getFilters().size());
+        TestCase.assertSame(filter2, this.httpService.getFilters().get(SERVLET_2_ALIAS));
+        TestCase.assertEquals(0, em.getOrphanMappings().size());
+        TestCase.assertSame(sampleContext, filter2.getHttpContext());
+
+        // unregister filter2
+        em.remove(filter2Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister filter1
+        em.remove(filter1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().get(id).getMappings().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_filter_no_context_id()
+    {
+        ExtenderManager em = new ExtenderManager();
+        final String id1 = HttpContextManagerTest.createId(bundle1, null);
+        final String id2 = HttpContextManagerTest.createId(bundle2, null);
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+
+        // register filter1 from bundle1
+        em.add(filter1, filter1Reference);
+
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id1).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(DefaultHttpContext.class, filter1.getHttpContext().getClass());
+
+        // register filter2 from bundle2
+        when(filter2Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn("");
+        em.add(filter2, filter2Reference);
+
+        TestCase.assertEquals(2, em.getMappings().size());
+        TestCase.assertSame(filter2, ((FilterMapping) em.getMappings().get(filter2Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id2).getMappings().size());
+        TestCase.assertEquals(2, this.httpService.getFilters().size());
+        TestCase.assertSame(filter2, this.httpService.getFilters().get(SERVLET_2_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(DefaultHttpContext.class, filter2.getHttpContext().getClass());
+
+        // different HttpContext instances per servlet/per bundle
+        TestCase.assertNotSame(filter1.getHttpContext(), filter2.getHttpContext());
+
+        // register servlet 1_1 from bundle 1
+        em.add(filter1_1, filter1_1Reference);
+
+        TestCase.assertEquals(3, em.getMappings().size());
+        TestCase.assertSame(filter1_1, ((FilterMapping) em.getMappings().get(filter1_1Reference)).getFilter());
+        TestCase.assertEquals(2, em.getHttpContexts().get(id1).getMappings().size());
+        TestCase.assertEquals(3, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1_1, this.httpService.getFilters().get(SERVLET_1_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(DefaultHttpContext.class, filter1_1.getHttpContext().getClass());
+
+        // same HttpContext instances per servlet in same bundle
+        TestCase.assertSame(filter1.getHttpContext(), filter1_1.getHttpContext());
+    }
+
+    @Test
+    public void test_filter_before_context_per_bundle()
+    {
+        ExtenderManager em = new ExtenderManager();
+        final String id = HttpContextManagerTest.createId(bundle1, SAMPLE_CONTEXT_ID);
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // register filter1 from bundle1
+        when(filter1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(filter1, filter1Reference);
+
+        // servlet not registered with HttpService yet
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertNull(this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(filter1Reference)));
+
+        // set up a context with context ID and not shared
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // servlet registered with HttpService
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, filter1.getHttpContext());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertNull(this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(filter1Reference)));
+
+        // unregister filter1
+        em.remove(filter1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_filter_before_context_shared()
+    {
+        ExtenderManager em = new ExtenderManager();
+        final String id = HttpContextManagerTest.createId(SAMPLE_CONTEXT_ID);
+
+        // prepare with http service
+        em.setHttpService(this.httpService);
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        // register filter1 from bundle1
+        when(filter1Reference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        em.add(filter1, filter1Reference);
+
+        // servlet not registered with HttpService yet
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertNull(this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(filter1Reference)));
+
+        // set up a context with context ID and not shared
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_ID)).thenReturn(SAMPLE_CONTEXT_ID);
+        when(httpContextReference.getProperty(HttpWhiteboardConstants.CONTEXT_SHARED)).thenReturn(true);
+        em.add(sampleContext, httpContextReference);
+        TestCase.assertEquals(1, em.getHttpContexts().size());
+
+        // servlet registered with HttpService
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(1, em.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter1, this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+        TestCase.assertSame(sampleContext, filter1.getHttpContext());
+
+        // unregister context
+        em.remove(sampleContext);
+        TestCase.assertEquals(1, em.getMappings().size());
+        TestCase.assertSame(filter1, ((FilterMapping) em.getMappings().get(filter1Reference)).getFilter());
+        TestCase.assertEquals(0, em.getHttpContexts().size());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertNull(this.httpService.getFilters().get(SERVLET_1_ALIAS));
+        TestCase.assertEquals(1, em.getOrphanMappings().size());
+        TestCase.assertEquals(1, em.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(em.getOrphanMappings().get(SAMPLE_CONTEXT_ID)
+            .contains(em.getMappings().get(filter1Reference)));
+
+        // unregister filter1
+        em.remove(filter1Reference);
+        TestCase.assertTrue(em.getMappings().isEmpty());
+        TestCase.assertTrue(em.getHttpContexts().isEmpty());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+        TestCase.assertTrue(em.getOrphanMappings().isEmpty());
+    }
+
+    static interface ExtFilter extends Filter
+    {
+        HttpContext getHttpContext();
+    }
+
+    static interface ExtServlet extends Servlet
+    {
+        HttpContext getHttpContext();
+    }
+
+    static final class MockExtHttpService implements ExtHttpService
+    {
+
+        private final BidiMap /* <String, Servlet> */servlets = new DualHashBidiMap();
+        private final BidiMap /* <String, Filter> */filters = new DualHashBidiMap();
+
+        /**
+         * @return BidiMap<String, Servlet>
+         */
+        public BidiMap getServlets()
+        {
+            return servlets;
+        }
+
+        /**
+         * @return BidiMap<String, Filter>
+         */
+        public BidiMap getFilters()
+        {
+            return filters;
+        }
+
+        public void registerServlet(String alias, Servlet servlet, @SuppressWarnings("rawtypes") Dictionary initparams,
+            HttpContext context)
+
+        {
+            // always expect a non-null HttpContext here !!
+            TestCase.assertNotNull(context);
+
+            this.servlets.put(alias, servlet);
+
+            // make HttpContext available
+            when(((ExtServlet) servlet).getHttpContext()).thenReturn(context);
+        }
+
+        public void registerResources(String alias, String name, HttpContext context)
+        {
+            // not used here
+        }
+
+        public void unregister(String alias)
+        {
+            Object servlet = this.servlets.remove(alias);
+            if (servlet instanceof ExtServlet)
+            {
+                when(((ExtServlet) servlet).getHttpContext()).thenReturn(null);
+            }
+        }
+
+        public HttpContext createDefaultHttpContext()
+        {
+            // not used here
+            return null;
+        }
+
+        public void registerFilter(Filter filter, String pattern, @SuppressWarnings("rawtypes") Dictionary initParams,
+            int ranking, HttpContext context)
+        {
+            // always expect a non-null HttpContext here !!
+            TestCase.assertNotNull(context);
+
+            this.filters.put(pattern, filter);
+
+            // make HttpContext available
+            when(((ExtFilter) filter).getHttpContext()).thenReturn(context);
+        }
+
+        public void unregisterFilter(Filter filter)
+        {
+            this.filters.removeValue(filter);
+            when(((ExtFilter) filter).getHttpContext()).thenReturn(null);
+        }
+
+        public void unregisterServlet(Servlet servlet)
+        {
+            this.servlets.removeValue(servlet);
+            when(((ExtServlet) servlet).getHttpContext()).thenReturn(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/FilterMappingTest.java b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/FilterMappingTest.java
new file mode 100644
index 0000000..3fda558
--- /dev/null
+++ b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/FilterMappingTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.whiteboard.internal.manager;
+
+import static org.mockito.Mockito.when;
+
+import junit.framework.TestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpContext;
+
+@RunWith(MockitoJUnitRunner.class)
+public class FilterMappingTest
+{
+
+    private static final String SAMPLE_CONTEXT_ID = "some.context.id";
+
+    private static final long BUNDLE_ID = 1L;
+
+    private static final String FILTER_PATTERN = "/sample/*";
+
+    private static final int FILTER_RANKING = 1234;
+
+    @Mock
+    private HttpContext sampleContext;
+
+    @Mock
+    private Bundle bundle;
+
+    @Mock
+    private ExtenderManagerTest.ExtFilter filter;
+
+    private ExtenderManagerTest.MockExtHttpService httpService;
+
+    @Before
+    public void setup()
+    {
+        when(bundle.getBundleId()).thenReturn(BUNDLE_ID);
+
+        this.httpService = new ExtenderManagerTest.MockExtHttpService();
+    }
+
+    @After
+    public void tearDown()
+    {
+        this.httpService = null;
+    }
+
+    @Test
+    public void test_with_context()
+    {
+        FilterMapping fm = new FilterMapping(bundle, filter, FILTER_PATTERN, FILTER_RANKING);
+        TestCase.assertSame(bundle, fm.getBundle());
+        TestCase.assertSame(filter, fm.getFilter());
+        TestCase.assertEquals(FILTER_PATTERN, fm.getPattern());
+        TestCase.assertEquals(FILTER_RANKING, fm.getRanking());
+
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertNotNull(fm.getInitParams());
+        TestCase.assertTrue(fm.getInitParams().isEmpty());
+        TestCase.assertFalse(fm.isRegistered());
+
+        fm.setContext(sampleContext);
+        TestCase.assertSame(sampleContext, fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        fm.register(this.httpService);
+        TestCase.assertSame(sampleContext, fm.getContext());
+        TestCase.assertTrue(fm.isRegistered());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter, this.httpService.getFilters().get(FILTER_PATTERN));
+        TestCase.assertSame(sampleContext, filter.getHttpContext());
+
+        fm.unregister(this.httpService);
+        TestCase.assertSame(sampleContext, fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+
+        fm.setContext(null);
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+    }
+
+    @Test
+    public void test_context_delayed()
+    {
+        FilterMapping fm = new FilterMapping(bundle, filter, FILTER_PATTERN, FILTER_RANKING);
+        TestCase.assertSame(bundle, fm.getBundle());
+        TestCase.assertSame(filter, fm.getFilter());
+        TestCase.assertEquals(FILTER_PATTERN, fm.getPattern());
+        TestCase.assertEquals(FILTER_RANKING, fm.getRanking());
+
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertNotNull(fm.getInitParams());
+        TestCase.assertTrue(fm.getInitParams().isEmpty());
+        TestCase.assertFalse(fm.isRegistered());
+
+        fm.register(this.httpService);
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        fm.unregister(httpService);
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        fm.setContext(sampleContext);
+        TestCase.assertSame(sampleContext, fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+    }
+
+    @Test
+    public void test_unset_context()
+    {
+        FilterMapping fm = new FilterMapping(bundle, filter, FILTER_PATTERN, FILTER_RANKING);
+        TestCase.assertSame(bundle, fm.getBundle());
+        TestCase.assertSame(filter, fm.getFilter());
+        TestCase.assertEquals(FILTER_PATTERN, fm.getPattern());
+        TestCase.assertEquals(FILTER_RANKING, fm.getRanking());
+
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertNotNull(fm.getInitParams());
+        TestCase.assertTrue(fm.getInitParams().isEmpty());
+        TestCase.assertFalse(fm.isRegistered());
+
+        fm.setContext(sampleContext);
+        TestCase.assertSame(sampleContext, fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertTrue(this.httpService.getFilters().isEmpty());
+
+        fm.register(this.httpService);
+        TestCase.assertSame(sampleContext, fm.getContext());
+        TestCase.assertTrue(fm.isRegistered());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter, this.httpService.getFilters().get(FILTER_PATTERN));
+        TestCase.assertSame(sampleContext, filter.getHttpContext());
+
+        // does not unregister yet
+        fm.setContext(null);
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertTrue(fm.isRegistered());
+        TestCase.assertEquals(1, this.httpService.getFilters().size());
+        TestCase.assertSame(filter, this.httpService.getFilters().get(FILTER_PATTERN));
+        TestCase.assertSame(sampleContext, filter.getHttpContext());
+
+        fm.unregister(this.httpService);
+        TestCase.assertNull(fm.getContext());
+        TestCase.assertFalse(fm.isRegistered());
+        TestCase.assertEquals(0, this.httpService.getFilters().size());
+    }
+}
diff --git a/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManagerTest.java b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManagerTest.java
new file mode 100644
index 0000000..5ce0a49
--- /dev/null
+++ b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/HttpContextManagerTest.java
@@ -0,0 +1,330 @@
+/*
+ * 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.whiteboard.internal.manager;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.http.whiteboard.internal.manager.HttpContextManager.HttpContextHolder;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpContext;
+
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class HttpContextManagerTest
+{
+
+    private static final String SAMPLE_CONTEXT_ID = "some.context.id";
+
+    private static final long BUNDLE_1_ID = 1L;
+
+    private static final String BUNDLE_1_ALIAS = "/bundle1";
+
+    private static final long BUNDLE_2_ID = 2L;
+
+    private static final String BUNDLE_2_ALIAS = "/bundle2";
+
+    @Mock
+    private HttpContext sampleContext;
+
+    @Mock
+    private Bundle bundle1;
+
+    @Mock
+    private Bundle bundle2;
+
+    @Before
+    public void setup()
+    {
+        when(bundle1.getBundleId()).thenReturn(BUNDLE_1_ID);
+        when(bundle2.getBundleId()).thenReturn(BUNDLE_2_ID);
+    }
+
+    @Test
+    public void test_HttpContextHolder()
+    {
+        TestCase.assertNotNull(sampleContext);
+
+        final HttpContextHolder h1 = new HttpContextHolder(sampleContext);
+        TestCase.assertSame(sampleContext, h1.getContext());
+        TestCase.assertTrue(h1.getMappings().isEmpty());
+
+        ServletMapping sm = new ServletMapping(bundle1, null, "");
+        h1.addMapping(sm);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertEquals(1, h1.getMappings().size());
+        TestCase.assertTrue(h1.getMappings().contains(sm));
+
+        h1.removeMapping(sm);
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertTrue(h1.getMappings().isEmpty());
+    }
+
+    @Test
+    public void test_add_remove_HttpContext_per_Bundle()
+    {
+        final HttpContextManager hcm = new HttpContextManager();
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+
+        Collection<AbstractMapping> mappings = hcm.addHttpContext(bundle1, SAMPLE_CONTEXT_ID, sampleContext);
+        TestCase.assertNotNull(mappings);
+        TestCase.assertTrue(mappings.isEmpty());
+
+        String holderId = createId(bundle1, SAMPLE_CONTEXT_ID);
+        Map<String, HttpContextHolder> holders = hcm.getHttpContexts();
+        TestCase.assertEquals(1, holders.size());
+        TestCase.assertSame(sampleContext, holders.get(holderId).getContext());
+        TestCase.assertEquals(mappings, holders.get(holderId).getMappings());
+
+        Collection<AbstractMapping> removedMappings = hcm.removeHttpContext(sampleContext);
+        TestCase.assertNotNull(removedMappings);
+        TestCase.assertTrue(removedMappings.isEmpty());
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+    }
+
+    @Test
+    public void test_add_remove_HttpContext_shared()
+    {
+        final HttpContextManager hcm = new HttpContextManager();
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+
+        Collection<AbstractMapping> mappings = hcm.addHttpContext(null, SAMPLE_CONTEXT_ID, sampleContext);
+        TestCase.assertNotNull(mappings);
+        TestCase.assertTrue(mappings.isEmpty());
+
+        String holderId = createId(SAMPLE_CONTEXT_ID);
+        Map<String, HttpContextHolder> holders = hcm.getHttpContexts();
+        TestCase.assertEquals(1, holders.size());
+        TestCase.assertSame(sampleContext, holders.get(holderId).getContext());
+        TestCase.assertEquals(mappings, holders.get(holderId).getMappings());
+
+        Collection<AbstractMapping> removedMappings = hcm.removeHttpContext(sampleContext);
+        TestCase.assertNotNull(removedMappings);
+        TestCase.assertTrue(removedMappings.isEmpty());
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+    }
+
+    @Test
+    public void test_get_unget_HttpContext_per_bundle_same_bundle()
+    {
+        final HttpContextManager hcm = new HttpContextManager();
+        final String id = createId(bundle1, SAMPLE_CONTEXT_ID);
+        hcm.addHttpContext(bundle1, SAMPLE_CONTEXT_ID, sampleContext);
+
+        // Servlet 1 gets the context
+        final ServletMapping bundle1Servlet = new ServletMapping(bundle1, null, BUNDLE_1_ALIAS);
+        HttpContext ctx1 = hcm.getHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        TestCase.assertNotNull(ctx1);
+        TestCase.assertSame(ctx1, bundle1Servlet.getContext());
+        TestCase.assertSame(sampleContext, ctx1);
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().contains(bundle1Servlet));
+        Map<String, Set<AbstractMapping>> orphans1 = hcm.getOrphanMappings();
+        TestCase.assertTrue(orphans1.isEmpty());
+
+        // unregister servlet again --> all references clear
+        hcm.ungetHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        TestCase.assertNull(bundle1Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().isEmpty());
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+
+        // register servlet, unregister context --> orphan
+        hcm.getHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        hcm.removeHttpContext(sampleContext);
+        TestCase.assertNull(bundle1Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+        TestCase.assertEquals(1, hcm.getOrphanMappings().size());
+        TestCase.assertEquals(1, hcm.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(hcm.getOrphanMappings().get(SAMPLE_CONTEXT_ID).contains(bundle1Servlet));
+
+        // cleanup
+        hcm.ungetHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        TestCase.assertNull(bundle1Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+    }
+
+    public void test_get_unget_HttpContext_per_bundle_other_bundle()
+    {
+        final HttpContextManager hcm = new HttpContextManager();
+
+        final String id1 = createId(bundle1, SAMPLE_CONTEXT_ID);
+        hcm.addHttpContext(bundle1, SAMPLE_CONTEXT_ID, sampleContext);
+
+        // Servlet 2 is an orphan
+        final ServletMapping bundle2Servlet = new ServletMapping(bundle2, null, BUNDLE_2_ALIAS);
+        HttpContext ctx2 = hcm.getHttpContext(bundle2, SAMPLE_CONTEXT_ID, bundle2Servlet);
+        TestCase.assertNull(ctx2);
+        TestCase.assertNull(bundle2Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id1).getMappings().isEmpty());
+        Map<String, Set<AbstractMapping>> orphans2 = hcm.getOrphanMappings();
+        TestCase.assertEquals(1, orphans2.size());
+        TestCase.assertEquals(1, orphans2.get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(orphans2.get(SAMPLE_CONTEXT_ID).contains(bundle2Servlet));
+
+        // unregister unused context for bundle1
+        hcm.removeHttpContext(sampleContext);
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+
+        // register context for bundle2
+        final String id2 = createId(bundle1, SAMPLE_CONTEXT_ID);
+        hcm.addHttpContext(bundle2, SAMPLE_CONTEXT_ID, sampleContext);
+        TestCase.assertEquals(1, hcm.getHttpContexts().size());
+        TestCase.assertSame(sampleContext, hcm.getHttpContexts().get(id2).getContext());
+
+        TestCase.assertSame(sampleContext, bundle2Servlet.getContext());
+        TestCase.assertEquals(1, hcm.getHttpContexts().get(id2).getMappings().size());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id2).getMappings().contains(bundle2Servlet));
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+
+        // cleanup
+        hcm.ungetHttpContext(bundle2, SAMPLE_CONTEXT_ID, bundle2Servlet);
+        TestCase.assertNull(bundle2Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id2).getMappings().isEmpty());
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_get_unget_HttpContext_shared()
+    {
+        final HttpContextManager hcm = new HttpContextManager();
+        final String id = createId(SAMPLE_CONTEXT_ID);
+        hcm.addHttpContext(null, SAMPLE_CONTEXT_ID, sampleContext);
+
+        // Servlet 1 gets the context
+        final ServletMapping bundle1Servlet = new ServletMapping(bundle1, null, BUNDLE_1_ALIAS);
+        HttpContext ctx1 = hcm.getHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        TestCase.assertNotNull(ctx1);
+        TestCase.assertSame(ctx1, bundle1Servlet.getContext());
+        TestCase.assertSame(sampleContext, ctx1);
+        TestCase.assertEquals(1, hcm.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().contains(bundle1Servlet));
+        Map<String, Set<AbstractMapping>> orphans1 = hcm.getOrphanMappings();
+        TestCase.assertTrue(orphans1.isEmpty());
+
+        // unregister serlvet 1 --> all references clear
+        hcm.ungetHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        TestCase.assertNull(bundle1Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().isEmpty());
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+
+        // Servlet 2 gets the context
+        final ServletMapping bundle2Servlet = new ServletMapping(bundle2, null, BUNDLE_2_ALIAS);
+        HttpContext ctx2 = hcm.getHttpContext(bundle2, SAMPLE_CONTEXT_ID, bundle2Servlet);
+        TestCase.assertNotNull(ctx2);
+        TestCase.assertSame(ctx2, bundle2Servlet.getContext());
+        TestCase.assertSame(sampleContext, ctx2);
+        TestCase.assertEquals(1, hcm.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().contains(bundle2Servlet));
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+
+        // register Servlet 1 again --> gets context
+        hcm.getHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        HttpContext ctx3 = hcm.getHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        TestCase.assertNotNull(ctx3);
+        TestCase.assertSame(ctx3, bundle1Servlet.getContext());
+        TestCase.assertSame(sampleContext, ctx3);
+        TestCase.assertEquals(2, hcm.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().contains(bundle1Servlet));
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+
+        // unregister context --> all references clear
+        hcm.removeHttpContext(sampleContext);
+        TestCase.assertNull(bundle1Servlet.getContext());
+        TestCase.assertNull(bundle2Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+        TestCase.assertEquals(1, hcm.getOrphanMappings().size());
+        TestCase.assertEquals(2, hcm.getOrphanMappings().get(SAMPLE_CONTEXT_ID).size());
+        TestCase.assertTrue(hcm.getOrphanMappings().get(SAMPLE_CONTEXT_ID).contains(bundle1Servlet));
+        TestCase.assertTrue(hcm.getOrphanMappings().get(SAMPLE_CONTEXT_ID).contains(bundle2Servlet));
+
+        // register context --> servlets 1, 2 get context
+        hcm.addHttpContext(null, SAMPLE_CONTEXT_ID, sampleContext);
+        TestCase.assertSame(sampleContext, bundle1Servlet.getContext());
+        TestCase.assertSame(sampleContext, bundle2Servlet.getContext());
+        TestCase.assertEquals(2, hcm.getHttpContexts().get(id).getMappings().size());
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().contains(bundle1Servlet));
+        TestCase.assertTrue(hcm.getHttpContexts().get(id).getMappings().contains(bundle2Servlet));
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+
+        // cleanup
+        hcm.removeHttpContext(sampleContext);
+        hcm.ungetHttpContext(bundle1, SAMPLE_CONTEXT_ID, bundle1Servlet);
+        hcm.ungetHttpContext(bundle2, SAMPLE_CONTEXT_ID, bundle2Servlet);
+        TestCase.assertNull(bundle1Servlet.getContext());
+        TestCase.assertNull(bundle2Servlet.getContext());
+        TestCase.assertTrue(hcm.getHttpContexts().isEmpty());
+        TestCase.assertTrue(hcm.getOrphanMappings().isEmpty());
+    }
+
+    @Test
+    public void test_createId_Bundle_String()
+    {
+        TestCase.assertEquals(BUNDLE_1_ID + "-", createId(bundle1, null));
+        TestCase.assertEquals(BUNDLE_1_ID + "-", createId(bundle1, ""));
+        TestCase.assertEquals(BUNDLE_1_ID + "-" + SAMPLE_CONTEXT_ID, createId(bundle1, SAMPLE_CONTEXT_ID));
+    }
+
+    @Test
+    public void test_createId_String()
+    {
+        TestCase.assertEquals("shared-", createId(null));
+        TestCase.assertEquals("shared-", createId(""));
+        TestCase.assertEquals("shared-" + SAMPLE_CONTEXT_ID, createId(SAMPLE_CONTEXT_ID));
+    }
+
+    static String createId(String contextId)
+    {
+        try
+        {
+            Method m = HttpContextManager.class.getDeclaredMethod("createId", String.class);
+            m.setAccessible(true);
+            return (String) m.invoke(null, contextId);
+        }
+        catch (Throwable t)
+        {
+            TestCase.fail(t.toString());
+            return null; // compiler satisfaction
+        }
+    }
+
+    static String createId(Bundle bundle, String contextId)
+    {
+        try
+        {
+            Method m = HttpContextManager.class.getDeclaredMethod("createId", Bundle.class, String.class);
+            m.setAccessible(true);
+            return (String) m.invoke(null, bundle, contextId);
+        }
+        catch (Throwable t)
+        {
+            TestCase.fail(t.toString());
+            return null; // compiler satisfaction
+        }
+    }
+}
diff --git a/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/ServletMappingTest.java b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/ServletMappingTest.java
new file mode 100644
index 0000000..2596838
--- /dev/null
+++ b/http/whiteboard/src/test/java/org/apache/felix/http/whiteboard/internal/manager/ServletMappingTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.whiteboard.internal.manager;
+
+import static org.mockito.Mockito.when;
+
+import junit.framework.TestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpContext;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ServletMappingTest
+{
+
+    private static final String SAMPLE_CONTEXT_ID = "some.context.id";
+
+    private static final long BUNDLE_ID = 1L;
+
+    private static final String SERVLET_ALIAS = "/bundle";
+
+    @Mock
+    private HttpContext sampleContext;
+
+    @Mock
+    private Bundle bundle;
+
+    @Mock
+    private ExtenderManagerTest.ExtServlet servlet;
+
+    private ExtenderManagerTest.MockExtHttpService httpService;
+
+    @Before
+    public void setup()
+    {
+        when(bundle.getBundleId()).thenReturn(BUNDLE_ID);
+
+        this.httpService = new ExtenderManagerTest.MockExtHttpService();
+    }
+
+    @After
+    public void tearDown()
+    {
+        this.httpService = null;
+    }
+
+    @Test
+    public void test_with_context()
+    {
+        ServletMapping sm = new ServletMapping(bundle, servlet, SERVLET_ALIAS);
+        TestCase.assertSame(bundle, sm.getBundle());
+        TestCase.assertSame(servlet, sm.getServlet());
+        TestCase.assertEquals(SERVLET_ALIAS, sm.getAlias());
+
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertNotNull(sm.getInitParams());
+        TestCase.assertTrue(sm.getInitParams().isEmpty());
+        TestCase.assertFalse(sm.isRegistered());
+
+        sm.setContext(sampleContext);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+
+        sm.register(this.httpService);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertTrue(sm.isRegistered());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet, this.httpService.getServlets().get(SERVLET_ALIAS));
+        TestCase.assertSame(sampleContext, servlet.getHttpContext());
+
+        sm.unregister(this.httpService);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+
+        sm.setContext(null);
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+    }
+
+    @Test
+    public void test_context_delayed()
+    {
+        ServletMapping sm = new ServletMapping(bundle, servlet, SERVLET_ALIAS);
+        TestCase.assertSame(bundle, sm.getBundle());
+        TestCase.assertSame(servlet, sm.getServlet());
+        TestCase.assertEquals(SERVLET_ALIAS, sm.getAlias());
+
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertNotNull(sm.getInitParams());
+        TestCase.assertTrue(sm.getInitParams().isEmpty());
+        TestCase.assertFalse(sm.isRegistered());
+
+        sm.register(this.httpService);
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+
+        sm.unregister(httpService);
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+
+        sm.setContext(sampleContext);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+    }
+
+    @Test
+    public void test_unset_context()
+    {
+        ServletMapping sm = new ServletMapping(bundle, servlet, SERVLET_ALIAS);
+        TestCase.assertSame(bundle, sm.getBundle());
+        TestCase.assertSame(servlet, sm.getServlet());
+        TestCase.assertEquals(SERVLET_ALIAS, sm.getAlias());
+
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertNotNull(sm.getInitParams());
+        TestCase.assertTrue(sm.getInitParams().isEmpty());
+        TestCase.assertFalse(sm.isRegistered());
+
+        sm.setContext(sampleContext);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertTrue(this.httpService.getServlets().isEmpty());
+
+        sm.register(this.httpService);
+        TestCase.assertSame(sampleContext, sm.getContext());
+        TestCase.assertTrue(sm.isRegistered());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet, this.httpService.getServlets().get(SERVLET_ALIAS));
+        TestCase.assertSame(sampleContext, servlet.getHttpContext());
+
+        // does not unregister yet
+        sm.setContext(null);
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertTrue(sm.isRegistered());
+        TestCase.assertEquals(1, this.httpService.getServlets().size());
+        TestCase.assertSame(servlet, this.httpService.getServlets().get(SERVLET_ALIAS));
+        TestCase.assertSame(sampleContext, servlet.getHttpContext());
+
+        sm.unregister(this.httpService);
+        TestCase.assertNull(sm.getContext());
+        TestCase.assertFalse(sm.isRegistered());
+        TestCase.assertEquals(0, this.httpService.getServlets().size());
+    }
+}