Applied patch to improve error handling of managed bundles (FELIX-937), to
account for explicitly stopped bundles (FELIX-938), and to try to optimize
processing (FELIX-939).


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@749325 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
index 7c164cf..3eb6219 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
@@ -18,20 +18,40 @@
  */
 package org.apache.felix.fileinstall;
 
-/**
- * -DirectoryWatcher-
- *
- * This class runs a background task that checks a directory for new files or
- * removed files. These files can be configuration files or jars.
- */
 import java.io.*;
 import java.util.*;
+import java.net.URISyntaxException;
+import java.net.URI;
 
 import org.osgi.framework.*;
 import org.osgi.service.cm.*;
 import org.osgi.service.log.*;
 import org.osgi.service.packageadmin.*;
 
+/**
+ * -DirectoryWatcher-
+ *
+ * This class runs a background task that checks a directory for new files or
+ * removed files. These files can be configuration files or jars.
+ * For jar files, its behavior is defined below:
+ * - If there are new jar files, it installs them and optionally starts them.
+ *    - If it fails to install a jar, it does not try to install it again until
+ *      the jar has been modified.
+ *    - If it fail to start a bundle, it attempts to start it in following
+ *      iterations until it succeeds or the corresponding jar is uninstalled.
+ * - If some jar files have been deleted, it uninstalls them.
+ * - If some jar files have been updated, it updates them.
+ *    - If it fails to update a bundle, it tries to update it in following
+ *      iterations until it is successful.
+ * - If any bundle gets updated or uninstalled, it refreshes the framework
+ *   for the changes to take effect.
+ * - If it detects any new installations, uninstallations or updations,
+ *   it tries to start all the managed bundle unless it has been configured
+ *   to only install bundles.
+ *
+ * @author Peter Kriens
+ * @author Sanjeeb Sahoo
+ */
 public class DirectoryWatcher extends Thread
 {
     final static String ALIAS_KEY = "_alias_factory_pid";
@@ -46,6 +66,13 @@
     boolean startBundles = true; // by default, we start bundles.
     BundleContext context;
     boolean reported;
+    Map/* <String, Jar> */ currentManagedBundles = new HashMap();
+
+    // Represents jars that could not be installed
+    Map/* <String, Jar> */ installationFailures = new HashMap();
+
+    // Represents jars that could not be installed
+    Set/* <Bundle> */ startupFailures = new HashSet();
 
     public DirectoryWatcher(Dictionary properties, BundleContext context)
     {
@@ -79,17 +106,16 @@
         log(DIR + "            " + watchedDirectory.getAbsolutePath(), null);
         log(DEBUG + "          " + debug, null);
         log(START_NEW_BUNDLES + "          " + startBundles, null);
-        Map currentManagedBundles = new HashMap(); // location -> Long(time)
+        initializeCurrentManagedBundles();
         Map currentManagedConfigs = new HashMap(); // location -> Long(time)
-
         while (!interrupted())
         {
             try
             {
-                Set/* <String> */ installed = new HashSet();
+                Map/* <String, Jar> */ installed = new HashMap();
                 Set/* <String> */ configs = new HashSet();
                 traverse(installed, configs, watchedDirectory);
-                doInstalled(currentManagedBundles, installed);
+                doInstalled(installed);
                 doConfigs(currentManagedConfigs, configs);
                 Thread.sleep(poll);
             }
@@ -276,138 +302,55 @@
     }
 
     /**
-     * Install bundles that were discovered and uninstall bundles that are gone
-     * from the current state.
+     * This is the core of this class.
+     * Install bundles that were discovered, uninstall bundles that are gone
+     * from the current state and update the ones that have been changed.
+     * Keep {@link #currentManagedBundles} up-to-date.
      *
-     * @param current
-     *            A map location -> path that holds the current state
      * @param discovered
-     *            A set of paths that represent the just found bundles
+     *            A map of path to {@link Jar} that holds the discovered state
      */
-    void doInstalled(Map current, Set discovered)
+    void doInstalled(Map discovered)
     {
-        boolean refresh = false;
-        Bundle bundles[] = context.getBundles();
-        for (int i = 0; i < bundles.length; i++)
+        // Find out all the new, deleted and common bundles.
+        // new = discovered - current,
+        Set newBundles = new HashSet(discovered.values());
+        newBundles.removeAll(currentManagedBundles.values());
+
+        // deleted = current - discovered
+        Set deletedBundles = new HashSet(currentManagedBundles.values());
+        deletedBundles.removeAll(discovered.values());
+
+        // existing = intersection of current & discovered
+        Set existingBundles = new HashSet(discovered.values());
+        existingBundles.retainAll(currentManagedBundles.values());
+
+        // We do the operations in the following order:
+        // uninstall, update, install, refresh & start.
+        Collection uninstalledBundles = uninstall(deletedBundles);
+        Collection updatedBundles = update(existingBundles);
+        Collection installedBundles = install(newBundles);
+        if (uninstalledBundles.size() > 0 || updatedBundles.size() > 0)
         {
-            Bundle bundle = bundles[i];
-            String location = bundle.getLocation();
-            if (discovered.contains(location))
-            {
-                // We have a bundle that is already installed
-                // so we know it
-                discovered.remove(location);
-
-                File file = new File(location);
-
-                // Modified date does not work on the Nokia
-                // for some reason, so we take size into account
-                // as well.
-                long newSize = file.length();
-                Long oldSizeObj = (Long) current.get(location);
-                long oldSize = oldSizeObj == null ? 0 : oldSizeObj.longValue();
-
-                if (file.lastModified() > bundle.getLastModified() + 4000 && oldSize != newSize)
-                {
-                    try
-                    {
-                        // We treat this as an update, it is modified,,
-                        // different size, and it is present in the dir
-                        // as well as in the list of bundles.
-                        current.put(location, new Long(newSize));
-                        InputStream in = new FileInputStream(file);
-                        bundle.update(in);
-                        refresh = true;
-                        in.close();
-                        log("Updated " + location, null);
-                    }
-                    catch (Exception e)
-                    {
-                        log("Failed to update bundle ", e);
-                    }
-                }
-
-                // Fragments can not be started. All other
-                // bundles are started only if user has asked us to
-                // start bundles. No need to check status of bundles
-                // before starting, because OSGi treats this
-                // as a noop when the bundle is already started
-                if (startBundles && !isFragment(bundle))
-                {
-                    try
-                    {
-                        bundle.start();
-                    }
-                    catch (Exception e)
-                    {
-                        log("Fail to start bundle " + location, e);
-                    }
-                }
-            }
-            else
-            {
-                // Hmm. We found a bundle that looks like it came from our
-                // watched directory but we did not find it this round.
-                // Just remove it.
-                if (bundle.getLocation().startsWith(watchedDirectory.getAbsolutePath()))
-                {
-                    try
-                    {
-                        bundle.uninstall();
-                        refresh = true;
-                        log("Uninstalled " + location, null);
-                    }
-                    catch (Exception e)
-                    {
-                        log("failed to uninstall bundle: ", e);
-                    }
-                }
-            }
-        }
-
-        List starters = new ArrayList();
-        for (Iterator it = discovered.iterator(); it.hasNext();)
-        {
-            try
-            {
-                String path = (String) it.next();
-                File file = new File(path);
-                InputStream in = new FileInputStream(file);
-                Bundle bundle = context.installBundle(path, in);
-                in.close();
-                if (startBundles)
-                {
-                    // We do not start this bundle yet. We wait after
-                    // refresh because this will minimize the disruption
-                    // as well as temporary unresolved errors.
-                    starters.add(bundle);
-                }
-                log("Installed " + file.getAbsolutePath(), null);
-            }
-            catch (Exception e)
-            {
-                log("failed to install/start bundle: ", e);
-            }
-        }
-
-        if (refresh || starters.size() != 0)
-        {
+            // Refresh if any bundle got uninstalled or updated.
+            // This can lead to restart of recently updated bundles, but
+            // don't worry about that at this point of time.
             refresh();
-            for (Iterator b = starters.iterator(); b.hasNext();)
-            {
-                Bundle bundle = (Bundle) b.next();
-                if (!isFragment(bundle))
-                {
-                    try
-                    {
-                        bundle.start();
-                    }
-                    catch (BundleException e)
-                    {
-                        log("Error while starting a newly installed bundle", e);
-                    }
-                }
-            }
+        }
+
+        // Try to start all the bundles that we could not start last time.
+        // Make a copy, because start() changes the underlying collection
+        start(new HashSet(startupFailures));
+
+        if (startBundles
+                &&
+                (uninstalledBundles.size() > 0
+                        || updatedBundles.size() > 0
+                        || installedBundles.size() > 0))
+        {
+            // Something has changed in the system, so
+            // try to start all the bundles.
+            startAllBundles();
         }
     }
 
@@ -461,17 +404,17 @@
     }
 
     /**
-     * Traverse the directory and fill the map with the found jars and
-     * configurations keyed by the abs file path.
+     * Traverse the directory and fill the set with the found jars and
+     * configurations.
      *
      * @param jars
-     *            Returns the abspath -> file for found jars
+     *            Returns path -> {@link Jar} map for found jars
      * @param configs
      *            Returns the abspath -> file for found configurations
      * @param jardir
      *            The directory to traverse
      */
-    void traverse(Set jars, Set configs, File jardir)
+    void traverse(Map/* <String, Jar> */ jars, Set configs, File jardir)
     {
         String list[] = jardir.list();
         for (int i = 0; i < list.length; i++)
@@ -479,7 +422,8 @@
             File file = new File(jardir, list[i]);
             if (list[i].endsWith(".jar"))
             {
-                jars.add(file.getAbsolutePath());
+                Jar jar = new Jar(file);
+                jars.put(jar.getPath(), jar);
             }
             else if (list[i].endsWith(".cfg"))
             {
@@ -570,4 +514,260 @@
             // Ignore
         }
     }
-}
+
+    /**
+     * This method goes through all the currently installed bundles
+     * and returns information about those bundles whose location
+     * refers to a file in our {@link #watchedDirectory}.
+     */
+    private void initializeCurrentManagedBundles()
+    {
+        Bundle[] bundles = this.context.getBundles();
+        String watchedDirPath = watchedDirectory.toURI().normalize().getPath();
+        for (int i = 0; i < bundles.length; ++i)
+        {
+            try
+            {
+                final URI uri = new URI(bundles[i].getLocation());
+                if (uri.isOpaque())
+                {
+                    // We can't do any meaningful processing of Opaque URI.
+                    // e.g. Path component of an opaque URI is null
+                    continue;
+                }
+                String location =  uri.normalize().getPath();
+                final int index = location.lastIndexOf('/');
+                if (index != -1 && location.substring(0, index + 1).equals(watchedDirPath))
+                {
+                    // This bundle's location matches our watched dir path
+                    Jar jar = new Jar(bundles[i]);
+                    currentManagedBundles.put(jar.getPath(), jar);
+                }
+            }
+            catch (URISyntaxException e)
+            {
+                // Ignore and continue.
+                // This can never happen for bundles that have been installed
+                // by FileInstall, as we always use proper filepath as location.
+            }
+        }
+    }
+
+    /**
+     * This method installs a collection of jar files.
+     * @param jars Collection of {@link Jar} to be installed
+     * @return List of Bundles just installed
+     */
+    private Collection/* <Bundle> */ install(Collection jars)
+    {
+        List bundles = new ArrayList();
+        for (Iterator iter = jars.iterator(); iter.hasNext();)
+        {
+            Jar jar = (Jar) iter.next();
+
+            Bundle bundle = install(jar);
+            if (bundle != null)
+            {
+                bundles.add(bundle);
+            }
+        }
+        return bundles;
+    }
+
+    /**
+     * @param jars Collection of {@link Jar} to be uninstalled
+     * @return Collection of Bundles that got uninstalled
+     */
+    private Collection/* <Bundle> */ uninstall(Collection jars)
+    {
+        List bundles = new ArrayList();
+        for (Iterator iter = jars.iterator(); iter.hasNext();)
+        {
+            final Jar jar = (Jar) iter.next();
+            Bundle b = uninstall(jar);
+            if (b != null)
+            {
+                bundles.add(b);
+            }
+        }
+        return bundles;
+    }
+
+    private void start(Collection bundles)
+    {
+        for (Iterator b = bundles.iterator(); b.hasNext();)
+        {
+            start((Bundle) b.next());
+        }
+    }
+
+    /**
+     * Update the bundles if the underlying files have changed.
+     * This method reads the information about jars to be updated,
+     * compares them with information available in {@link #currentManagedBundles}.
+     * If the file is newer, it updates the bundle.
+     *
+     * @param jars    Collection of {@link Jar}s representing state of files.
+     * @return Collection of bundles that got updated
+     */
+    private Collection/* <Bundle> */ update(Collection jars)
+    {
+        List bundles = new ArrayList();
+        for (Iterator iter = jars.iterator(); iter.hasNext();)
+        {
+            Jar e = (Jar) iter.next();
+            Jar c = (Jar) currentManagedBundles.get(e.getPath());
+            if (e.isNewer(c))
+            {
+                Bundle b = update(c);
+                if (b != null)
+                {
+                    bundles.add(b);
+                }
+            }
+        }
+        return bundles;
+    }
+
+    /**
+     * Install a jar and return the bundle object.
+     * It uses {@link org.apache.felix.fileinstall.Jar#getPath()} as location
+     * of the new bundle. Before installing a file,
+     * it sees if the file has been identified as a bad file in
+     * earlier run. If yes, then it compares to see if the file has changed
+     * since then. It installs the file if the file has changed.
+     * If the file has not been identified as a bad file in earlier run,
+     * then it always installs it.
+     *
+     * @param jar the jar to be installed
+     * @return Bundle object that was installed
+     */
+    private Bundle install(Jar jar)
+    {
+        Bundle bundle = null;
+        try
+        {
+            String path = jar.getPath();
+            Jar badJar = (Jar) installationFailures.get(jar.getPath());
+            if (badJar != null && badJar.getLastModified() == jar.getLastModified())
+            {
+                return null; // Don't attempt to install it; nothing has changed.
+            }
+            File file = new File(path);
+            InputStream in = new FileInputStream(file);
+            try
+            {
+                bundle = context.installBundle(path, in);
+            }
+            finally
+            {
+                in.close();
+            }
+            installationFailures.remove(path);
+            currentManagedBundles.put(path, new Jar(bundle));
+            log("Installed " + file.getAbsolutePath(), null);
+        }
+        catch (Exception e)
+        {
+            log("Failed to install bundle: " + jar.getPath(), e);
+
+            // Add it our bad jars list, so that we don't
+            // attempt to install it again and again until the underlying
+            // jar has been modified.
+            installationFailures.put(jar.getPath(), jar);
+        }
+        return bundle;
+    }
+
+    /**
+     * Uninstall a jar file.
+     */
+    private Bundle uninstall(Jar jar)
+    {
+        try
+        {
+            Jar old = (Jar) currentManagedBundles.remove(jar.getPath());
+
+            // old can't be null because of the way we calculate deleted list.
+            Bundle bundle = context.getBundle(old.getBundleId());
+            bundle.uninstall();
+            startupFailures.remove(bundle);
+            log("Uninstalled " + jar.getPath(), null);
+            return bundle;
+        }
+        catch (Exception e)
+        {
+            log("Failed to uninstall bundle: " + jar.getPath(), e);
+        }
+        return null;
+    }
+
+    private Bundle update(Jar jar)
+    {
+        InputStream in = null;
+        try
+        {
+            File file = new File(jar.getPath());
+            in = new FileInputStream(file);
+            Bundle bundle = context.getBundle(jar.getBundleId());
+            bundle.update(in);
+            startupFailures.remove(bundle);
+            jar.setLastModified(bundle.getLastModified());
+            jar.setLength(file.length());
+            log("Updated " + jar.getPath(), null);
+            return bundle;
+        }
+        catch (Exception e)
+        {
+            log("Failed to update bundle " + jar.getPath(), e);
+        }
+        finally
+        {
+            if (in != null)
+            {
+                try
+                {
+                    in.close();
+                }
+                catch (IOException e)
+                {
+                }
+            }
+        }
+        return null;
+    }
+
+    private void start(Bundle bundle)
+    {
+        // Fragments can not be started.
+        // No need to check status of bundles
+        // before starting, because OSGi treats this
+        // as a noop when the bundle is already started
+        if (!isFragment(bundle))
+        {
+            try
+            {
+                bundle.start();
+                startupFailures.remove(bundle);
+            }
+            catch (BundleException e)
+            {
+                log("Error while starting bundle: " + bundle.getLocation(), e);
+                startupFailures.add(bundle);
+            }
+        }
+    }
+
+    /**
+     * Start all bundles that we are currently managing.
+     */
+    private void startAllBundles()
+    {
+        for (Iterator jars = currentManagedBundles.values().iterator(); jars.hasNext();)
+        {
+            Jar jar = (Jar) jars.next();
+            Bundle bundle = context.getBundle(jar.getBundleId());
+            start(bundle);
+        }
+    }
+}
\ No newline at end of file
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/Jar.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/Jar.java
new file mode 100644
index 0000000..bca2019
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/Jar.java
@@ -0,0 +1,114 @@
+/*
+ * 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.fileinstall;
+
+import org.osgi.framework.Bundle;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * This class is used to cache vital information of a jar file
+ * that is used during later processing. It also overrides hashCode and
+ * equals methods so that it can be used in various Set operations.
+ * It uses file's path as the primary key. Before
+ *
+ * @author Sanjeeb.Sahoo@Sun.COM
+ */
+class Jar
+{
+    private String path;
+    private long length = -1;
+    private long lastModified = -1;
+    private long bundleId = -1;
+
+    Jar(File file)
+    {
+        // Convert to a URI because the location of a bundle
+        // is typically a URI. At least, that's the case for
+        // autostart bundles.
+        // Normalisation is needed to ensure that we don't treat (e.g.)
+        // /tmp/foo and /tmp//foo differently.
+        path = file.toURI().normalize().getPath();
+        lastModified = file.lastModified();
+        length = file.length();
+    }
+
+    Jar(Bundle b) throws URISyntaxException
+    {
+        // Normalisation is needed to ensure that we don't treat (e.g.)
+        // /tmp/foo and /tmp//foo differently.
+        URI uri = new URI(b.getLocation()).normalize();
+        path = uri.getPath();
+        lastModified = b.getLastModified();
+        bundleId = b.getBundleId();
+    }
+
+    public String getPath()
+    {
+        return path;
+    }
+
+    public long getLastModified()
+    {
+        return lastModified;
+    }
+
+    public void setLastModified(long lastModified)
+    {
+        this.lastModified = lastModified;
+    }
+
+    public long getLength()
+    {
+        return length;
+    }
+
+    public void setLength(long length)
+    {
+        this.length = length;
+    }
+
+    public long getBundleId()
+    {
+        return bundleId;
+    }
+
+    public boolean isNewer(Jar other)
+    {
+        return (getLastModified() > other.getLastModified());
+    }
+
+    // Override hashCode and equals as this object is used in Set
+    public int hashCode()
+    {
+        return path.hashCode();
+    }
+
+    public boolean equals(Object obj)
+    {
+        if (obj instanceof Jar)
+        {
+            return this.path.equals(((Jar) obj).path);
+        }
+        return false;
+    }
+}
\ No newline at end of file