Bug 454256 - [http] add customizer to provide default context selection
and context path prefix
diff --git a/bundles/org.eclipse.equinox.http.servlet.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.http.servlet.tests/META-INF/MANIFEST.MF
index 37c5897..cb3b051 100644
--- a/bundles/org.eclipse.equinox.http.servlet.tests/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.http.servlet.tests/META-INF/MANIFEST.MF
@@ -9,6 +9,7 @@
  javax.servlet.http;version="2.6.0",
  junit.framework;version="4.8.2",
  org.eclipse.equinox.http.servlet;version="1.1.0",
+ org.eclipse.equinox.http.servlet.context; version="1.0.0",
  org.eclipse.osgi.service.urlconversion;version="1.0.0",
  org.junit;version="4.11.0",
  org.osgi.framework;version="1.6.0",
diff --git a/bundles/org.eclipse.equinox.http.servlet.tests/src/org/eclipse/equinox/http/servlet/tests/ServletTest.java b/bundles/org.eclipse.equinox.http.servlet.tests/src/org/eclipse/equinox/http/servlet/tests/ServletTest.java
index accbc54..1dd7dd4 100644
--- a/bundles/org.eclipse.equinox.http.servlet.tests/src/org/eclipse/equinox/http/servlet/tests/ServletTest.java
+++ b/bundles/org.eclipse.equinox.http.servlet.tests/src/org/eclipse/equinox/http/servlet/tests/ServletTest.java
@@ -17,7 +17,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Dictionary;
-import java.util.EventListener;
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
@@ -39,6 +38,7 @@
 import junit.framework.TestCase;
 
 import org.eclipse.equinox.http.servlet.ExtendedHttpService;
+import org.eclipse.equinox.http.servlet.context.ContextPathCustomizer;
 import org.eclipse.equinox.http.servlet.tests.bundle.Activator;
 import org.eclipse.equinox.http.servlet.tests.bundle.BundleAdvisor;
 import org.eclipse.equinox.http.servlet.tests.bundle.BundleInstaller;
@@ -56,6 +56,7 @@
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceFactory;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.http.HttpService;
@@ -1200,6 +1201,7 @@
 	private static final String UNREGISTER = "unregister";
 	private static final String STATUS_PARAM = "servlet.init.status";
 	private static final String TEST_PROTOTYPE_NAME = "test.prototype.name";
+	private static final String TEST_PATH_CUSTOMIZER_NAME = "test.path.customizer.name";
 	public void testWBServletChangeInitParams() throws Exception{
 			String actual;
 
@@ -1277,6 +1279,84 @@
 		Assert.assertEquals(getName() + 2, actual);
 	}
 
