FELIX-6168 Enable WebConsole login only after specified Security Providers are present
WebConsoleSecurityProvider implementations can identify themselves by registering a service property
"webconsole.security.provider.id"="some.id"
The Web Console itself can then be configured through the OSGi Framework property:
"felix.webconsole.security.providers"="id1,id2"
The framework property is a comma-separated list of provider IDs. The Web Console will not start
until all listed security providers are present in the service registry.
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1865232 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/pom.xml b/webconsole/pom.xml
index 9bcade1..855d2c7 100644
--- a/webconsole/pom.xml
+++ b/webconsole/pom.xml
@@ -370,7 +370,7 @@
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.compendium</artifactId>
- <version>4.1.0</version>
+ <version>4.3.0</version>
<scope>provided</scope>
</dependency>
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 0eb211f..1203c56 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -24,6 +24,7 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
@@ -36,6 +37,7 @@
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
@@ -73,6 +75,7 @@
import org.osgi.service.http.HttpService;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
/**
* The <code>OSGi Manager</code> is the actual Web Console Servlet which
@@ -136,6 +139,10 @@
private static final String FRAMEWORK_PROP_LOCALE = "felix.webconsole.locale"; //$NON-NLS-1$
+ static final String FRAMEWORK_PROP_SECURITY_PROVIDERS = "felix.webconsole.security.providers"; //$NON-NLS-1$
+
+ static final String SECURITY_PROVIDER_PROPERTY_NAME = "webconsole.security.provider.id"; //$NON-NLS-1$
+
static final String PROP_MANAGER_ROOT = "manager.root"; //$NON-NLS-1$
static final String PROP_DEFAULT_RENDER = "default.render"; //$NON-NLS-1$
@@ -206,7 +213,7 @@
private HttpServiceTracker httpServiceTracker;
- private HttpService httpService;
+ private volatile HttpService httpService;
private PluginHolder holder;
@@ -239,6 +246,10 @@
private Set enabledPlugins;
+ final ConcurrentSkipListSet<String> registeredSecurityProviders = new ConcurrentSkipListSet<String>();
+
+ final Set<String> requiredSecurityProviders;
+
ResourceBundleManager resourceBundleManager;
private int logLevel = DEFAULT_LOG_LEVEL;
@@ -318,9 +329,12 @@
brandingTracker = new BrandingServiceTracker(this);
brandingTracker.open();
+ this.requiredSecurityProviders = splitCommaSeparatedString(bundleContext.getProperty(FRAMEWORK_PROP_SECURITY_PROVIDERS));
+
// add support for pluggable security
securityProviderTracker = new ServiceTracker(bundleContext,
- WebConsoleSecurityProvider.class.getName(), null);
+ WebConsoleSecurityProvider.class.getName(),
+ new UpdateDependenciesStateCustomizer());
securityProviderTracker.open();
// load the default configuration from the framework
@@ -382,6 +396,21 @@
} );
}
+ void updateRegistrationState() {
+ if (this.httpService != null) {
+ if (this.registeredSecurityProviders.containsAll(this.requiredSecurityProviders)) {
+ // register HTTP service
+ registerHttpService();
+ return;
+ } else {
+ log(LogService.LOG_INFO, "Not all requirements met for the Web Console. Required security providers: "
+ + this.registeredSecurityProviders + " Registered security providers: " + this.registeredSecurityProviders);
+ }
+ }
+ // Not all requirements met, unregister service.
+ unregisterHttpService();
+ }
+
public void dispose()
{
// dispose off held plugins
@@ -917,7 +946,7 @@
}
- protected synchronized void bindHttpService(HttpService httpService)
+ protected void bindHttpService(HttpService httpService)
{
// do not bind service, when we are already bound
if (this.httpService != null)
@@ -927,6 +956,11 @@
return;
}
+ this.httpService = httpService;
+ updateRegistrationState();
+ }
+
+ synchronized void registerHttpService() {
Map config = getConfiguration();
// get authentication details
@@ -937,7 +971,7 @@
// register the servlet and resources
try
{
- HttpContext httpContext = new OsgiManagerHttpContext(httpService,
+ HttpContext httpContext = new OsgiManagerHttpContext(bundleContext, httpService,
securityProviderTracker, userId, password, realm);
Dictionary servletConfig = toStringConfig(config);
@@ -957,11 +991,9 @@
{
log(LogService.LOG_ERROR, "bindHttpService: Problem setting up", e);
}
-
- this.httpService = httpService;
}
- protected synchronized void unbindHttpService(HttpService httpService)
+ protected void unbindHttpService(HttpService httpService)
{
if (this.httpService != httpService)
{
@@ -972,7 +1004,10 @@
// drop the service reference
this.httpService = null;
+ updateRegistrationState();
+ }
+ synchronized void unregisterHttpService() {
if (httpResourcesRegistered)
{
try
@@ -1149,6 +1184,20 @@
return stringConfig;
}
+ static Set<String> splitCommaSeparatedString(final String str) {
+ if (str == null)
+ return Collections.emptySet();
+
+ final Set<String> values = new HashSet<String>();
+ for (final String s : str.split(",")) {
+ String trimmed = s.trim();
+ if (trimmed.length() > 0) {
+ values.add(trimmed);
+ }
+ }
+ return Collections.unmodifiableSet(values);
+ }
+
private Map langMap;
@@ -1177,4 +1226,33 @@
return langMap = map;
}
+ class UpdateDependenciesStateCustomizer implements ServiceTrackerCustomizer {
+ @Override
+ public Object addingService(ServiceReference reference) {
+ Object nameObj = reference.getProperty(SECURITY_PROVIDER_PROPERTY_NAME);
+ if (nameObj instanceof String) {
+ String name = (String) nameObj;
+ registeredSecurityProviders.add(name);
+ updateRegistrationState();
+ }
+ return bundleContext.getService(reference);
+ }
+
+ @Override
+ public void modifiedService(ServiceReference reference, Object service) {
+ removedService(reference, service);
+ addingService(reference);
+ }
+
+ @Override
+ public void removedService(ServiceReference reference, Object service) {
+ Object nameObj = reference.getProperty(SECURITY_PROVIDER_PROPERTY_NAME);
+ if (nameObj instanceof String) {
+ String name = (String) nameObj;
+ registeredSecurityProviders.remove(name);
+ updateRegistrationState();
+ }
+ }
+
+ }
}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
index e7957ca..03cf735 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
@@ -25,6 +25,7 @@
import org.apache.felix.webconsole.WebConsoleSecurityProvider;
import org.apache.felix.webconsole.WebConsoleSecurityProvider2;
+import org.osgi.framework.BundleContext;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.util.tracker.ServiceTracker;
@@ -39,6 +40,8 @@
private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";
+ private final BundleContext bundleContext;
+
private final HttpContext base;
private final ServiceTracker tracker;
@@ -50,9 +53,11 @@
private final String realm;
- OsgiManagerHttpContext( HttpService httpService, final ServiceTracker tracker, final String username,
+ OsgiManagerHttpContext(final BundleContext bundleContext,
+ final HttpService httpService, final ServiceTracker tracker, final String username,
final String password, final String realm )
{
+ this.bundleContext = bundleContext;
this.tracker = tracker;
this.username = username;
this.password = new Password(password);
@@ -228,9 +233,12 @@
}
if ( this.username.equals( username ) && this.password.matches( password ) )
{
- return true;
+ if (bundleContext.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS) == null) {
+ // Only allow username and password authentication if no mandatory security providers are registered
+ return true;
+ }
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
new file mode 100644
index 0000000..2664100
--- /dev/null
+++ b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.webconsole.internal.servlet;
+
+import org.apache.felix.webconsole.WebConsoleSecurityProvider;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.http.HttpService;
+
+import java.lang.reflect.Method;
+
+import static org.junit.Assert.assertEquals;
+
+public class OsgiManagerHttpContextTest {
+ @Test
+ public void testAuthenticate() throws Exception {
+ BundleContext bc = Mockito.mock(BundleContext.class);
+ HttpService svc = Mockito.mock(HttpService.class);
+ OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(bc, svc, null, "foo", "bar", "blah");
+
+ Method authenticateMethod = OsgiManagerHttpContext.class.getDeclaredMethod(
+ "authenticate", new Class [] {Object.class, String.class, byte[].class});
+ authenticateMethod.setAccessible(true);
+
+ assertEquals(true, authenticateMethod.invoke(ctx, null, "foo", "bar".getBytes()));
+ assertEquals(false, authenticateMethod.invoke(ctx, null, "foo", "blah".getBytes()));
+
+ WebConsoleSecurityProvider sp = new TestSecurityProvider();
+ assertEquals(true, authenticateMethod.invoke(ctx, sp, "xxx", "yyy".getBytes()));
+ assertEquals("The default username and password should not be accepted with security provider",
+ false, authenticateMethod.invoke(ctx, sp, "foo", "bar".getBytes()));
+ }
+
+ @Test
+ public void testAuthenticatePwdDisabledWithRequiredSecurityProvider() throws Exception {
+ BundleContext bc = Mockito.mock(BundleContext.class);
+ Mockito.when(bc.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS)).thenReturn("a");
+
+ HttpService svc = Mockito.mock(HttpService.class);
+ OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(bc, svc, null, "foo", "bar", "blah");
+
+ Method authenticateMethod = OsgiManagerHttpContext.class.getDeclaredMethod(
+ "authenticate", new Class [] {Object.class, String.class, byte[].class});
+ authenticateMethod.setAccessible(true);
+
+ assertEquals("A required security provider is configured, logging in using "
+ + "username and password should be disabled",
+ false, authenticateMethod.invoke(ctx, null, "foo", "bar".getBytes()));
+ assertEquals(false, authenticateMethod.invoke(ctx, null, "foo", "blah".getBytes()));
+ assertEquals(false, authenticateMethod.invoke(ctx, null, "blah", "bar".getBytes()));
+
+ WebConsoleSecurityProvider sp = new TestSecurityProvider();
+ assertEquals(true, authenticateMethod.invoke(ctx, sp, "xxx", "yyy".getBytes()));
+ assertEquals(false, authenticateMethod.invoke(ctx, sp, "foo", "bar".getBytes()));
+ }
+
+ private static class TestSecurityProvider implements WebConsoleSecurityProvider {
+ @Override
+ public Object authenticate(String username, String password) {
+ if ("xxx".equals(username) && "yyy".equals(password))
+ return new Object();
+ return null;
+ }
+
+ @Override
+ public boolean authorize(Object user, String role) {
+ return false;
+ }
+ }
+}
diff --git a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java
new file mode 100644
index 0000000..d4ed351
--- /dev/null
+++ b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.webconsole.internal.servlet;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpService;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class OsgiManagerTest {
+ @Test
+ public void testSplitCommaSeparatedString() {
+ assertEquals(0, OsgiManager.splitCommaSeparatedString(null).size());
+ assertEquals(0, OsgiManager.splitCommaSeparatedString("").size());
+ assertEquals(0, OsgiManager.splitCommaSeparatedString(" ").size());
+ assertEquals(Collections.singleton("foo.bar"),
+ OsgiManager.splitCommaSeparatedString("foo.bar "));
+
+ Set<String> expected = new HashSet<String>();
+ expected.add("abc");
+ expected.add("x.y.z");
+ expected.add("123");
+ assertEquals(expected,
+ OsgiManager.splitCommaSeparatedString(" abc , x.y.z,123"));
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes", "serial" })
+ @Test
+ public void testUpdateDependenciesCustomizerAdd() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ final List<Boolean> updateCalled = new ArrayList<Boolean>();
+ OsgiManager mgr = new OsgiManager(bc) {
+ void updateRegistrationState() {
+ updateCalled.add(true);
+ }
+ };
+
+ ServiceTrackerCustomizer stc = mgr.new UpdateDependenciesStateCustomizer();
+
+ ServiceReference sref = Mockito.mock(ServiceReference.class);
+ stc.addingService(sref);
+ assertEquals(0, updateCalled.size());
+
+ ServiceReference sref2 = Mockito.mock(ServiceReference.class);
+ Mockito.when(sref2.getProperty(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME)).thenReturn("xyz");
+ stc.addingService(sref2);
+ assertEquals(Collections.singleton("xyz"), mgr.registeredSecurityProviders);
+ assertEquals(1, updateCalled.size());
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes", "serial" })
+ @Test
+ public void testUpdateDependenciesCustomzerRemove() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ final List<Boolean> updateCalled = new ArrayList<Boolean>();
+ OsgiManager mgr = new OsgiManager(bc) {
+ void updateRegistrationState() {
+ updateCalled.add(true);
+ }
+ };
+ mgr.registeredSecurityProviders.add("abc");
+ mgr.registeredSecurityProviders.add("xyz");
+
+ ServiceTrackerCustomizer stc = mgr.new UpdateDependenciesStateCustomizer();
+
+ ServiceReference sref = Mockito.mock(ServiceReference.class);
+ stc.removedService(sref, null);
+ assertEquals(0, updateCalled.size());
+ assertEquals(2, mgr.registeredSecurityProviders.size());
+ assertTrue(mgr.registeredSecurityProviders.contains("abc"));
+ assertTrue(mgr.registeredSecurityProviders.contains("xyz"));
+
+ ServiceReference sref2 = Mockito.mock(ServiceReference.class);
+ Mockito.when(sref2.getProperty(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME)).thenReturn("xyz");
+ stc.removedService(sref2, null);
+ assertEquals(Collections.singleton("abc"), mgr.registeredSecurityProviders);
+ assertEquals(1, updateCalled.size());
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testUpdateDependenciesCustomzerModified() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ OsgiManager mgr = new OsgiManager(bc);
+
+ final List<String> invocations = new ArrayList<String>();
+ ServiceTrackerCustomizer stc = mgr.new UpdateDependenciesStateCustomizer() {
+ @Override
+ public Object addingService(ServiceReference reference) {
+ invocations.add("added:" + reference);
+ return null;
+ }
+
+ @Override
+ public void removedService(ServiceReference reference, Object service) {
+ invocations.add("removed:" + reference);
+ }
+ };
+
+ ServiceReference sref = Mockito.mock(ServiceReference.class);
+ Mockito.when(sref.toString()).thenReturn("blah!");
+
+ assertEquals("Precondition", 0, invocations.size());
+ stc.modifiedService(sref, null);
+ assertEquals(2, invocations.size());
+ assertEquals("removed:blah!", invocations.get(0));
+ assertEquals("added:blah!", invocations.get(1));
+ }
+
+
+ @SuppressWarnings("serial")
+ @Test
+ public void testUpdateRegistrationStateNoRequiredProviders() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ final List<String> invocations = new ArrayList<String>();
+ OsgiManager mgr = new OsgiManager(bc) {
+ @Override
+ protected synchronized void registerHttpService() {
+ invocations.add("register");
+ }
+
+ @Override
+ protected synchronized void unregisterHttpService() {
+ invocations.add("unregister");
+ }
+ };
+
+ // HTTP Service not present -> unregister
+ mgr.updateRegistrationState();
+ assertEquals(Collections.singletonList("unregister"), invocations);
+
+ // HTTP Service present, no required providers, no registered providers -> register
+ invocations.clear();
+ mgr.registeredSecurityProviders.clear();
+ mgr.requiredSecurityProviders.clear();
+ setPrivateField(OsgiManager.class, mgr, "httpService", Mockito.mock(HttpService.class));
+ mgr.updateRegistrationState();
+ assertEquals(Collections.singletonList("register"), invocations);
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void testUpdateRegistrationStateSomeRequiredProviders() throws Exception {
+ BundleContext bc = mockBundleContext();
+ Mockito.when(bc.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS)).
+ thenReturn("foo,blah");
+
+ final List<String> invocations = new ArrayList<String>();
+ OsgiManager mgr = new OsgiManager(bc) {
+ @Override
+ protected synchronized void registerHttpService() {
+ invocations.add("register");
+ }
+
+ @Override
+ protected synchronized void unregisterHttpService() {
+ invocations.add("unregister");
+ }
+ };
+
+ // HTTP Service present, some required providers, no registered providers -> unregister
+ invocations.clear();
+ mgr.registeredSecurityProviders.clear();
+ setPrivateField(OsgiManager.class, mgr, "httpService", Mockito.mock(HttpService.class));
+ mgr.updateRegistrationState();
+ assertEquals(Collections.singletonList("unregister"), invocations);
+
+ // HTTP Service present, some required providers, more registered ones -> register
+ invocations.clear();
+ mgr.registeredSecurityProviders.addAll(Arrays.asList("foo", "bar", "blah"));
+ setPrivateField(OsgiManager.class, mgr, "httpService", Mockito.mock(HttpService.class));
+ mgr.updateRegistrationState();
+ assertEquals(Collections.singletonList("register"), invocations);
+
+ // HTTP Service present, some required providers, different registered ones -> unregister
+ invocations.clear();
+ mgr.registeredSecurityProviders.clear();
+ mgr.registeredSecurityProviders.addAll(Arrays.asList("foo", "bar"));
+ setPrivateField(OsgiManager.class, mgr, "httpService", Mockito.mock(HttpService.class));
+ mgr.updateRegistrationState();
+ assertEquals(Collections.singletonList("unregister"), invocations);
+
+ // HTTP Service not present, some required providers, more registered ones -> unregister
+ invocations.clear();
+ mgr.registeredSecurityProviders.addAll(Arrays.asList("foo", "bar", "blah"));
+ setPrivateField(OsgiManager.class, mgr, "httpService", null);
+ mgr.updateRegistrationState();
+ assertEquals(Collections.singletonList("unregister"), invocations);
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void testBindService() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ final List<Boolean> updateCalled = new ArrayList<Boolean>();
+ OsgiManager mgr = new OsgiManager(bc) {
+ void updateRegistrationState() {
+ updateCalled.add(true);
+ }
+ };
+
+ assertEquals("Precondition", 0, updateCalled.size());
+
+ HttpService svc = Mockito.mock(HttpService.class);
+ mgr.bindHttpService(svc);
+ assertSame(svc, getPrivateField(OsgiManager.class, mgr, "httpService"));
+ assertEquals(1, updateCalled.size());
+
+ updateCalled.clear();
+ mgr.bindHttpService(null);
+ assertSame(svc, getPrivateField(OsgiManager.class, mgr, "httpService"));
+ assertEquals(0, updateCalled.size());
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void testUnbindService() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ final List<Boolean> updateCalled = new ArrayList<Boolean>();
+ OsgiManager mgr = new OsgiManager(bc) {
+ void updateRegistrationState() {
+ updateCalled.add(true);
+ }
+ };
+
+ HttpService svc = Mockito.mock(HttpService.class);
+ mgr.bindHttpService(svc);
+ assertSame(svc, getPrivateField(OsgiManager.class, mgr, "httpService"));
+ assertEquals(1, updateCalled.size());
+
+ updateCalled.clear();
+ mgr.unbindHttpService(null);
+ assertEquals(0, updateCalled.size());
+ assertSame(svc, getPrivateField(OsgiManager.class, mgr, "httpService"));
+
+ updateCalled.clear();
+ // unbind a different service, this should be ignored
+ mgr.unbindHttpService(Mockito.mock(HttpService.class));
+ assertEquals(0, updateCalled.size());
+ assertSame(svc, getPrivateField(OsgiManager.class, mgr, "httpService"));
+
+ updateCalled.clear();
+ // unbind the bound service, this should remove it
+ mgr.unbindHttpService(svc);
+ assertEquals(1, updateCalled.size());
+ assertNull(getPrivateField(OsgiManager.class, mgr, "httpService"));
+ }
+
+ private Object getPrivateField(Class<?> cls, Object obj, String field) throws Exception {
+ Field f = cls.getDeclaredField(field);
+ f.setAccessible(true);
+ return f.get(obj);
+ }
+
+ private void setPrivateField(Class<?> cls, Object obj, String field, Object value) throws Exception {
+ Field f = cls.getDeclaredField(field);
+ f.setAccessible(true);
+ f.set(obj, value);
+ }
+
+ private BundleContext mockBundleContext() throws InvalidSyntaxException {
+ Bundle bundle = Mockito.mock(Bundle.class);
+ BundleContext bc = Mockito.mock(BundleContext.class);
+ Mockito.when(bc.getBundle()).thenReturn(bundle);
+ Mockito.when(bundle.getBundleContext()).thenReturn(bc);
+ Mockito.when(bc.createFilter(Mockito.anyString())).then(new Answer<Filter>() {
+ @Override
+ public Filter answer(InvocationOnMock invocation) throws Throwable {
+ String fs = invocation.getArgumentAt(0, String.class);
+ return FrameworkUtil.createFilter(fs);
+ }
+ });
+ return bc;
+ }
+}