Hi,
PFA a patch for following File Install bundle issues;
https://issues.apache.org/jira/browse/FELIX-937
https://issues.apache.org/jira/browse/FELIX-938
https://issues.apache.org/jira/browse/FELIX-939
Your comments are welcome.
Thanks,
Sahoo
Index: src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
===================================================================
--- src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java (revision 744132)
+++ src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java (working copy)
@@ -26,6 +26,8 @@
*/
import java.io.*;
import java.util.*;
+import java.net.URISyntaxException;
+import java.net.URI;
import org.osgi.framework.*;
import org.osgi.service.cm.*;
@@ -46,6 +48,7 @@
boolean startBundles = true; // by default, we start bundles.
BundleContext context;
boolean reported;
+ Map/* <String, Jar> */ currentManagedBundles = new HashMap();
public DirectoryWatcher(Dictionary properties, BundleContext context)
{
@@ -79,17 +82,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,137 +278,40 @@
}
/**
- * Install bundles that were discovered and uninstall bundles that are gone
- * from the current state.
+ * 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++)
- {
- 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);
+ // Find out all the new, deleted and common bundles.
+ // new = discovered - current,
+ Set newBundles = new HashSet(discovered.values());
+ newBundles.removeAll(currentManagedBundles.values());
- File file = new File(location);
+ // deleted = current - discovered
+ Set deletedBundles = new HashSet(currentManagedBundles.values());
+ deletedBundles.removeAll(discovered.values());
- // 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();
+ // existing = intersection of current & discovered
+ Set existingBundles = new HashSet(discovered.values());
+ existingBundles.retainAll(currentManagedBundles.values());
- 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);
- }
- }
-
+ // We do the operations in the following order:
+ // uninstall, update, install, refresh & start.
+ uninstall(deletedBundles);
+ boolean refresh = updateIfNeeded(existingBundles);
+ Collection starters = install(newBundles);
+ if (!startBundles) starters = Collections.emptyList();
if (refresh || starters.size() != 0)
{
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);
- }
- }
+ start((Bundle) b.next());
}
}
}
@@ -461,17 +366,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 +384,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 +476,179 @@
// 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
+ {
+ String location = new URI(bundles[i].getLocation()).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.
+ }
+ }
+ }
+
+ /**
+ * @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();)
+ {
+ Bundle bundle = install((Jar) iter.next());
+ if (bundle != null)
+ {
+ bundles.add(bundle);
+ }
+ }
+ return bundles;
+ }
+
+ /**
+ * @param jars Collection of {...@link Jar} to be uninstalled
+ */
+ private void uninstall(Collection jars)
+ {
+ for (Iterator iter = jars.iterator(); iter.hasNext();)
+ {
+ final Jar jar = (Jar) iter.next();
+ uninstall(jar);
+ }
+ }
+
+ /**
+ * Update the bundles if the underlying file has 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 update the bundle.
+ *
+ * @param jars Collection of {...@link Jar}s representing state of files.
+ * @return boolean if any bundle got updated, else false
+ */
+ private boolean updateIfNeeded(Collection jars)
+ {
+ boolean refresh = false;
+ for (Iterator iter = jars.iterator(); iter.hasNext();)
+ {
+ Jar e = (Jar) iter.next();
+ Jar c = (Jar) currentManagedBundles.get(e.getPath());
+ if (e.isNewer(c))
+ {
+ refresh |= update(c);
+ }
+ }
+ return refresh;
+ }
+
+ private Bundle install(Jar jar)
+ {
+ Bundle bundle = null;
+ try
+ {
+ String path = jar.getPath();
+ File file = new File(path);
+ InputStream in = new FileInputStream(file);
+ bundle = context.installBundle(path, in);
+ in.close();
+ currentManagedBundles.put(path, new Jar(bundle));
+ log("Installed " + file.getAbsolutePath(), null);
+ }
+ catch (Exception e)
+ {
+ log("failed to install bundle: ", e);
+ }
+ return bundle;
+ }
+
+ private void uninstall(Jar jar)
+ {
+ try
+ {
+ Jar old = (Jar) currentManagedBundles.remove(jar.getPath());
+ Bundle bundle = context.getBundle(old.getBundleId());
+ bundle.uninstall();
+ log("Uninstalled " + jar.getPath(), null);
+ }
+ catch (Exception e)
+ {
+ log("failed to uninstall bundle: ", e);
+ }
+ }
+
+ private boolean update(Jar jar)
+ {
+ boolean refresh = false;
+ InputStream in = null;
+ try
+ {
+ File file = new File(jar.getPath());
+ in = new FileInputStream(file);
+ Bundle bundle = context.getBundle(jar.getBundleId());
+ bundle.update(in);
+ refresh = true;
+ jar.setLastModified(bundle.getLastModified());
+ jar.setLength(file.length());
+ log("Updated " + jar.getPath(), null);
+ }
+ catch (Exception e)
+ {
+ log("Failed to update bundle ", e);
+ }
+ finally
+ {
+ if (in != null)
+ {
+ try
+ {
+ in.close();
+ }
+ catch (IOException e)
+ {
+ }
+ }
+ }
+ return refresh;
+ }
+
+ 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();
+ }
+ catch (BundleException e)
+ {
+ log("Error while starting a newly installed bundle", e);
+ }
+ }
+ }
+
+}
\ No newline at end of file
Index: src/main/java/org/apache/felix/fileinstall/Jar.java
===================================================================
--- src/main/java/org/apache/felix/fileinstall/Jar.java (revision 0)
+++ src/main/java/org/apache/felix/fileinstall/Jar.java (revision 0)
@@ -0,0 +1,116 @@
+/*
+ * 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 [email protected]
+ */
+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;
+ }
+}