+	public void testWBServletDefaultContextAdaptor1() throws Exception{
+		Dictionary<String, String> helperProps = new Hashtable<String, String>();
+		helperProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME, "testContext" + getName());
+		helperProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH, "/testContext");
+		helperProps.put(TEST_PATH_CUSTOMIZER_NAME, getName());
+		ServiceRegistration<ServletContextHelper> helperReg = getBundleContext().registerService(ServletContextHelper.class, new TestServletContextHelperFactory(), helperProps);
+
+		ServiceRegistration<ContextPathCustomizer> pathAdaptorReg = null;
+		try {
+			Map<String, String> params = new HashMap<String, String>();
+			params.put(TEST_PROTOTYPE_NAME, getName());
+			params.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, '/' + getName());
+			params.put(STATUS_PARAM, getName());
+			params.put("servlet.init." + TEST_PATH_CUSTOMIZER_NAME, getName());
+			String actual = doRequest(CONFIGURE, params);
+			Assert.assertEquals(getName(), actual);
+
+			actual = requestAdvisor.request(getName());
+			Assert.assertEquals(getName(), actual);
+
+			ContextPathCustomizer pathAdaptor = new TestContextPathAdaptor("(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + "testContext" + getName() + ")", null, getName());
+			pathAdaptorReg = getBundleContext().registerService(ContextPathCustomizer.class, pathAdaptor, null);
+
+			actual = requestAdvisor.request("testContext/" + getName());
+			Assert.assertEquals(getName(), actual);
+
+			pathAdaptorReg.unregister();
+			pathAdaptorReg = null;
+
+			actual = requestAdvisor.request(getName());
+			Assert.assertEquals(getName(), actual);
+		} finally {
+			helperReg.unregister();
+			if (pathAdaptorReg != null) {
+				pathAdaptorReg.unregister();
+			}
+		}
+	}
+
+	public void testWBServletDefaultContextAdaptor2() throws Exception{
+		Dictionary<String, String> helperProps = new Hashtable<String, String>();
+		helperProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME, "testContext" + getName());
+		helperProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH, "/testContext");
+		helperProps.put(TEST_PATH_CUSTOMIZER_NAME, getName());
+		ServiceRegistration<ServletContextHelper> helperReg = getBundleContext().registerService(ServletContextHelper.class, new TestServletContextHelperFactory(), helperProps);
+
+		ServiceRegistration<ContextPathCustomizer> pathAdaptorReg = null;
+		try {
+			Map<String, String> params = new HashMap<String, String>();
+			params.put(TEST_PROTOTYPE_NAME, getName());
+			params.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, '/' + getName());
+			params.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + "testContext" + getName() + ")");
+			params.put(STATUS_PARAM, getName());
+			params.put("servlet.init." + TEST_PATH_CUSTOMIZER_NAME, getName());
+			String actual = doRequest(CONFIGURE, params);
+			Assert.assertEquals(getName(), actual);
+
+			actual = requestAdvisor.request("testContext/" + getName());
+			Assert.assertEquals(getName(), actual);
+
+			ContextPathCustomizer pathAdaptor = new TestContextPathAdaptor(null, "testPrefix", getName());
+			pathAdaptorReg = getBundleContext().registerService(ContextPathCustomizer.class, pathAdaptor, null);
+
+			actual = requestAdvisor.request("testPrefix/testContext/" + getName());
+			Assert.assertEquals(getName(), actual);
+
+			pathAdaptorReg.unregister();
+			pathAdaptorReg = null;
+
+			actual = requestAdvisor.request("testContext/" + getName());
+			Assert.assertEquals(getName(), actual);
+		} finally {
+			helperReg.unregister();
+			if (pathAdaptorReg != null) {
+				pathAdaptorReg.unregister();
+			}
+		}
+	}
 	private String doRequest(String action, Map<String, String> params) throws IOException {
 		StringBuilder requestInfo = new StringBuilder(PROTOTYPE);
 		requestInfo.append(action);
@@ -1387,4 +1467,55 @@
 		public void init(FilterConfig arg0) throws ServletException {/**/}
 	}
 
+	static class TestServletContextHelperFactory implements ServiceFactory<ServletContextHelper> {
+		static class TestServletContextHelper extends ServletContextHelper {
+			public TestServletContextHelper(Bundle bundle) {
+				super(bundle);
+			}};
+		@Override
+		public ServletContextHelper getService(Bundle bundle, ServiceRegistration<ServletContextHelper> registration) {
+			return new TestServletContextHelper(bundle);
+		}
+
+		@Override
+		public void ungetService(Bundle bundle, ServiceRegistration<ServletContextHelper> registration,
+				ServletContextHelper service) {
+			// nothing
+		}
+		
+	}
+
+	static class TestContextPathAdaptor extends ContextPathCustomizer {
+		private final String defaultFilter;
+		private final String contextPrefix;
+		private final String testName;
+		
+		/**
+		 * @param defaultFilter
+		 * @param contextPrefix
+		 */
+		public TestContextPathAdaptor(String defaultFilter, String contextPrefix, String testName) {
+			super();
+			this.defaultFilter = defaultFilter;
+			this.contextPrefix = contextPrefix;
+			this.testName = testName;
+		}
+
+		@Override
+		public String getDefaultContextSelectFilter(ServiceReference<?> httpWhiteBoardService) {
+			if (testName.equals(httpWhiteBoardService.getProperty("servlet.init." + TEST_PATH_CUSTOMIZER_NAME))) {
+				return defaultFilter;
+			}
+			return null;
+		}
+
+		@Override
+		public String getContextPathPrefix(ServiceReference<ServletContextHelper> helper) {
+			if (testName.equals(helper.getProperty(TEST_PATH_CUSTOMIZER_NAME))) {
+				return contextPrefix;
+			}
+			return null;
+		}
+		
+	}
 }
