SLING-8568 Allow the API Regions runtime to be disabled using dynamic configuration
The API Regions fragment is enabled early using the framework property
org.apache.sling.feature.apiregions.regions=*
This commit allows the API Regions fragment to be disabled dynamically
using ConfigAdmin configuration. To disable the API regions runtime set
the following configuration:
PID: org.apache.sling.feature.apiregions.impl
disable=true
diff --git a/README.md b/README.md
index 4200f52..91fa347 100644
--- a/README.md
+++ b/README.md
@@ -31,3 +31,20 @@
org.apache.sling.feature.apiregions.regions=*
If this framework property is not set the component will be disabled.
+
+### Runtime Configuration
+If this component runs in a framework with Configuration Admin present, and it is set to be enabled using the framework property, it can be disabled at runtime
+through Configuration Admin configuration.
+
+Runtime configuration supported:
+
+**PID**: `org.apache.sling.feature.apiregions.impl`
+
+Key | Value
+--- | ---
+`disable` | if `true` then the API Regions component is disabled. Otherwise the component is enabled.
+
+No meta type is defined for this configuration since it's not a typical user setting. However, when using the web console
+the configuration can be created using `curl`:
+
+ curl -u <user>:<pass> -X POST -d "apply=true" -d "propertylist=disable" -d "disable=true" http://localhost:8080/system/console/configMgr/org.apache.sling.feature.apiregions.impl
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index cc15dc0..a73104d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,7 @@
<exclude>src/test/resources/*</exclude>
<exclude>src/test/resources/props1/*</exclude>
<exclude>src/test/resources/props2/*</exclude>
+ <exclude>src/test/resources/props3/*</exclude>
</excludes>
</configuration>
</plugin>
@@ -92,5 +93,11 @@
<version>2.27.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.cmpn</artifactId>
+ <version>6.0.0</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java b/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
index e86c38a..190e29b 100644
--- a/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
@@ -20,32 +20,166 @@
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.hooks.resolver.ResolverHookFactory;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
+import java.util.Map;
import java.util.logging.Level;
-public class Activator implements BundleActivator {
+public class Activator implements BundleActivator, FrameworkListener {
+ static final String MANAGED_SERVICE_PKG_NAME = "org.osgi.service.cm";
+ static final String MANAGED_SERVICE_CLASS_NAME = MANAGED_SERVICE_PKG_NAME + ".ManagedService";
static final String REGIONS_PROPERTY_NAME = "org.apache.sling.feature.apiregions.regions";
+ BundleContext bundleContext;
+ ServiceRegistration<ResolverHookFactory> hookRegistration;
+
@Override
public synchronized void start(BundleContext context) throws Exception {
- String regions = context.getProperty(REGIONS_PROPERTY_NAME);
+ bundleContext = context;
+
+ registerHook();
+
+ context.addFrameworkListener(this);
+ }
+
+ @Override
+ public synchronized void stop(BundleContext context) throws Exception {
+ // All services automatically get unregistered by the framework.
+ }
+
+ synchronized void registerHook() {
+ if (hookRegistration != null)
+ return; // There is already a hook, no need to re-register
+
+ String regions = bundleContext.getProperty(REGIONS_PROPERTY_NAME);
if (regions == null)
return; // Component not enabled
Dictionary<String, Object> props = new Hashtable<>();
try {
- RegionEnforcer enforcer = new RegionEnforcer(context, props, regions);
- context.registerService(ResolverHookFactory.class, enforcer, props);
+ RegionEnforcer enforcer = new RegionEnforcer(bundleContext, props, regions);
+ hookRegistration = bundleContext.registerService(ResolverHookFactory.class, enforcer, props);
} catch (Exception e) {
RegionEnforcer.LOG.log(Level.SEVERE, "Problem activating API Regions runtime enforcement component", e);
}
}
+ synchronized void unregisterHook() {
+ if (hookRegistration != null) {
+ hookRegistration.unregister();
+ hookRegistration = null;
+ }
+ }
+
@Override
- public synchronized void stop(BundleContext context) throws Exception {
- // Nothing to do
+ public void frameworkEvent(FrameworkEvent event) {
+ if (event.getType() == FrameworkEvent.STARTED) {
+ bundleContext.removeFrameworkListener(this);
+
+ FrameworkWiring fw = bundleContext.getBundle().adapt(FrameworkWiring.class);
+ if (fw == null) {
+ RegionEnforcer.LOG.log(Level.WARNING, "The API Regions runtime fragment is not attached to the system bundle.");
+ return;
+ }
+
+ Requirement cmReq = createPackageRequirement();
+
+ // Reflectively register a Configuration Admin ManagedService, if the Config Admin API is available.
+ // Because this fragment is a framework extension, we need to use the wiring API to find the CM API.
+ Collection<BundleCapability> providers = fw.findProviders(cmReq);
+ for (BundleCapability cap : providers) {
+ try {
+ ClassLoader loader = cap.getRevision().getWiring().getClassLoader();
+ Class<?> msClass = loader.loadClass(MANAGED_SERVICE_CLASS_NAME);
+ Object ms = Proxy.newProxyInstance(loader, new Class[] {msClass}, new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ Class<?> mdDecl = method.getDeclaringClass();
+ if (mdDecl.equals(Object.class)) {
+ switch (method.getName()) {
+ case "equals" :
+ return proxy == args[0];
+ case "hashCode" :
+ return System.identityHashCode(proxy);
+ case "toString" :
+ return "Proxy for " + msClass;
+ default :
+ throw new UnsupportedOperationException("Method " + method
+ + " not supported on proxy for " + msClass);
+ }
+ }
+ if ("updated".equals(method.getName())) {
+ if (args.length == 1) {
+ Object arg = args[0];
+ if (arg == null) {
+ registerHook();
+ } else if (arg instanceof Dictionary) {
+ Dictionary<?,?> props = (Dictionary<?,?>) args[0];
+ Object disabled = props.get("disable");
+ if ("true".equals(disabled)) {
+ unregisterHook();
+ } else {
+ registerHook();
+ }
+ }
+ }
+ }
+ return null;
+ }
+ });
+ Dictionary<String, Object> props = new Hashtable<>();
+ props.put(Constants.SERVICE_PID, getClass().getPackage().getName());
+ bundleContext.registerService(MANAGED_SERVICE_CLASS_NAME, ms, props);
+
+ return; // ManagedService registration successful. Exit method.
+ } catch (Exception e) {
+ RegionEnforcer.LOG.log(Level.WARNING, "Problem attempting to register ManagedService from " + cap, e);
+ }
+ }
+ RegionEnforcer.LOG.log(Level.INFO, "No Configuration Admin API available");
+ }
+ }
+
+ static Requirement createPackageRequirement() {
+ Requirement cmReq = new Requirement() {
+ @Override
+ public String getNamespace() {
+ return PackageNamespace.PACKAGE_NAMESPACE;
+ }
+
+ @Override
+ public Map<String, String> getDirectives() {
+ return Collections.singletonMap("filter",
+ "(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + MANAGED_SERVICE_PKG_NAME + ")");
+ }
+
+ @Override
+ public Map<String, Object> getAttributes() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Resource getResource() {
+ return null;
+ }
+
+ };
+ return cmReq;
}
}
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/ActivatorTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/ActivatorTest.java
index c2f13d1..7deb0ea 100644
--- a/src/test/java/org/apache/sling/feature/apiregions/impl/ActivatorTest.java
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/ActivatorTest.java
@@ -22,12 +22,28 @@
import org.junit.Before;
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.FrameworkEvent;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.hooks.resolver.ResolverHookFactory;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.resource.Requirement;
+import org.osgi.service.cm.ManagedService;
import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
+import java.util.List;
import java.util.Properties;
import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.BUNDLE_FEATURE_FILENAME;
@@ -35,6 +51,9 @@
import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.IDBSNVER_FILENAME;
import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.PROPERTIES_RESOURCE_PREFIX;
import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.REGION_PACKAGE_FILENAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
public class ActivatorTest {
private Properties savedProps;
@@ -82,5 +101,178 @@
Mockito.eq(ResolverHookFactory.class),
Mockito.isA(RegionEnforcer.class),
Mockito.eq(expectedProps));
+
+ Mockito.verify(bc).addFrameworkListener(a);
+ }
+
+ @Test
+ public void testRegistryHookNotEnabled() {
+ BundleContext bc = Mockito.mock(BundleContext.class);
+
+ Activator a = new Activator();
+ a.bundleContext = bc;
+ a.registerHook();
+
+ assertNull(a.hookRegistration);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testRegistryHookAlreadyPresent() {
+ BundleContext bc = Mockito.mock(BundleContext.class);
+
+ Activator a = new Activator();
+ a.bundleContext = bc;
+ a.hookRegistration = Mockito.mock(ServiceRegistration.class);
+ a.registerHook();
+
+ assertNotNull(a.hookRegistration);
+ Mockito.verifyZeroInteractions(bc);
+ }
+
+ @Test
+ public void testUnregisterHook() {
+ Activator a = new Activator();
+ a.unregisterHook(); // Should not throw an exception
+ assertNull(a.hookRegistration);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testUnregisterHook2() {
+ ServiceRegistration reg = Mockito.mock(ServiceRegistration.class);
+
+ Activator a = new Activator();
+ a.hookRegistration = reg;
+
+ a.unregisterHook();
+ Mockito.verify(reg).unregister();
+ assertNull(a.hookRegistration);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testFrameworkEvent() throws Exception {
+ String resourceDir = new File(getClass().getResource("/props3/idbsnver.properties").getFile()).getParent();
+
+ BundleWiring bw = Mockito.mock(BundleWiring.class);
+ Mockito.when(bw.getClassLoader()).thenReturn(getClass().getClassLoader());
+ BundleRevision rev = Mockito.mock(BundleRevision.class);
+ Mockito.when(rev.getWiring()).thenReturn(bw);
+ BundleCapability cap = Mockito.mock(BundleCapability.class);
+ Mockito.when(cap.getRevision()).thenReturn(rev);
+
+ FrameworkWiring wiring = Mockito.mock(FrameworkWiring.class);
+ Mockito.when(wiring.findProviders(Mockito.any(Requirement.class))).thenAnswer(
+ new Answer<Collection<BundleCapability>>() {
+ @Override
+ public Collection<BundleCapability> answer(InvocationOnMock invocation) throws Throwable {
+ Requirement req = invocation.getArgument(0);
+ if ("osgi.wiring.package".equals(req.getNamespace())) {
+ if ("(osgi.wiring.package=org.osgi.service.cm)".equals(req.getDirectives().get("filter"))) {
+ return Collections.singleton(cap);
+ }
+ }
+ return null;
+ }
+ });
+
+
+ Bundle fw = Mockito.mock(Bundle.class);
+ Mockito.when(fw.adapt(FrameworkWiring.class)).thenReturn(wiring);
+
+ List<Object> managedServices = new ArrayList<Object>();
+ BundleContext bc = Mockito.mock(BundleContext.class);
+ Mockito.when(bc.getBundle()).thenReturn(fw);
+ Mockito.when(bc.getProperty(Activator.REGIONS_PROPERTY_NAME)).thenReturn("*");
+ Mockito.when(bc.getProperty(RegionEnforcer.PROPERTIES_FILE_LOCATION)).thenReturn(resourceDir);
+ Mockito.when(bc.registerService(
+ Mockito.eq("org.osgi.service.cm.ManagedService"),
+ Mockito.any(),
+ Mockito.any(Dictionary.class))).thenAnswer(new Answer<ServiceRegistration<?>>() {
+ @Override
+ public ServiceRegistration<?> answer(InvocationOnMock invocation) throws Throwable {
+ Dictionary<String,?> dict = invocation.getArgument(2);
+ if ("org.apache.sling.feature.apiregions.impl".equals(dict.get("service.pid"))) {
+ managedServices.add(invocation.getArgument(1));
+ }
+ return Mockito.mock(ServiceRegistration.class);
+ }
+ });
+ Mockito.when(bc.registerService(
+ Mockito.eq(ResolverHookFactory.class),
+ Mockito.isA(RegionEnforcer.class),
+ Mockito.any(Dictionary.class))).thenReturn(Mockito.mock(ServiceRegistration.class));
+
+
+ Activator a = new Activator();
+ a.bundleContext = bc;
+
+ FrameworkEvent ev = Mockito.mock(FrameworkEvent.class);
+ Mockito.when(ev.getType()).thenReturn(FrameworkEvent.STARTED);
+
+ a.frameworkEvent(ev);
+ Mockito.verify(bc).removeFrameworkListener(a);
+ assertEquals(1, managedServices.size());
+ ManagedService managedService = (ManagedService) managedServices.get(0);
+
+ assertNull("Precondition", a.hookRegistration);
+ managedService.updated(null);
+ assertNotNull(a.hookRegistration);
+
+ ServiceRegistration<ResolverHookFactory> hookReg = a.hookRegistration;
+ Mockito.verifyZeroInteractions(hookReg);
+ managedService.updated(new Hashtable<>(Collections.singletonMap("disable", "true")));
+ Mockito.verify(hookReg).unregister();
+ assertNull(a.hookRegistration);
+
+ managedService.updated(new Hashtable<>(Collections.singletonMap("disable", "false")));
+ assertNotNull(a.hookRegistration);
+ }
+
+ @Test
+ public void testFrameworkEventNoCMProviders() {
+ FrameworkWiring wiring = Mockito.mock(FrameworkWiring.class);
+
+ Bundle fw = Mockito.mock(Bundle.class);
+ Mockito.when(fw.adapt(FrameworkWiring.class)).thenReturn(wiring);
+
+ BundleContext bc = Mockito.mock(BundleContext.class);
+ Mockito.when(bc.getBundle()).thenReturn(fw);
+
+ Activator a = new Activator();
+ a.bundleContext = bc;
+
+ FrameworkEvent ev = Mockito.mock(FrameworkEvent.class);
+ Mockito.when(ev.getType()).thenReturn(FrameworkEvent.STARTED);
+
+ a.frameworkEvent(ev);
+ Mockito.verify(bc).removeFrameworkListener(a);
+ }
+
+ @Test
+ public void testFrameworkEventNoSystemBundle() {
+ BundleContext bc = Mockito.mock(BundleContext.class);
+ Mockito.when(bc.getBundle()).thenReturn(Mockito.mock(Bundle.class));
+
+ Activator a = new Activator();
+ a.bundleContext = bc;
+
+ FrameworkEvent ev = Mockito.mock(FrameworkEvent.class);
+ Mockito.when(ev.getType()).thenReturn(FrameworkEvent.STARTED);
+
+ a.frameworkEvent(ev);
+ Mockito.verify(bc).removeFrameworkListener(a);
+ }
+
+ @Test
+ public void testCreatePackageRequirement() {
+ Requirement req = Activator.createPackageRequirement();
+ assertEquals(PackageNamespace.PACKAGE_NAMESPACE, req.getNamespace());
+ assertEquals(1, req.getDirectives().size());
+ String directive = req.getDirectives().get("filter");
+ assertEquals("(osgi.wiring.package=org.osgi.service.cm)", directive);
+ assertEquals(0, req.getAttributes().size());
+ assertNull(req.getResource());
}
}
diff --git a/src/test/resources/props3/bundles.properties b/src/test/resources/props3/bundles.properties
new file mode 100644
index 0000000..7497eaf
--- /dev/null
+++ b/src/test/resources/props3/bundles.properties
@@ -0,0 +1,5 @@
+#Generated at Sat Nov 03 10:58:58 GMT 2018
+#Sat Nov 03 10:58:58 GMT 2018
+org.sling\:b3\:1=some.other\:feature\:123,org.sling\:something\:1.2.3\:slingosgifeature\:myclassifier
+org.sling\:b2\:1=org.sling\:something\:1.2.3\:slingosgifeature\:myclassifier
+org.sling\:b1\:1=org.sling\:something\:1.2.3\:slingosgifeature\:myclassifier
diff --git a/src/test/resources/props3/features.properties b/src/test/resources/props3/features.properties
new file mode 100644
index 0000000..9efad8d
--- /dev/null
+++ b/src/test/resources/props3/features.properties
@@ -0,0 +1,4 @@
+#Generated at Sat Nov 03 11:10:29 GMT 2018
+#Sat Nov 03 11:10:29 GMT 2018
+an.other\:feature\:123=global
+org.sling\:something\:1.2.3=internal,global
diff --git a/src/test/resources/props3/idbsnver.properties b/src/test/resources/props3/idbsnver.properties
new file mode 100644
index 0000000..00a740d
--- /dev/null
+++ b/src/test/resources/props3/idbsnver.properties
@@ -0,0 +1,6 @@
+#Generated at Sat Nov 03 10:26:37 GMT 2018
+#Sat Nov 03 10:26:37 GMT 2018
+g\:b2\:1.2.3=b2~1.2.3
+g\:b1\:1=b1~1.0.0
+g2\:b2\:1.2.4=b2~1.2.3
+
diff --git a/src/test/resources/props3/regions.properties b/src/test/resources/props3/regions.properties
new file mode 100644
index 0000000..a4982d7
--- /dev/null
+++ b/src/test/resources/props3/regions.properties
@@ -0,0 +1,4 @@
+#Generated at Sat Nov 03 11:10:29 GMT 2018
+#Sat Nov 03 11:10:29 GMT 2018
+internal=xyz
+global=d.e.f,test,a.b.c