Author: davidb Date: Thu Aug 15 15:08:31 2019 New Revision: 1865232 URL: http://svn.apache.org/viewvc?rev=1865232&view=rev Log: 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. Added: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java Modified: felix/trunk/webconsole/pom.xml felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java Modified: felix/trunk/webconsole/pom.xml URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/pom.xml?rev=1865232&r1=1865231&r2=1865232&view=diff ============================================================================== --- felix/trunk/webconsole/pom.xml (original) +++ felix/trunk/webconsole/pom.xml Thu Aug 15 15:08:31 2019 @@ -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> Modified: felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java?rev=1865232&r1=1865231&r2=1865232&view=diff ============================================================================== --- felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java (original) +++ felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java Thu Aug 15 15:08:31 2019 @@ -24,6 +24,7 @@ import java.security.PrivilegedException 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; 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.HttpContext 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 @@ public class OsgiManager extends Generic 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 @@ public class OsgiManager extends Generic private HttpServiceTracker httpServiceTracker; - private HttpService httpService; + private volatile HttpService httpService; private PluginHolder holder; @@ -239,6 +246,10 @@ public class OsgiManager extends Generic 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 @@ public class OsgiManager extends Generic 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 @@ public class OsgiManager extends Generic } ); } + 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 @@ public class OsgiManager extends Generic } - 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 @@ public class OsgiManager extends Generic return; } + this.httpService = httpService; + updateRegistrationState(); + } + + synchronized void registerHttpService() { Map config = getConfiguration(); // get authentication details @@ -937,7 +971,7 @@ public class OsgiManager extends Generic // 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 @@ public class OsgiManager extends Generic { 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 @@ public class OsgiManager extends Generic // drop the service reference this.httpService = null; + updateRegistrationState(); + } + synchronized void unregisterHttpService() { if (httpResourcesRegistered) { try @@ -1149,6 +1184,20 @@ public class OsgiManager extends Generic 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 @@ public class OsgiManager extends Generic 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(); + } + } + + } } Modified: felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java?rev=1865232&r1=1865231&r2=1865232&view=diff ============================================================================== --- felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java (original) +++ felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java Thu Aug 15 15:08:31 2019 @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletRes 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 @@ final class OsgiManagerHttpContext imple private static final String AUTHENTICATION_SCHEME_BASIC = "Basic"; + private final BundleContext bundleContext; + private final HttpContext base; private final ServiceTracker tracker; @@ -50,9 +53,11 @@ final class OsgiManagerHttpContext imple 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 @@ final class OsgiManagerHttpContext imple } 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 +} Added: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java?rev=1865232&view=auto ============================================================================== --- felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java (added) +++ felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java Thu Aug 15 15:08:31 2019 @@ -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; + } + } +} Added: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java?rev=1865232&view=auto ============================================================================== --- felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java (added) +++ felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java Thu Aug 15 15:08:31 2019 @@ -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; + } +}