diff --git a/bundles/org.eclipse.equinox.http.servlet/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.http.servlet/META-INF/MANIFEST.MF
index c79c4e1..4d7b121 100644
--- a/bundles/org.eclipse.equinox.http.servlet/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.http.servlet/META-INF/MANIFEST.MF
@@ -7,7 +7,8 @@
 Bundle-Activator: org.eclipse.equinox.http.servlet.internal.Activator
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.6
-Export-Package: org.eclipse.equinox.http.servlet;version="1.1.0"
+Export-Package: org.eclipse.equinox.http.servlet;version="1.1.0",
+ org.eclipse.equinox.http.servlet.context; x-internal:=true;version="1.0.0"
 Import-Package: javax.servlet;version="[2.3.0,4.0.0)",
  javax.servlet.annotation;version="2.6.0";resolution:=optional,
  javax.servlet.descriptor;version="2.6.0";resolution:=optional,
diff --git a/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/context/ContextPathCustomizer.java b/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/context/ContextPathCustomizer.java
new file mode 100644
index 0000000..c899fe9
--- /dev/null
+++ b/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/context/ContextPathCustomizer.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) Dec 5, 2014 Liferay, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Liferay, Inc. - initial API and implementation and/or initial 
+ *                    documentation
+ ******************************************************************************/
+
+package org.eclipse.equinox.http.servlet.context;
+
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.context.ServletContextHelper;
+
+/**
+ * A customizer that is called by the Http Whiteboard runtime in order to allow
+ * customization of context path used for servlets, resources and filters.
+ * There are two types of customizations that are allowed.
+ * <ol>
+ *   <li>Control the default selection filter used when no &quot;osgi.http.whiteboard.context.select&quot;
+ *       is specified.</li>
+ *   <li>Provide a prefix to the context path &quot;osgi.http.whiteboard.context.path&quot; 
+ *       specified by ServletContextHelper registrations.</li>
+ * </ol>
+ * <p>
+ * Registering a customizer results in re-initializing all existing ServletContextHelper registrations.
+ * This should not be done often.  Only the highest ranked customizer is used the runtime.
+ * </p>
+ * <p>
+ * <b>Note:</b> This class is part of an interim SPI that is still under 
+ * development and expected to change significantly before reaching stability. 
+ * It is being made available at this early stage to solicit feedback from pioneering 
+ * adopters on the understanding that any code that uses this SPI will almost certainly 
+ * be broken (repeatedly) as the SPI evolves.
+ * </p>
+ * @since 1.2
+ */
+public abstract class ContextPathCustomizer {
+	/**
+	 * Returns a service filter that is used to select the default ServletContextHelper when no
+	 * selection filter is specified by the whiteboard service.  This method is only
+	 * called if the supplied whiteboard service does not provide the 
+	 * &quot;osgi.http.whiteboard.context.select&quot; service property.
+	 * @param httpWhiteBoardService
+	 * @return a service filter that is used to select the default SErvletContextHelper for the
+	 * specified whiteboard service.
+	 */
+	public String getDefaultContextSelectFilter(ServiceReference<?> httpWhiteBoardService) {
+		return null;
+	}
+
+	/**
+	 * Returns a prefix that is prepended to the context path value 
+	 * specified by the supplied helper's &quot;osgi.http.whiteboard.context.path&quot;
+	 * service property.
+	 * @param helper the helper for which the context path will be prepended to
+	 * @return the prefix to prepend to the context path 
+	 */
+	public String getContextPathPrefix(ServiceReference<ServletContextHelper> helper) {
+		return null;
+	}
+}
diff --git a/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/HttpServiceRuntimeImpl.java b/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/HttpServiceRuntimeImpl.java
index 0a4f158..128e18b 100644
--- a/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/HttpServiceRuntimeImpl.java
+++ b/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/HttpServiceRuntimeImpl.java
@@ -20,6 +20,7 @@
 import javax.servlet.*;
 import javax.servlet.Filter;
 import javax.servlet.http.*;
