FELIX-3780 - allow DA to be configured by ConfigAdmin:

- register the DA implementation also as managed service with PID
  "org.apache.felix.deploymentadmin";
- it accepts a single property "stopUnaffectedBundle" which should
  be "true" or "false";
- when no configuration is supplied, the fallback is to use the old
  framework/system property: 
  "org.apache.felix.deploymentadmin.stopunaffectedbundle".



git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1588290 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java
index 7013e78..217a84a 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java
@@ -18,9 +18,13 @@
  */
 package org.apache.felix.deploymentadmin;
 
+import java.util.Dictionary;
+import java.util.Hashtable;
+
 import org.apache.felix.dm.DependencyActivatorBase;
 import org.apache.felix.dm.DependencyManager;
 import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ManagedService;
 import org.osgi.service.deploymentadmin.DeploymentAdmin;
 import org.osgi.service.event.EventAdmin;
 import org.osgi.service.log.LogService;
@@ -34,8 +38,13 @@
 public class Activator extends DependencyActivatorBase {
 
     public void init(BundleContext context, DependencyManager manager) throws Exception {
+        String[] ifaces = { DeploymentAdmin.class.getName(), ManagedService.class.getName() };
+        
+        Dictionary props = new Hashtable();
+        props.put(Constants.SERVICE_PID, DeploymentAdminImpl.PID);
+        
         manager.add(createComponent()
-            .setInterface(DeploymentAdmin.class.getName(), null)
+            .setInterface(ifaces, props)
             .setImplementation(DeploymentAdminImpl.class)
             .add(createServiceDependency()
                 .setService(PackageAdmin.class)
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
index 4a3b497..70ff456 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
@@ -48,6 +48,8 @@
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Version;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
 import org.osgi.service.deploymentadmin.DeploymentAdmin;
 import org.osgi.service.deploymentadmin.DeploymentException;
 import org.osgi.service.deploymentadmin.DeploymentPackage;
@@ -56,7 +58,9 @@
 import org.osgi.service.log.LogService;
 import org.osgi.service.packageadmin.PackageAdmin;
 
-public class DeploymentAdminImpl implements DeploymentAdmin {
+public class DeploymentAdminImpl implements DeploymentAdmin, ManagedService {
+    /** Configuration PID used to dynamically configure DA at runtime. */
+    public static final String PID = "org.apache.felix.deploymentadmin";
 
     public static final String PACKAGE_DIR = "packages";
     public static final String TEMP_DIR = "temp";
@@ -66,19 +70,31 @@
     public static final String TEMP_POSTFIX = "";
 
     private static final long TIMEOUT = 10000;
+    /** Configuration key used for dynamic configuration of DA. */
+    public static final String KEY_STOP_UNAFFECTED_BUNDLE = "stopUnaffectedBundle";
 
     private volatile BundleContext m_context;       /* will be injected by dependencymanager */
     private volatile PackageAdmin m_packageAdmin;   /* will be injected by dependencymanager */
     private volatile EventAdmin m_eventAdmin;       /* will be injected by dependencymanager */
     private volatile LogService m_log;              /* will be injected by dependencymanager */
     private volatile DeploymentSessionImpl m_session = null;
+    private volatile Boolean m_stopUnaffectedBundles = null;
+    
     private final Map m_packages = new HashMap();
     private final Semaphore m_semaphore = new Semaphore();
 
     /**
-     * Create new instance of this <code>DeploymentAdmin</code>.
+     * Creates a new {@link DeploymentAdminImpl} instance.
      */
     public DeploymentAdminImpl() {
+        // Nop
+    }
+    
+    /**
+     * Creates a new {@link DeploymentAdminImpl} instance.
+     */
+    DeploymentAdminImpl(BundleContext context) {
+        m_context = context;
     }
 
     public boolean cancel() {
@@ -350,6 +366,37 @@
         }
     }
     
+    public void updated(Dictionary properties) throws ConfigurationException {
+        Boolean stopUnaffectedBundles = null;
+        if (properties != null) {
+            Object value = properties.get(KEY_STOP_UNAFFECTED_BUNDLE);
+            if (value == null || !(value instanceof String || value instanceof Boolean)) {
+                throw new ConfigurationException(KEY_STOP_UNAFFECTED_BUNDLE, "missing value!");
+            }
+
+            if (value instanceof Boolean) {
+                stopUnaffectedBundles = (Boolean) value;
+            } else {
+                stopUnaffectedBundles = Boolean.valueOf(value.toString());
+            }
+        }
+        m_stopUnaffectedBundles = stopUnaffectedBundles;
+    }
+    
+    public boolean isStopUnaffectedBundles() {
+        Boolean stopUnaffectedBundles = m_stopUnaffectedBundles;
+        if (stopUnaffectedBundles == null) {
+            String prop = m_context.getProperty(PID + "." + KEY_STOP_UNAFFECTED_BUNDLE);
+            if (prop == null) {
+                prop = m_context.getProperty(PID + "." + KEY_STOP_UNAFFECTED_BUNDLE.toLowerCase());
+            }
+            if (prop != null) {
+                stopUnaffectedBundles = Boolean.valueOf(prop);
+            }
+        }
+        return (stopUnaffectedBundles == null) ? true : stopUnaffectedBundles.booleanValue();
+    }
+    
     private List createInstallCommandChain() {
         List commandChain = new ArrayList();
 
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java
index 6c89186..425a7b1 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java
@@ -196,4 +196,8 @@
     public AbstractDeploymentPackage getSourceAbstractDeploymentPackage() {
         return m_source;
     }
+    
+    public boolean isStopUnaffectedBundles() {
+        return m_admin.isStopUnaffectedBundles();
+    }
 }
\ No newline at end of file
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java
index fa75d90..2d9d423 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java
@@ -41,8 +41,6 @@
 public class StopBundleCommand extends Command {
 
     protected void doExecute(DeploymentSessionImpl session) throws Exception {
-        String stopUnaffectedBundle = System.getProperty("org.apache.felix.deploymentadmin.stopunaffectedbundle", "true");
-
         LogService log = session.getLog();
 
         AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage();
@@ -54,7 +52,7 @@
             String symbolicName = bundleInfos[i].getSymbolicName();
             Bundle bundle = target.getBundle(symbolicName);
             if (bundle != null) {
-                if ("false".equalsIgnoreCase(stopUnaffectedBundle) && omitBundleStop(session, symbolicName)) {
+                if (omitBundleStop(session, symbolicName)) {
                     continue;
                 }
                 if (isFragmentBundle(bundle)) {
@@ -87,7 +85,9 @@
      *         deployment package. Returns <code>false</code> otherwise.
      */
     private boolean omitBundleStop(DeploymentSessionImpl session, String symbolicName) {
-        boolean result = false;
+        boolean stopUnaffectedBundle = session.isStopUnaffectedBundles();
+
+        boolean result = stopUnaffectedBundle;
         BundleInfoImpl sourceBundleInfo = session.getSourceAbstractDeploymentPackage().getBundleInfoByName(symbolicName);
         BundleInfoImpl targetBundleInfo = session.getTargetAbstractDeploymentPackage().getBundleInfoByName(symbolicName);
         boolean fixPackageMissing = sourceBundleInfo != null && sourceBundleInfo.isMissing();
diff --git a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/DeploymentAdminImplTest.java b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/DeploymentAdminImplTest.java
new file mode 100644
index 0000000..4c9c90b
--- /dev/null
+++ b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/DeploymentAdminImplTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.deploymentadmin;
+
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ConfigurationException;
+
+/**
+ * Test cases for {@link DeploymentAdminImpl}.
+ */
+public class DeploymentAdminImplTest extends TestCase
+{
+    private static final String CONF_KEY = DeploymentAdminImpl.KEY_STOP_UNAFFECTED_BUNDLE;
+    private static final String SYS_PROP = DeploymentAdminImpl.PID + "." + CONF_KEY.toLowerCase();
+    private static final String SYS_PROP_LOWERCASE = DeploymentAdminImpl.PID + "." + CONF_KEY.toLowerCase();
+
+    private final Map m_fwProperties = new HashMap();
+
+    /**
+     * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration.
+     */
+    public void testDefaultConfigurationOk()
+    {
+        DeploymentAdminImpl da = createDeploymentAdmin();
+
+        assertTrue(da.isStopUnaffectedBundles());
+    }
+
+    /**
+     * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration.
+     */
+    public void testExplicitConfigurationOk() throws ConfigurationException
+    {
+        Dictionary dict = new Hashtable();
+        dict.put(CONF_KEY, "false");
+
+        DeploymentAdminImpl da = createDeploymentAdmin();
+        da.updated(dict);
+
+        // Should use the explicit configured value...
+        assertFalse(da.isStopUnaffectedBundles());
+
+        da.updated(null);
+
+        // Should use the system wide value...
+        assertTrue(da.isStopUnaffectedBundles());
+    }
+
+    /**
+     * Tests that an explicit configuration cannot miss any properties. 
+     */
+    public void testExplicitConfigurationWithMissingValueFail() throws ConfigurationException
+    {
+        Dictionary dict = new Hashtable();
+
+        DeploymentAdminImpl da = createDeploymentAdmin();
+        try
+        {
+            da.updated(dict);
+            fail("ConfigurationException expected!");
+        }
+        catch (ConfigurationException e)
+        {
+            assertEquals(CONF_KEY, e.getProperty());
+        }
+    }
+
+    /**
+     * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration.
+     */
+    public void testFrameworkConfigurationOk()
+    {
+        m_fwProperties.put(SYS_PROP, "false");
+
+        DeploymentAdminImpl da = createDeploymentAdmin();
+
+        assertFalse(da.isStopUnaffectedBundles());
+    }
+
+    /**
+     * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration.
+     */
+    public void testSystemConfigurationOk()
+    {
+        System.setProperty(SYS_PROP, "false");
+
+        try
+        {
+            DeploymentAdminImpl da = createDeploymentAdmin();
+
+            assertFalse(da.isStopUnaffectedBundles());
+        }
+        finally
+        {
+            System.clearProperty(SYS_PROP);
+        }
+
+        System.setProperty(SYS_PROP_LOWERCASE, "false");
+
+        try
+        {
+            DeploymentAdminImpl da = createDeploymentAdmin();
+
+            assertFalse(da.isStopUnaffectedBundles());
+        }
+        finally
+        {
+            System.clearProperty(SYS_PROP_LOWERCASE);
+        }
+    }
+
+    protected void setUp() throws Exception
+    {
+        m_fwProperties.clear();
+    }
+
+    private DeploymentAdminImpl createDeploymentAdmin()
+    {
+        return new DeploymentAdminImpl(createMockBundleContext());
+    }
+
+    private BundleContext createMockBundleContext()
+    {
+        BundleContext result = (BundleContext) Mockito.mock(BundleContext.class);
+        Mockito.when(result.getProperty(Matchers.anyString())).thenAnswer(new Answer()
+        {
+            public Object answer(InvocationOnMock invocation) throws Throwable
+            {
+                String prop = (String) invocation.getArguments()[0];
+
+                Object result = m_fwProperties.get(prop);
+                if (result == null)
+                {
+                    result = System.getProperty(prop);
+                }
+                return result;
+            }
+        });
+        return result;
+    }
+}
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java
index b3e1bc5..7fac396 100644
--- a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java
@@ -48,6 +48,7 @@
 import org.osgi.framework.FrameworkListener;
 import org.osgi.framework.Version;
 import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.service.cm.ConfigurationAdmin;
 import org.osgi.service.deploymentadmin.DeploymentAdmin;
 import org.osgi.service.deploymentadmin.DeploymentException;
 import org.osgi.service.deploymentadmin.DeploymentPackage;
@@ -66,7 +67,9 @@
     @Inject
     protected volatile BundleContext m_context;
     @Inject
-    private volatile DeploymentAdmin m_deploymentAdmin;
+    protected volatile DeploymentAdmin m_deploymentAdmin;
+    @Inject
+    protected volatile ConfigurationAdmin m_configAdmin;
 
     protected volatile AtomicInteger m_gate = new AtomicInteger(0);
     protected volatile String m_testBundleBasePath;
diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java
index adfc51a..fa03aba 100644
--- a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java
+++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java
@@ -21,13 +21,18 @@
 import static org.osgi.service.deploymentadmin.DeploymentException.CODE_BUNDLE_NAME_ERROR;
 import static org.osgi.service.deploymentadmin.DeploymentException.CODE_OTHER_ERROR;
 
+import java.util.Dictionary;
+import java.util.Hashtable;
+
 import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder;
 import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder.JarManifestManipulatingFilter;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.service.cm.Configuration;
 import org.osgi.service.deploymentadmin.DeploymentAdmin;
 import org.osgi.service.deploymentadmin.DeploymentException;
+import org.osgi.service.deploymentadmin.DeploymentPackage;
 
 /**
  * Generic tests for {@link DeploymentAdmin}.
@@ -35,6 +40,52 @@
 @RunWith(PaxExam.class)
 public class DeploymentAdminTest extends BaseIntegrationTest {
 
+    /**
+     * Tests that we can update the configuration of {@link DeploymentAdmin} at runtime. Based on the test case for FELIX-4184, see 
+     * {@link org.apache.felix.deploymentadmin.itest.InstallFixPackageTest#testInstallAndUpdateImplementationBundleWithSeparateAPIBundle_FELIX4184()}
+     */
+    @Test
+    public void testUpdateConfigurationOk() throws Exception
+    {
+        Dictionary props = new Hashtable();
+        props.put("stopUnaffectedBundle", Boolean.FALSE);
+        
+        Configuration config = m_configAdmin.getConfiguration("org.apache.felix.deploymentadmin", null);
+        config.update(props);
+
+        Thread.sleep(100);
+
+        // This test case will only work if stopUnaffectedBundle is set to 'false'...
+        try {
+            // first, install a deployment package with implementation and api bundles in version 1.0.0
+            DeploymentPackageBuilder dpBuilder = createDeploymentPackageBuilder("a", "1.0.0");
+            dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundle("bundleimpl1", "bundleimpl1", "1.0.0")));
+            dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundle("bundleapi1", "bundleapi1", "1.0.0")));
+    
+            DeploymentPackage dp1 = installDeploymentPackage(dpBuilder);
+            assertNotNull("No deployment package returned?!", dp1);
+    
+            assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages());
+    
+            // then, install a fix package with implementation and api bundles in version 2.0.0
+            dpBuilder = createDeploymentPackageBuilder("a", "2.0.0").setFixPackage("[1.0.0,2.0.0]");
+            dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundle("bundleimpl2", "bundleimpl2", "2.0.0")));
+            dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundle("bundleapi2", "bundleapi2", "2.0.0")));
+
+            DeploymentPackage dp2 = installDeploymentPackage(dpBuilder);
+            assertNotNull("No deployment package returned?!", dp2);
+
+            awaitRefreshPackagesEvent();
+
+            assertBundleExists(getSymbolicName("bundleimpl"), "2.0.0");
+            assertBundleExists(getSymbolicName("bundleapi"), "2.0.0");
+            assertBundleNotExists(getSymbolicName("bundleimpl"), "1.0.0");
+            assertBundleNotExists(getSymbolicName("bundleapi"), "1.0.0");
+        } finally {
+            config.delete();
+        }
+    }
+
     @Test
     public void testBundleSymbolicNameMustMatchManifestEntry() throws Exception {
         DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0");