+import org.eclipse.equinox.http.servlet.context.ContextPathCustomizer;
 import org.eclipse.equinox.http.servlet.internal.context.*;
 import org.eclipse.equinox.http.servlet.internal.error.*;
 import org.eclipse.equinox.http.servlet.internal.servlet.*;
@@ -64,6 +65,11 @@
 			new ServiceTracker<ServletContextHelper, AtomicReference<ContextController>>(
 				trackingContext, ServletContextHelper.class, this);
 
+		contextPathCustomizerHolder = new ContextPathCustomizerHolder(consumingContext, contextServiceTracker);
+		contextPathAdaptorTracker = new ServiceTracker<ContextPathCustomizer, ContextPathCustomizer>(
+			consumingContext, ContextPathCustomizer.class, contextPathCustomizerHolder);
+		contextPathAdaptorTracker.open();
+
 		contextServiceTracker.open();
 
 		Hashtable<String, Object> defaultContextProps = new Hashtable<String, Object>();
@@ -106,6 +112,7 @@
 		if (contextPath == null || contextPath.equals(Const.SLASH)) {
 			contextPath = Const.BLANK;
 		}
+		contextPath = adaptContextPath(contextPath, serviceReference);
 
 		long serviceId = (Long)serviceReference.getProperty(
 			Constants.SERVICE_ID);
@@ -124,6 +131,28 @@
 		return result;
 	}
 
+	private String adaptContextPath(String contextPath, ServiceReference<ServletContextHelper> helper) {
+		ContextPathCustomizer pathAdaptor = contextPathCustomizerHolder.getHighestRanked();
+		if (pathAdaptor != null) {
+			String contextPrefix = pathAdaptor.getContextPathPrefix(helper);
+			if (contextPrefix != null && !contextPrefix.isEmpty() && !contextPrefix.equals(Const.SLASH)) {
+				if (!contextPrefix.startsWith(Const.SLASH)) {
+					contextPrefix = Const.SLASH + contextPrefix;
+				}
+				return contextPrefix + contextPath;
+			}
+		}
+		return contextPath;
+	}
+
+	public String getDefaultContextSelectFilter(ServiceReference<?> httpWhiteBoardService) {
+		ContextPathCustomizer pathAdaptor = contextPathCustomizerHolder.getHighestRanked();
+		if (pathAdaptor != null) {
+			return pathAdaptor.getDefaultContextSelectFilter(httpWhiteBoardService);
+		}
+		return null;
+	}
+
 	public ContextController addServletContextHelper(
 		ServiceReference<ServletContextHelper> servletContextHelperRef,
 		String contextName, String contextPath, long serviceId,
@@ -172,7 +201,10 @@
 
 	public void destroy() {
 		defaultContextReg.unregister();
+
 		contextServiceTracker.close();
+		contextPathAdaptorTracker.close();
+
 		controllerMap.clear();
 		contextPathMap.clear();
 		registeredObjects.clear();
@@ -185,6 +217,7 @@
 		parentServletContext = null;
 		registeredObjects = null;
 		contextServiceTracker = null;
+		contextPathCustomizerHolder = null;
 	}
 
 	public boolean doDispatch(
@@ -210,18 +243,6 @@
 		return initParameters;
 	}
 
-	public ContextController getContextController(
-		org.osgi.framework.Filter targetFilter) {
-
-		for (ContextController contextController : controllerMap.keySet()) {
-			if (contextController.matches(targetFilter)) {
-				return contextController;
-			}
-		}
-
-		return null;
-	}
-
 	public Set<Object> getRegisteredObjects() {
 		return registeredObjects;
 	}
@@ -981,6 +1002,8 @@
 	private Set<String> registeredContextNames = new ConcurrentSkipListSet<String>();
 
 	private ServiceTracker<ServletContextHelper, AtomicReference<ContextController>> contextServiceTracker;
+	private ServiceTracker<ContextPathCustomizer, ContextPathCustomizer> contextPathAdaptorTracker;
+	private ContextPathCustomizerHolder contextPathCustomizerHolder;
 
 	static class DefaultServletContextHelperFactory implements ServiceFactory<ServletContextHelper> {
 		@Override
@@ -1045,4 +1068,65 @@
 		
 	}
 
+	static class ContextPathCustomizerHolder implements ServiceTrackerCustomizer<ContextPathCustomizer, ContextPathCustomizer> {
+		private final BundleContext context;
+		private final ServiceTracker<ServletContextHelper, AtomicReference<ContextController>> contextServiceTracker;
+		private final NavigableMap<ServiceReference<ContextPathCustomizer>, ContextPathCustomizer> pathCustomizers =
+			new TreeMap<ServiceReference<ContextPathCustomizer>, ContextPathCustomizer>(Collections.reverseOrder());
+
+		public ContextPathCustomizerHolder(
+			BundleContext context,
+			ServiceTracker<ServletContextHelper, AtomicReference<ContextController>> contextServiceTracker) {
+			super();
+			this.context = context;
+			this.contextServiceTracker = contextServiceTracker;
+		}
+
+		@Override
+		public ContextPathCustomizer addingService(
+			ServiceReference<ContextPathCustomizer> reference) {
+			ContextPathCustomizer service = context.getService(reference);
+			boolean reset = false;
+			synchronized (pathCustomizers) {
+				pathCustomizers.put(reference, service);
+				reset = pathCustomizers.firstKey().equals(reference);
+			}
+			if (reset) {
+				contextServiceTracker.close();
+				contextServiceTracker.open();
+			}
+			return service;
+		}
+
+		@Override
+		public void modifiedService(
+			ServiceReference<ContextPathCustomizer> reference,
+			ContextPathCustomizer service) {
+			removedService(reference, service);
+			addingService(reference);
+		}
+		@Override
+		public void removedService(
+			ServiceReference<ContextPathCustomizer> reference,
+			ContextPathCustomizer service) {
+			boolean reset = false;
+			synchronized (pathCustomizers) {
+				ServiceReference<ContextPathCustomizer> currentFirst = pathCustomizers.firstKey();
+				pathCustomizers.remove(reference);
+				reset = currentFirst.equals(reference);
+			}
+			if (reset) {
+				contextServiceTracker.close();
+				contextServiceTracker.open();
+			}
+			context.ungetService(reference);
+		}
+
+		ContextPathCustomizer getHighestRanked() {
+			synchronized (pathCustomizers) {
+				Map.Entry<ServiceReference<ContextPathCustomizer>, ContextPathCustomizer> firstEntry = pathCustomizers.firstEntry();
+				return firstEntry == null ? null : firstEntry.getValue();
+			}
+		}
+	}
 }
diff --git a/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/context/ContextController.java b/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/context/ContextController.java
index 871ce6b..8d92431 100644
--- a/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/context/ContextController.java
+++ b/bundles/org.eclipse.equinox.http.servlet/src/org/eclipse/equinox/http/servlet/internal/context/ContextController.java
@@ -760,9 +760,12 @@
 		String contextSelector = (String) whiteBoardService.getProperty(
 			HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT);
 		if (contextSelector == null) {
-			contextSelector = "(" + //$NON-NLS-1$
-				HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" //$NON-NLS-1$
-				+ HttpWhiteboardConstants.HTTP_WHITEBOARD_DEFAULT_CONTEXT_NAME + ")"; //$NON-NLS-1$
+			contextSelector = httpServiceRuntime.getDefaultContextSelectFilter(whiteBoardService);
+			if (contextSelector == null) {
+				contextSelector = "(" + //$NON-NLS-1$
+					HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" //$NON-NLS-1$
+					+ HttpWhiteboardConstants.HTTP_WHITEBOARD_DEFAULT_CONTEXT_NAME + ")"; //$NON-NLS-1$
+			}
 		}
 
 		if (!contextSelector.startsWith(Const.OPEN_PAREN)) {