You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2009/08/31 08:40:44 UTC

svn commit: r809470 [1/2] - in /felix/trunk/fileinstall/src: main/java/org/apache/felix/fileinstall/ main/java/org/apache/felix/fileinstall/listener/ main/java/org/apache/felix/fileinstall/util/ main/resources/OSGI-INF/metatype/ test/java/org/apache/fe...

Author: gnodet
Date: Mon Aug 31 06:40:43 2009
New Revision: 809470

URL: http://svn.apache.org/viewvc?rev=809470&view=rev
Log:
FELIX-922, FELIX-1483 and FELIX-1377: support for new artifact types, support for exploded artifacts, wait until copy is finished before processing an artifact

Added:
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java
    felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/BundleTransformerTest.java
    felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/ConfigInstallerTest.java
      - copied, changed from r809199, felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/DirectoryWatcherTest.java
Modified:
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java
    felix/trunk/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml
    felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/DirectoryWatcherTest.java
    felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/util/UtilTest.java

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,84 @@
+/*
+ * 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 java.io.File;
+
+import org.apache.felix.fileinstall.listener.ArtifactListener;
+
+/**
+ * An artifact that has been dropped into one watched directory.
+ */
+public class Artifact {
+
+    private File path;
+    private File jaredDirectory;
+    private long lastModified = -1;
+    private ArtifactListener listener;
+    private File transformed;
+    private long bundleId = -1;
+
+    public File getPath() {
+        return path;
+    }
+
+    public void setPath(File path) {
+        this.path = path;
+    }
+
+    public File getJaredDirectory() {
+        return jaredDirectory;
+    }
+
+    public void setJaredDirectory(File jaredDirectory) {
+        this.jaredDirectory = jaredDirectory;
+    }
+
+    public long getLastModified() {
+        return lastModified;
+    }
+
+    public void setLastModified(long lastModified) {
+        this.lastModified = lastModified;
+    }
+
+    public ArtifactListener getListener() {
+        return listener;
+    }
+
+    public void setListener(ArtifactListener listener) {
+        this.listener = listener;
+    }
+
+    public File getTransformed() {
+        return transformed;
+    }
+
+    public void setTransformed(File transformed) {
+        this.transformed = transformed;
+    }
+
+    public long getBundleId() {
+        return bundleId;
+    }
+
+    public void setBundleId(long bundleId) {
+        this.bundleId = bundleId;
+    }
+}

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,82 @@
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes;
+
+import org.apache.felix.fileinstall.listener.ArtifactTransformer;
+
+/**
+ * ArtifactTransformer for plain bundles.
+ */
+public class BundleTransformer implements ArtifactTransformer
+{
+    public boolean canHandle(File artifact)
+    {
+        JarFile jar = null;
+        try
+        {
+            // Handle OSGi bundles with the default deployer
+            String name = artifact.getName();
+            if (!artifact.canRead()  
+                || name.endsWith(".txt") || name.endsWith(".xml")
+                || name.endsWith(".properties") || name.endsWith(".cfg"))
+            {
+                // that's file type which is not supported as bundle and avoid
+                // exception in the log
+                return false;
+            }
+            jar = new JarFile(artifact);
+            Manifest m = jar.getManifest();
+            if (m.getMainAttributes().getValue(new Attributes.Name("Bundle-SymbolicName")) != null
+                && m.getMainAttributes().getValue(new Attributes.Name("Bundle-Version")) != null)
+            {
+                return true;
+            }
+        }
+        catch (Exception e)
+        {
+            // Ignore
+        }
+        finally
+        {
+            if (jar != null)
+            {
+                try
+                {
+                    jar.close();
+                }
+                catch (IOException e)
+                {
+                    // Ignore
+                }
+            }
+        }
+        return false;
+    }
+
+    public File transform(File artifact, File tmpDir) {
+        return artifact;
+    }
+
+}

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,174 @@
+/*
+ * 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 java.io.File;
+import java.io.InputStream;
+import java.io.FileInputStream;
+import java.util.Properties;
+import java.util.Hashtable;
+
+import org.apache.felix.fileinstall.listener.ArtifactInstaller;
+import org.apache.felix.fileinstall.util.Util;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.Configuration;
+
+/**
+ * ArtifactInstaller for configurations.
+ * TODO: This service lifecycle should be bound to the ConfigurationAdmin service lifecycle.
+ */
+public class ConfigInstaller implements ArtifactInstaller
+{
+    BundleContext context;
+
+    ConfigInstaller(BundleContext context) {
+        this.context = context;
+    }
+
+    public boolean canHandle(File artifact) {
+        return artifact.getName().endsWith(".cfg");
+    }
+
+    public void install(File artifact) throws Exception {
+        setConfig(artifact);
+    }
+
+    public void update(File artifact) throws Exception {
+        setConfig(artifact);
+    }
+
+    public void uninstall(File artifact) throws Exception {
+        deleteConfig(artifact);
+    }
+
+    /**
+     * Set the configuration based on the config file.
+     *
+     * @param f
+     *            Configuration file
+     * @return
+     * @throws Exception
+     */
+    boolean setConfig(File f) throws Exception
+    {
+        Properties p = new Properties();
+        InputStream in = new FileInputStream(f);
+        try
+        {
+            p.load(in);
+        }
+        finally
+        {
+            in.close();
+        }
+        Util.performSubstitution(p);
+        String pid[] = parsePid(f.getName());
+        Hashtable ht = new Hashtable();
+        ht.putAll(p);
+        ht.put(DirectoryWatcher.FILENAME, f.getName());
+        Configuration config = getConfiguration(pid[0], pid[1]);
+        if (config.getBundleLocation() != null)
+        {
+            config.setBundleLocation(null);
+        }
+        config.update(ht);
+        return true;
+    }
+
+    /**
+     * Remove the configuration.
+     *
+     * @param f
+     *            File where the configuration in whas defined.
+     * @return
+     * @throws Exception
+     */
+    boolean deleteConfig(File f) throws Exception
+    {
+        String pid[] = parsePid(f.getName());
+        Configuration config = getConfiguration(pid[0], pid[1]);
+        config.delete();
+        return true;
+    }
+
+    String[] parsePid(String path)
+    {
+        String pid = path.substring(0, path.length() - 4);
+        int n = pid.indexOf('-');
+        if (n > 0)
+        {
+            String factoryPid = pid.substring(n + 1);
+            pid = pid.substring(0, n);
+            return new String[]
+                {
+                    pid, factoryPid
+                };
+        }
+        else
+        {
+            return new String[]
+                {
+                    pid, null
+                };
+        }
+    }
+
+    Configuration getConfiguration(String pid, String factoryPid)
+        throws Exception
+    {
+        Configuration oldConfiguration = findExistingConfiguration(pid, factoryPid);
+        if (oldConfiguration != null)
+        {
+            Util.log(context, 0, "Updating configuration from " + pid
+                + (factoryPid == null ? "" : "-" + factoryPid) + ".cfg", null);
+            return oldConfiguration;
+        }
+        else
+        {
+            Configuration newConfiguration;
+            if (factoryPid != null)
+            {
+                newConfiguration = FileInstall.getConfigurationAdmin().createFactoryConfiguration(pid, null);
+            }
+            else
+            {
+                newConfiguration = FileInstall.getConfigurationAdmin().getConfiguration(pid, null);
+            }
+            return newConfiguration;
+        }
+    }
+
+    Configuration findExistingConfiguration(String pid, String factoryPid) throws Exception
+    {
+        String suffix = factoryPid == null ? ".cfg" : "-" + factoryPid + ".cfg";
+
+        String filter = "(" + DirectoryWatcher.FILENAME + "=" + pid + suffix + ")";
+        Configuration[] configurations = FileInstall.getConfigurationAdmin().listConfigurations(filter);
+        if (configurations != null && configurations.length > 0)
+        {
+            return configurations[0];
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+}

Modified: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java?rev=809470&r1=809469&r2=809470&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java (original)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java Mon Aug 31 06:40:43 2009
@@ -21,12 +21,16 @@
 import java.io.*;
 import java.util.*;
 import java.net.URISyntaxException;
+import java.net.URI;
 
 import org.apache.felix.fileinstall.util.Util;
+import org.apache.felix.fileinstall.listener.ArtifactInstaller;
+import org.apache.felix.fileinstall.listener.ArtifactTransformer;
+import org.apache.felix.fileinstall.listener.ArtifactListener;
 import org.osgi.framework.*;
-import org.osgi.service.cm.*;
-import org.osgi.service.log.*;
 import org.osgi.service.packageadmin.*;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
 
 /**
  * -DirectoryWatcher-
@@ -57,50 +61,62 @@
     public final static String POLL = "felix.fileinstall.poll";
     public final static String DIR = "felix.fileinstall.dir";
     public final static String DEBUG = "felix.fileinstall.debug";
+    public final static String TMPDIR = "felix.fileinstall.tmpdir";
     public final static String FILTER = "felix.fileinstall.filter";
-    public final static String START_NEW_BUNDLES =
-        "felix.fileinstall.bundles.new.start";
+    public final static String START_NEW_BUNDLES = "felix.fileinstall.bundles.new.start";
+
     File watchedDirectory;
-    long poll = 2000;
+    File tmpDir;
+    long poll;
     long debug;
+    boolean startBundles;
     String filter;
-    boolean startBundles = true; // by default, we start bundles.
     BundleContext context;
-    boolean reported;
     String originatingFileName;
-    
-    Map/* <String, Jar> */ currentManagedBundles = new HashMap();
 
-    // Represents jars that could not be installed
-    Map/* <String, Jar> */ installationFailures = new HashMap();
+    // Map of all installed artifacts
+    Map/* <File, Artifact> */ currentManagedArtifacts = new HashMap/* <File, Artifact> */();
+
+    // The scanner to report files changes
+    Scanner scanner;
+
+    // Represents files that could not be processed because of a missing artifact listener
+    Set/* <File> */ processingFailures = new HashSet/* <File> */();
+
+    // Represents artifacts that could not be installed
+    Map/* <File, Artifact> */ installationFailures = new HashMap/* <File, Artifact> */();
 
-    // Represents jars that could not be installed
-    Set/* <Bundle> */ startupFailures = new HashSet();
+    // Represents artifacts that could not be installed
+    Set/* <Bundle> */ startupFailures = new HashSet/* <Bundle> */();
     
     public DirectoryWatcher(Dictionary properties, BundleContext context)
     {
         super(properties.toString());
         this.context = context;
-        poll = getLong(properties, POLL, poll);
+        poll = getLong(properties, POLL, 2000);
         debug = getLong(properties, DEBUG, -1);
         originatingFileName = (String) properties.get(FILENAME);
-        
-        String dir = (String) properties.get(DIR);
-        if (dir == null)
-        {
-            dir = "./load";
-        }
-        watchedDirectory = new File(dir);
-        
-        prepareWatchedDir(watchedDirectory);
-        
-        Object value = properties.get(START_NEW_BUNDLES);
-        if (value != null)
+        watchedDirectory = getFile(properties, DIR, new File("./load"));
+        prepareDir(watchedDirectory);
+        tmpDir = getFile(properties, TMPDIR, new File("./tmp"));
+        startBundles = getBoolean(properties, START_NEW_BUNDLES, true);  // by default, we start bundles.
+        filter = (String) properties.get(FILTER);
+
+        FilenameFilter flt;
+        if (filter != null && filter.length() > 0)
         {
-            startBundles = "true".equalsIgnoreCase((String)value);
+            flt = new FilenameFilter()
+            {
+                public boolean accept(File dir, String name) {
+                    return name.matches(filter);
+                }
+            };
         }
-
-        filter = (String) properties.get(FILTER);
+        else
+        {
+            flt = null;
+        }
+        scanner = new Scanner(watchedDirectory, flt);
     }
 
     /**
@@ -113,34 +129,162 @@
         log("{" + POLL + " (ms) = " + poll + ", "
                 + DIR + " = " + watchedDirectory.getAbsolutePath() + ", "
                 + DEBUG + " = " + debug + ", "
-                + FILTER + " = " + filter + ", "
-                + START_NEW_BUNDLES + " = " + startBundles + "}", null);
+                + START_NEW_BUNDLES + " = " + startBundles + ", "
+                + TMPDIR + " = " + tmpDir + ", "
+                + FILTER + " = " + filter + "}", null);
+
         initializeCurrentManagedBundles();
-        Map currentManagedConfigs = new HashMap(); // location -> Long(time)
 
-        FilenameFilter flt;
-        if (filter != null)
-        {
-            flt = new FilenameFilter()
-            {
-                public boolean accept(File dir, String name) {
-                    return name.matches(filter);
-                }
-            };
-        }
-        else
-        {
-            flt = null;
-        }
+        scanner.initialize(currentManagedArtifacts.keySet());
+
         while (!interrupted())
         {
             try
             {
-                Map/* <String, Jar> */ installed = new HashMap();
-                Set/* <String> */ configs = new HashSet();
-                traverse(installed, configs, watchedDirectory, flt);
-                doInstalled(installed);
-                doConfigs(currentManagedConfigs, configs);
+                Set/*<File>*/ files = scanner.scan();
+                List/*<ArtifactListener>*/ listeners = FileInstall.getListeners();
+                List/*<Artifact>*/ deleted = new ArrayList/*<Artifact>*/();
+                List/*<Artifact>*/ modified = new ArrayList/*<Artifact>*/();
+                List/*<Artifact>*/ created = new ArrayList/*<Artifact>*/();
+
+                // Try to process again files that could not be processed
+                files.addAll(processingFailures);
+                processingFailures.clear();
+
+                for (Iterator it = files.iterator(); it.hasNext();)
+                {
+                    File file = (File) it.next();
+                    boolean exists = file.exists();
+                    Artifact artifact = (Artifact) currentManagedArtifacts.get(file);
+                    // File has been deleted
+                    if (!exists && artifact != null)
+                    {
+                        deleteJaredDirectory(artifact);
+                        deleteTransformedFile(artifact);
+                        deleted.add(artifact);
+                    }
+                    else
+                    {
+                        File jar  = file;
+                        // Jar up the directory if needed
+                        if (file.isDirectory())
+                        {
+                            prepareDir(tmpDir);
+                            try
+                            {
+                                jar = new File(tmpDir, file.getName() + ".jar");
+                                Util.jarDir(file, jar);
+
+                            }
+                            catch (IOException e)
+                            {
+                                log("Unable to create jar for: " + file.getAbsolutePath(), e);
+                                continue;
+                            }
+                        }
+                        // File has been modified
+                        if (exists && artifact != null)
+                        {
+                            // Check the last modified date against
+                            // the artifact last modified date if available.  This will loose
+                            // the possibility of the jar being replaced by an older one
+                            // or the content changed without the date being modified, but
+                            // else, we'd have to reinstall all the deployed bundles on restart.
+                            if (artifact.getLastModified() > Util.getLastModified(file))
+                            {
+                                continue;
+                            }
+                            // If there's no listener, this is because this artifact has been installed before
+                            // fileinstall has been restarted.  In this case, try to find a listener.
+                            if (artifact.getListener() == null)
+                            {
+                                ArtifactListener listener = findListener(jar, listeners);
+                                // If no listener can handle this artifact, we need to defer the
+                                // processing for this artifact until one is found
+                                if (listener == null)
+                                {
+                                    processingFailures.add(file);
+                                    continue;
+                                }
+                                artifact.setListener(listener);
+                            }
+                            // If the listener can not handle this file anymore,
+                            // uninstall the artifact and try as if is was new
+                            if (!listeners.contains(artifact.getListener()) || !artifact.getListener().canHandle(jar))
+                            {
+                                deleted.add(artifact);
+                                artifact = null;
+                            }
+                            // The listener is still ok
+                            else
+                            {
+                                deleteTransformedFile(artifact);
+                                artifact.setJaredDirectory(jar);
+                                if (transformArtifact(artifact))
+                                {
+                                    modified.add(artifact);
+                                }
+                                else
+                                {
+                                    deleteJaredDirectory(artifact);
+                                    deleted.add(artifact);
+                                }
+                                continue;
+                            }
+                        }
+                        // File has been added
+                        if (exists && artifact == null)
+                        {
+                            // Find the listener
+                            ArtifactListener listener = findListener(jar, listeners);
+                            // If no listener can handle this artifact, we need to defer the
+                            // processing for this artifact until one is found
+                            if (listener == null)
+                            {
+                                processingFailures.add(file);
+                                continue;
+                            }
+                            // Create the artifact
+                            artifact = new Artifact();
+                            artifact.setPath(file);
+                            artifact.setJaredDirectory(jar);
+                            artifact.setListener(listener);
+                            if (transformArtifact(artifact))
+                            {
+                                created.add(artifact);
+                            }
+                            else
+                            {
+                                deleteJaredDirectory(artifact);
+                            }
+                        }
+                    }
+                }
+                // Handle deleted artifacts
+                // We do the operations in the following order:
+                // uninstall, update, install, refresh & start.
+                Collection uninstalledBundles = uninstall(deleted);
+                Collection updatedBundles = update(modified);
+                Collection installedBundles = install(created);
+                if (uninstalledBundles.size() > 0 || updatedBundles.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();
+                }
+
+                if (startBundles)
+                {
+                    // 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));
+		            // Start updated bundles.
+		            start(updatedBundles);
+                    // Start newly installed bundles
+                    start(installedBundles);
+                }
+
                 Thread.sleep(poll);
             }
             catch (InterruptedException e)
@@ -153,283 +297,85 @@
             }
         }
     }
-    
-    /**
-     * Create the watched directory, if not existing.
-     * Throws a runtime exception if the directory cannot be created,
-     * or if the provided File parameter does not refer to a directory.
-     * 
-     * @param watchedDirectory 
-     *            The directory File Install will monitor
-     */
-    private void prepareWatchedDir(File watchedDirectory)
-    {
-        if (!watchedDirectory.exists() && !watchedDirectory.mkdirs())
-        {
-            log("Cannot create folder "
-                + watchedDirectory
-                + ". Is the folder write-protected?", null);
-            throw new RuntimeException("Cannot create folder: " + watchedDirectory);
-        }
 
-        if (!watchedDirectory.isDirectory())
+    ArtifactListener findListener(File artifact, List/* <ArtifactListener> */ listeners)
+    {
+        for (Iterator itL = listeners.iterator(); itL.hasNext();)
         {
-            log("Cannot watch "
-                + watchedDirectory
-                + " because it's not a directory", null);
-            throw new RuntimeException(
-                "Cannot start FileInstall to watch something that is not a directory");
+            ArtifactListener listener = (ArtifactListener) itL.next();
+            if (listener.canHandle(artifact))
+            {
+                return listener;
+            }
         }
+        return null;
     }
 
-    /**
-     * Handle the changes between the configurations already installed and the
-     * newly found/lost configurations.
-     *
-     * @param current
-     *            Existing installed configurations abspath -> File
-     * @param discovered
-     *            Newly found configurations
-     */
-    void doConfigs(Map current, Set discovered)
-    {
-        try
+    boolean transformArtifact(Artifact artifact) {
+        if (artifact.getListener() instanceof ArtifactTransformer)
         {
-            // Set all old keys as inactive, we remove them
-            // when we find them to be active, will be left
-            // with the inactive ones.
-            Set inactive = new HashSet(current.keySet());
-
-            for (Iterator e = discovered.iterator(); e.hasNext(); )
+            prepareDir(tmpDir);
+            try
             {
-                String path = (String) e.next();
-                File f = new File(path);
-
-                if (!current.containsKey(path))
+                File transformed = ((ArtifactTransformer) artifact.getListener()).transform(artifact.getJaredDirectory(), tmpDir);
+                if (transformed != null)
                 {
-                    // newly found entry, set the config immedialey
-                    Long l = new Long(f.lastModified());
-                    if (setConfig(f))
-                    {
-                        // Remember it for the next round
-                        current.put(path, l);
-                    }
+                    artifact.setTransformed(transformed);
+                    return true;
                 }
-                else
-                {
-                    // Found an existing one.
-                    // Check if it has been updated
-                    long lastModified = f.lastModified();
-                    long oldTime = ((Long) current.get(path)).longValue();
-                    if (oldTime < lastModified)
-                    {
-                        if (setConfig(f))
-                        {
-                            // Remember it for the next round.
-                            current.put(path, new Long(lastModified));
-                        }
-                    }
-                }
-                // Mark this one as active
-                inactive.remove(path);
             }
-            for (Iterator e = inactive.iterator(); e.hasNext();)
+            catch (Exception e)
             {
-                String path = (String) e.next();
-                File f = new File(path);
-                if (deleteConfig(f))
-                {
-                    current.remove(path);
-                }
+                log("Unable to transform artifact: " + artifact.getPath().getAbsolutePath(), e);
             }
+            return false;
         }
-        catch (Exception ee)
-        {
-            log("Processing config: ", ee);
-        }
+        return true;
     }
 
-    /**
-     * Set the configuration based on the config file.
-     *
-     * @param f
-     *            Configuration file
-     * @return
-     * @throws Exception
-     */
-    boolean setConfig(File f) throws Exception
-    {
-        ConfigurationAdmin cm = (ConfigurationAdmin) FileInstall.cmTracker.getService();
-        if (cm == null)
+    private void deleteTransformedFile(Artifact artifact) {
+        if (artifact.getTransformed() != null
+                && !artifact.getTransformed().equals(artifact.getPath())
+                && !artifact.getTransformed().delete())
         {
-            if (debug != 0 && !reported)
-            {
-                log("Can't find a Configuration Manager, configurations do not work",
-                    null);
-                reported = true;
-            }
-            return false;
+            log("Unable to delete transformed artifact: " + artifact.getTransformed().getAbsolutePath(), null);
         }
+    }
 
-        Properties p = new Properties();
-        InputStream in = new FileInputStream(f);
-        try
+    private void deleteJaredDirectory(Artifact artifact) {
+        if (artifact.getJaredDirectory() != null
+                && !artifact.getJaredDirectory().equals(artifact.getPath())
+                && !artifact.getJaredDirectory().delete())
         {
-            p.load(in);
+            log("Unable to delete jared artifact: " + artifact.getJaredDirectory().getAbsolutePath(), null);
         }
-        finally
-        {
-            in.close();
-        }
-        for (Enumeration e = p.keys(); e.hasMoreElements(); )
-        {
-            String name = (String) e.nextElement();
-            Object value = p.get(name);
-            p.put(name,
-                value instanceof String
-                    ? Util.substVars((String) value, name, null, p)
-                    : value);
-        }
-        String pid[] = parsePid(f.getName());
-        Hashtable ht = new Hashtable();
-        ht.putAll(p);
-        ht.put(FILENAME, f.getName());
-        Configuration config = getConfiguration(pid[0], pid[1]);
-        if (config.getBundleLocation() != null)
-        {
-            config.setBundleLocation(null);
-        }
-        config.update(ht);
-        return true;
     }
 
     /**
-     * Remove the configuration.
+     * Create the watched directory, if not existing.
+     * Throws a runtime exception if the directory cannot be created,
+     * or if the provided File parameter does not refer to a directory.
      *
-     * @param f
-     *            File where the configuration in whas defined.
-     * @return
-     * @throws Exception
+     * @param dir
+     *            The directory File Install will monitor
      */
-    boolean deleteConfig(File f) throws Exception
-    {
-        String pid[] = parsePid(f.getName());
-        Configuration config = getConfiguration(pid[0], pid[1]);
-        config.delete();
-        return true;
-    }
-
-    String[] parsePid(String path)
-    {
-        String pid = path.substring(0, path.length() - 4);
-        int n = pid.indexOf('-');
-        if (n > 0)
-        {
-            String factoryPid = pid.substring(n + 1);
-            pid = pid.substring(0, n);
-            return new String[]
-                {
-                    pid, factoryPid
-                };
-        }
-        else
-        {
-            return new String[]
-                {
-                    pid, null
-                };
-        }
-    }
-
-    Configuration getConfiguration(String pid, String factoryPid)
-        throws Exception
+    private void prepareDir(File dir)
     {
-	    Configuration oldConfiguration = findExistingConfiguration(pid, factoryPid);
-        if (oldConfiguration != null)
+        if (!dir.exists() && !dir.mkdirs())
         {
-            log("Updating configuration from " + pid
-                + (factoryPid == null ? "" : "-" + factoryPid) + ".cfg", null);
-            return oldConfiguration;
-        }
-        else
-        {
-            ConfigurationAdmin cm = (ConfigurationAdmin) FileInstall.cmTracker.getService();
-            Configuration newConfiguration = null;
-            if (factoryPid != null)
-            {
-                newConfiguration = cm.createFactoryConfiguration(pid, null);
-            }
-            else
-            {
-                newConfiguration = cm.getConfiguration(pid, null);
-            }
-            return newConfiguration;
+            log("Cannot create folder "
+                + dir
+                + ". Is the folder write-protected?", null);
+            throw new RuntimeException("Cannot create folder: " + dir);
         }
-    }
-    
-    Configuration findExistingConfiguration(String pid, String factoryPid) throws Exception
-    {
-        String suffix = factoryPid == null ? ".cfg" : "-" + factoryPid + ".cfg";
 
-        ConfigurationAdmin cm = (ConfigurationAdmin) FileInstall.cmTracker.getService();
-        String filter = "(" + FILENAME + "=" + pid + suffix + ")";
-        Configuration[] configurations = cm.listConfigurations(filter);
-        if (configurations != null && configurations.length > 0)
-        {
-            return configurations[0];
-        }
-        else
+        if (!dir.isDirectory())
         {
-            return null;
-        }
-    }
-
-    /**
-     * 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 discovered
-     *            A map of path to {@link Jar} that holds the discovered state
-     */
-    void doInstalled(Map discovered)
-    {
-        // 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)
-        {
-            // 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();
-        }
-
-        if (startBundles)
-        {
-            // 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));
-            // Start updated bundles.
-            start(updatedBundles);
-            // Start newly installed bundles.
-            start(installedBundles);
+            log("Cannot use "
+                + dir
+                + " because it's not a directory", null);
+            throw new RuntimeException(
+                "Cannot start FileInstall using something that is not a directory");
         }
     }
 
@@ -444,150 +390,94 @@
      */
     void log(String message, Throwable e)
     {
-        LogService log = getLogService();
-        if (log == null)
-        {
-            System.out.println(message + (e == null ? "" : ": " + e));
-            if (debug > 0 && e != null)
-            {
-                e.printStackTrace(System.out);
-            }
-        }
-        else
-        {
-            if (e != null)
-            {
-                log.log(LogService.LOG_ERROR, message, e);
-                if (debug > 0 && e != null)
-                {
-                    e.printStackTrace();
-                }
-            }
-            else
-            {
-                log.log(LogService.LOG_INFO, message);
-            }
-        }
+        Util.log(context, debug, message, e);
     }
 
     /**
-     * Answer the Log Service
+     * Check if a bundle is a fragment.
      *
+     * @param bundle
      * @return
      */
-    LogService getLogService()
+    boolean isFragment(Bundle bundle)
     {
-        ServiceReference ref = context.getServiceReference(LogService.class.getName());
-        if (ref != null)
+        PackageAdmin padmin = FileInstall.getPackageAdmin();
+        if (padmin != null)
         {
-            LogService log = (LogService) context.getService(ref);
-            return log;
+            return padmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
         }
-        return null;
+        return false;
     }
 
     /**
-     * Traverse the directory and fill the set with the found jars and
-     * configurations.
-     *
-     * @param jars
-     *            Returns path -> {@link Jar} map for found jars
-     * @param configs
-     *            Returns the abspath -> file for found configurations
-     * @param jardir
-     *            The directory to traverse
-     * @param filter
-     *            A filter for file names
+     * Convenience to refresh the packages
      */
-    void traverse(Map/* <String, Jar> */ jars, Set configs, File jardir, FilenameFilter filter)
+    void refresh()
     {
-        String list[] = jardir.list(filter);
-        if (list == null)
+        PackageAdmin padmin = FileInstall.getPackageAdmin();
+        if (padmin != null)
         {
-            prepareWatchedDir(jardir);
-            list = jardir.list(filter);
-        }
-        for (int i = 0; (list != null) && (i < list.length); i++)
-        {
-            File file = new File(jardir, list[i]);
-            if (list[i].endsWith(".cfg"))
-            {
-                configs.add(file.getAbsolutePath());
-            }
-            else if (Util.isValidJar(file.getAbsolutePath()))
-            {
-                Jar jar = new Jar(file);
-                jars.put(jar.getPath(), jar);
-            }
+            padmin.refreshPackages(null);
         }
     }
 
     /**
-     * Check if a bundle is a fragment.
+     * Retrieve a property as a long.
      *
-     * @param bundle
-     * @return
+     * @param properties the properties to retrieve the value from
+     * @param property the name of the property to retrieve
+     * @param dflt the default value
+     * @return the property as a long or the default value
      */
-    boolean isFragment(Bundle bundle)
+    long getLong(Dictionary properties, String property, long dflt)
     {
-        PackageAdmin padmin;
-        if (FileInstall.padmin == null)
-        {
-            return false;
-        }
-
-        try
+        String value = (String) properties.get(property);
+        if (value != null)
         {
-            padmin = (PackageAdmin) FileInstall.padmin.waitForService(10000);
-            if (padmin != null)
+            try
             {
-                return padmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
+                return Long.parseLong(value);
+            }
+            catch (Exception e)
+            {
+                log(property + " set, but not a long: " + value, null);
             }
         }
-        catch (InterruptedException e)
-        {
-            // stupid exception
-        }
-        return false;
+        return dflt;
     }
 
     /**
-     * Convenience to refresh the packages
+     * Retrieve a property as a File.
+     *
+     * @param properties the properties to retrieve the value from
+     * @param property the name of the property to retrieve
+     * @param dflt the default value
+     * @return the property as a File or the default value
      */
-    void refresh()
+    File getFile(Dictionary properties, String property, File dflt)
     {
-        PackageAdmin padmin;
-        try
-        {
-            padmin = (PackageAdmin) FileInstall.padmin.waitForService(10000);
-            padmin.refreshPackages(null);
-        }
-        catch (InterruptedException e)
+        String value = (String) properties.get(property);
+        if (value != null)
         {
-            Thread.currentThread().interrupt();
+            return new File(value);
         }
+        return dflt;
     }
 
     /**
-     * Answer the long from a property.
+     * Retrieve a property as a boolan.
      *
-     * @param property
-     * @param dflt
-     * @return
+     * @param properties the properties to retrieve the value from
+     * @param property the name of the property to retrieve
+     * @param dflt the default value
+     * @return the property as a boolean or the default value
      */
-    long getLong(Dictionary properties, String property, long dflt)
+    boolean getBoolean(Dictionary properties, String property, boolean dflt)
     {
         String value = (String) properties.get(property);
         if (value != null)
         {
-            try
-            {
-                return Long.parseLong(value);
-            }
-            catch (Exception e)
-            {
-                log(property + " set, but not a long: " + value, null);
-            }
+            return Boolean.parseBoolean(value);
         }
         return dflt;
     }
@@ -616,46 +506,62 @@
         String watchedDirPath = watchedDirectory.toURI().normalize().getPath();
         for (int i = 0; i < bundles.length; i++)
         {
-            try
+            Artifact artifact = new Artifact();
+            artifact.setBundleId(bundles[i].getBundleId());
+            artifact.setLastModified(bundles[i].getLastModified());
+            artifact.setListener(null);
+            // Convert to a URI because the location of a bundle
+            // is typically a URI. At least, that's the case for
+            // autostart bundles and bundles installed by fileinstall.
+            // Normalisation is needed to ensure that we don't treat (e.g.)
+            // /tmp/foo and /tmp//foo differently.
+            String location = bundles[i].getLocation();
+            String path = null;
+            if (location != null &&
+                    !location.equals(Constants.SYSTEM_BUNDLE_LOCATION))
             {
-                Jar jar = new Jar(bundles[i]);
-                String path =  jar.getPath();
-                if (path == null)
+                URI uri;
+                try
                 {
-                    // jar.getPath is null means we could not parse the location
-                    // as a meaningful URI or file path. e.g., location
-                    // represented an Opaque URI.
-                    // We can't do any meaningful processing for this bundle.
-                    continue;
+                    uri = new URI(bundles[i].getLocation()).normalize();
                 }
-                final int index = path.lastIndexOf('/');
-                if (index != -1 && path.substring(0, index + 1).equals(watchedDirPath))
+                catch (URISyntaxException e)
                 {
-                    currentManagedBundles.put(path, jar);
+                    // Let's try to interpret the location as a file path
+                    uri = new File(location).toURI().normalize();
                 }
+                path = uri.getPath();
             }
-            catch (URISyntaxException e)
+            if (path == null)
             {
-                // Ignore and continue.
-                // This can never happen for bundles that have been installed
-                // by FileInstall, as we always use proper filepath as location.
+                // jar.getPath is null means we could not parse the location
+                // as a meaningful URI or file path. e.g., location
+                // represented an Opaque URI.
+                // We can't do any meaningful processing for this bundle.
+                continue;
+            }
+            artifact.setPath(new File(path));
+            final int index = path.lastIndexOf('/');
+            if (index != -1 && path.startsWith(watchedDirPath))
+            {
+                currentManagedArtifacts.put(new File(path), artifact);
             }
         }
     }
 
     /**
-     * This method installs a collection of jar files.
-     * @param jars Collection of {@link Jar} to be installed
+     * This method installs a collection of artifacts.
+     * @param artifacts Collection of {@link Artifact}s to be installed
      * @return List of Bundles just installed
      */
-    private Collection/* <Bundle> */ install(Collection jars)
+    private Collection/* <Bundle> */ install(Collection/* <Artifact> */ artifacts)
     {
         List bundles = new ArrayList();
-        for (Iterator iter = jars.iterator(); iter.hasNext();)
+        for (Iterator iter = artifacts.iterator(); iter.hasNext();)
         {
-            Jar jar = (Jar) iter.next();
+            Artifact artifact = (Artifact) iter.next();
 
-            Bundle bundle = install(jar);
+            Bundle bundle = install(artifact);
             if (bundle != null)
             {
                 bundles.add(bundle);
@@ -665,16 +571,17 @@
     }
 
     /**
-     * @param jars Collection of {@link Jar} to be uninstalled
+     * This method uninstalls a collection of artifacts.
+     * @param artifacts Collection of {@link Artifact}s to be uninstalled
      * @return Collection of Bundles that got uninstalled
      */
-    private Collection/* <Bundle> */ uninstall(Collection jars)
+    private Collection/* <Bundle> */ uninstall(Collection/* <Artifact> */ artifacts)
     {
         List bundles = new ArrayList();
-        for (Iterator iter = jars.iterator(); iter.hasNext();)
+        for (Iterator iter = artifacts.iterator(); iter.hasNext();)
         {
-            final Jar jar = (Jar) iter.next();
-            Bundle b = uninstall(jar);
+            final Artifact artifact = (Artifact) iter.next();
+            Bundle b = uninstall(artifact);
             if (b != null)
             {
                 bundles.add(b);
@@ -683,45 +590,30 @@
         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.
+     * This method updates a collection of artifacts.
      *
-     * @param jars    Collection of {@link Jar}s representing state of files.
+     * @param artifacts    Collection of {@link Artifact}s to be updated.
      * @return Collection of bundles that got updated
      */
-    private Collection/* <Bundle> */ update(Collection jars)
+    private Collection/* <Bundle> */ update(Collection/* <Artifact> */ artifacts)
     {
         List bundles = new ArrayList();
-        for (Iterator iter = jars.iterator(); iter.hasNext(); )
+        for (Iterator iter = artifacts.iterator(); iter.hasNext(); )
         {
-            Jar e = (Jar) iter.next();
-            Jar c = (Jar) currentManagedBundles.get(e.getPath());
-            if (e.isNewer(c))
+            Artifact e = (Artifact) iter.next();
+            Bundle b = update(e);
+            if (b != null)
             {
-                Bundle b = update(c);
-                if (b != null)
-                {
-                    bundles.add(b);
-                }
+                bundles.add(b);
             }
         }
         return bundles;
     }
 
     /**
-     * Install a jar and return the bundle object.
-     * It uses {@link org.apache.felix.fileinstall.Jar#getPath()} as location
+     * Install an artifact and return the bundle object.
+     * It uses {@link org.apache.felix.fileinstall.Artifact#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
@@ -729,44 +621,55 @@
      * 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
+     * @param artifact the artifact to be installed
      * @return Bundle object that was installed
      */
-    private Bundle install(Jar jar)
+    private Bundle install(Artifact artifact)
     {
         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
+            File path = artifact.getPath();
+            // If the listener is an installer, ask for an update
+            if (artifact.getListener() instanceof ArtifactInstaller)
             {
-                // Some users wanted the location to be a URI (See FELIX-1269)
-                final String location = file.toURI().normalize().toString();
-                bundle = context.installBundle(location, in);
+                ((ArtifactInstaller) artifact.getListener()).install(path);
             }
-            finally
+            // else we need to ask for an update on the bundle
+            else if (artifact.getListener() instanceof ArtifactTransformer)
             {
-                in.close();
+                File transformed = artifact.getTransformed();
+                Artifact badArtifact = (Artifact) installationFailures.get(artifact.getPath());
+                if (badArtifact != null && badArtifact.getLastModified() == artifact.getLastModified())
+                {
+                    return null; // Don't attempt to install it; nothing has changed.
+                }
+                InputStream in = new FileInputStream(transformed != null ? transformed : path);
+                try
+                {
+                    // Some users wanted the location to be a URI (See FELIX-1269)
+                    final String location = path.toURI().normalize().toString();
+                    bundle = context.installBundle(location, in);
+                }
+                finally
+                {
+                    in.close();
+                }
+                artifact.setBundleId(bundle.getBundleId());
             }
+            artifact.setLastModified(Util.getLastModified(path));
             installationFailures.remove(path);
-            currentManagedBundles.put(path, new Jar(bundle));
-            log("Installed " + file.getAbsolutePath(), null);
+            currentManagedArtifacts.put(path, artifact);
+            log("Installed " + path, null);
         }
         catch (Exception e)
         {
-            log("Failed to install bundle: " + jar.getPath(), e);
+            log("Failed to install artifact: " + artifact.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);
+            installationFailures.put(artifact.getPath(), artifact);
         }
         return bundle;
     }
@@ -774,74 +677,97 @@
     /**
      * Uninstall a jar file.
      */
-    private Bundle uninstall(Jar jar)
+    private Bundle uninstall(Artifact artifact)
     {
+        Bundle bundle = null;
         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());
-            if (bundle == null)
-            {
-            	log("Failed to uninstall bundle: "
-                    + jar.getPath() + " with id: "
-                    + old.getBundleId()
-                    + ". The bundle has already been uninstalled", null);
-            	return null;
+            File path = artifact.getPath();
+            // Forget this artifact
+            currentManagedArtifacts.remove(path);
+            // Delete transformed file
+            deleteTransformedFile(artifact);
+            // if the listener is an installer, uninstall the artifact
+            if (artifact.getListener() instanceof ArtifactInstaller)
+            {
+                ((ArtifactInstaller) artifact.getListener()).uninstall(path);
+            }
+            // else we need uninstall the bundle
+            else if (artifact.getListener() instanceof ArtifactTransformer)
+            {
+                // old can't be null because of the way we calculate deleted list.
+                bundle = context.getBundle(artifact.getBundleId());
+                if (bundle == null)
+                {
+                    log("Failed to uninstall bundle: "
+                        + path + " with id: "
+                        + artifact.getBundleId()
+                        + ". The bundle has already been uninstalled", null);
+                    return null;
+                }
+                bundle.uninstall();
             }
-            bundle.uninstall();
             startupFailures.remove(bundle);
-            log("Uninstalled " + jar.getPath(), null);
-            return bundle;
+            log("Uninstalled " + path, null);
         }
         catch (Exception e)
         {
-            log("Failed to uninstall bundle: " + jar.getPath(), e);
+            log("Failed to uninstall artifact: " + artifact.getPath(), e);
         }
-        return null;
+        return bundle;
     }
 
-    private Bundle update(Jar jar)
+    private Bundle update(Artifact artifact)
     {
-        InputStream in = null;
+        Bundle bundle = null;
         try
         {
-            File file = new File(jar.getPath());
-            in = new FileInputStream(file);
-            Bundle bundle = context.getBundle(jar.getBundleId());
-            if (bundle == null)
-            {
-            	log("Failed to update bundle: "
-                    + jar.getPath() + " with ID "
-                    + jar.getBundleId()
-                    + ". The bundle has been uninstalled", null);
-            	return null;
+            File path = artifact.getPath();
+            // If the listener is an installer, ask for an update
+            if (artifact.getListener() instanceof ArtifactInstaller)
+            {
+                ((ArtifactInstaller) artifact.getListener()).update(path);
+            }
+            // else we need to ask for an update on the bundle
+            else if (artifact.getListener() instanceof ArtifactTransformer)
+            {
+                File transformed = artifact.getTransformed();
+                bundle = context.getBundle(artifact.getBundleId());
+                if (bundle == null)
+                {
+                    log("Failed to update bundle: "
+                        + path + " with ID "
+                        + artifact.getBundleId()
+                        + ". The bundle has been uninstalled", null);
+                    return null;
+                }
+                InputStream in = new FileInputStream(transformed != null ? transformed : path);
+                try
+                {
+                    bundle.update(in);
+                }
+                finally
+                {
+                    in.close();
+                }
             }
-            bundle.update(in);
             startupFailures.remove(bundle);
-            jar.setLastModified(bundle.getLastModified());
-            log("Updated " + jar.getPath(), null);
-            return bundle;
+            artifact.setLastModified(Util.getLastModified(path));
+            log("Updated " + path, null);
         }
         catch (Exception e)
         {
-            log("Failed to update bundle " + jar.getPath(), e);
+            log("Failed to update artifact " + artifact.getPath(), e);
         }
-        finally
+        return bundle;
+    }
+
+    private void start(Collection/* <Bundle> */ bundles)
+    {
+        for (Iterator b = bundles.iterator(); b.hasNext(); )
         {
-            if (in != null)
-            {
-                try
-                {
-                    in.close();
-                }
-                catch (IOException e)
-                {
-                }
-            }
+            start((Bundle) b.next());
         }
-        return null;
     }
 
     private void start(Bundle bundle)

Modified: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java?rev=809470&r1=809469&r2=809470&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java (original)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java Mon Aug 31 06:40:43 2009
@@ -21,6 +21,9 @@
 import java.util.*;
 
 import org.apache.felix.fileinstall.util.Util;
+import org.apache.felix.fileinstall.listener.ArtifactListener;
+import org.apache.felix.fileinstall.listener.ArtifactInstaller;
+import org.apache.felix.fileinstall.listener.ArtifactTransformer;
 import org.osgi.framework.*;
 import org.osgi.service.cm.*;
 import org.osgi.service.packageadmin.*;
@@ -36,12 +39,16 @@
 {
     static ServiceTracker padmin;
     static ServiceTracker cmTracker;
+    static List /* <ArtifactListener> */ listeners = new ArrayList /* <ArtifactListener> */();
     BundleContext context;
     Map watchers = new HashMap();
+    ConfigInstaller configInstaller;
+    ServiceTracker listenersTracker;
 
     public void start(BundleContext context) throws Exception
     {
         this.context = context;
+        addListener(new BundleTransformer());
         Hashtable props = new Hashtable();
         props.put(Constants.SERVICE_PID, getName());
         context.registerService(ManagedServiceFactory.class.getName(), this,
@@ -49,8 +56,39 @@
 
         padmin = new ServiceTracker(context, PackageAdmin.class.getName(), null);
         padmin.open();
-        cmTracker = new ServiceTracker(context, ConfigurationAdmin.class.getName(), null);
+        cmTracker = new ServiceTracker(context, ConfigurationAdmin.class.getName(), null)
+        {
+            public Object addingService(ServiceReference serviceReference)
+            {
+                ConfigurationAdmin cm = (ConfigurationAdmin) super.addingService(serviceReference);
+                configInstaller = new ConfigInstaller(context);
+                addListener(configInstaller);
+                return cm;
+            }
+            public void removedService(ServiceReference serviceReference, Object o)
+            {
+                configInstaller = null;
+                removeListener(configInstaller);
+                super.removedService(serviceReference, o);
+            }
+        };
         cmTracker.open();
+        String flt = "(|(" + Constants.OBJECTCLASS + "=" + ArtifactInstaller.class.getName() + ")"
+                     + "(" + Constants.OBJECTCLASS + "=" + ArtifactTransformer.class.getName() + "))";
+        listenersTracker = new ServiceTracker(context, FrameworkUtil.createFilter(flt), null)
+        {
+            public Object addingService(ServiceReference serviceReference)
+            {
+                ArtifactListener listener = (ArtifactListener) super.addingService(serviceReference);
+                addListener(listener);
+                return listener;
+            }
+            public void removedService(ServiceReference serviceReference, Object o)
+            {
+                removeListener((ArtifactListener) o);
+            }
+        };
+        listenersTracker.open();
 
         // Created the initial configuration
         Hashtable ht = new Hashtable();
@@ -59,6 +97,7 @@
         set(ht, DirectoryWatcher.DIR);
         set(ht, DirectoryWatcher.DEBUG);
         set(ht, DirectoryWatcher.FILTER);
+        set(ht, DirectoryWatcher.TMPDIR);
         set(ht, DirectoryWatcher.START_NEW_BUNDLES);
         updated("initial", ht);
     }
@@ -93,6 +132,7 @@
                 // Ignore
             }
         }
+        listenersTracker.close();
         cmTracker.close();
         padmin.close();
     }
@@ -115,23 +155,71 @@
         throws ConfigurationException
     {
         deleted(pid);
-        performSubstitution(properties);    
+        Util.performSubstitution(properties);    
         
         DirectoryWatcher watcher = new DirectoryWatcher(properties, context);
         watchers.put(pid, watcher);
         watcher.start();
     }
 
-    private void performSubstitution(Dictionary properties)
+    private void addListener(ArtifactListener listener)
     {
-        for (Enumeration e = properties.keys(); e.hasMoreElements(); )
+        synchronized (listeners)
         {
-            String name = (String) e.nextElement();
-            Object value = properties.get(name);
-            properties.put(name,
-                value instanceof String
-                    ? Util.substVars((String) value, name, null, properties)
-                    : value);
+            listeners.add(listener);
         }
     }
+
+    private void removeListener(ArtifactListener listener)
+    {
+        synchronized (listeners)
+        {
+            listeners.remove(listener);
+        }
+    }
+
+    static List getListeners()
+    {
+        synchronized (listeners)
+        {
+            return new ArrayList(listeners);
+        }
+    }
+
+    static PackageAdmin getPackageAdmin()
+    {
+        return getPackageAdmin(10000);
+    }
+
+    static PackageAdmin getPackageAdmin(long timeout)
+    {
+        try
+        {
+            return (PackageAdmin) padmin.waitForService(timeout);
+        }
+        catch (InterruptedException e)
+        {
+            Thread.currentThread().interrupt();
+            return null;
+        }
+    }
+
+    static ConfigurationAdmin getConfigurationAdmin()
+    {
+        return getConfigurationAdmin(10000);
+    }
+
+    static ConfigurationAdmin getConfigurationAdmin(long timeout)
+    {
+        try
+        {
+            return (ConfigurationAdmin) cmTracker.waitForService(timeout);
+        }
+        catch (InterruptedException e)
+        {
+            Thread.currentThread().interrupt();
+            return null;
+        }
+    }
+
 }
\ No newline at end of file

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,180 @@
+/*
+ * 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 java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.HashSet;
+import java.util.zip.CRC32;
+import java.io.File;
+import java.io.FilenameFilter;
+
+/**
+ * A Scanner object is able to detect and report new, modified
+ * and deleted files.
+ *
+ * The scanner use an internal checksum to identify the signature
+ * of a file or directory.  The checksum will change if the file
+ * or any of the directory's child is modified.
+ *
+ * In addition, if the scanner detects a change on a given file, it
+ * will wait until the checksum does not change anymore before reporting
+ * the change on this file.  This allows to not report the change until
+ * a big copy if complete for example.
+ */
+public class Scanner {
+
+    final File directory;
+    final FilenameFilter filter;
+
+    // Store checksums of files or directories
+    Map/* <File, Long> */ lastChecksums = new HashMap/* <File, Long> */();
+    Map/* <File, Long> */ storedChecksums = new HashMap/* <File, Long> */();
+
+    /**
+     * Create a scanner for the specified directory
+     *
+     * @param directory the directory to scan
+     */
+    public Scanner(File directory)
+    {
+        this(directory, null);
+    }
+
+    /**
+     * Create a scanner for the specified directory and file filter
+     *
+     * @param directory the directory to scan
+     * @param filter a filter for file names
+     */
+    public Scanner(File directory, FilenameFilter filter)
+    {
+        this.directory = directory;
+        this.filter = filter;
+    }
+
+    /**
+     * Initialize the list of known files.
+     * This should be called before the first scan to initialize
+     * the list of known files.  The purpose is to be able to detect
+     * files that have been deleted while the scanner was inactive.
+     *
+     * @param files a list of known files
+     */
+    public void initialize(Collection/*<File>*/ files)
+    {
+        for (Iterator it = files.iterator(); it.hasNext();)
+        {
+            storedChecksums.put(it.next(), Long.valueOf(0));
+        }
+    }
+
+    /**
+     * Report a set of new, modified or deleted files.
+     * Modifications are checked against a computed checksum on some file
+     * attributes to detect any modification.
+     * Upon restart, such checksums are not known so that all files will
+     * be reported as modified. 
+     *
+     * @return a list of changes on the files included in the directory
+     */
+    public Set/*<File>*/ scan()
+    {
+        File[] list = directory.listFiles(filter);
+        if (list == null)
+        {
+            return null;
+        }
+        Set/*<File>*/ files = new HashSet/*<File>*/();
+        Set/*<File>*/ removed = new HashSet/*<File>*/(storedChecksums.keySet());
+        for (int i = 0; i < list.length; i++)
+        {
+            File file  = list[i];
+            long lastChecksum = lastChecksums.get(file) != null ? ((Long) lastChecksums.get(file)).longValue() : 0;
+            long storedChecksum = storedChecksums.get(file) != null ? ((Long) storedChecksums.get(file)).longValue() : 0;
+            long newChecksum = checksum(file);
+            lastChecksums.put(file, Long.valueOf(newChecksum));
+            // Only handle file when it does not change anymore and it has changed since last reported
+            if (newChecksum == lastChecksum && newChecksum != storedChecksum)
+            {
+                storedChecksums.put(file, Long.valueOf(newChecksum));
+                files.add(file);
+            }
+            removed.remove(file);
+        }
+        for (Iterator it = removed.iterator(); it.hasNext();)
+        {
+            File file = (File) it.next();
+            // Make sure we'll handle a file that has been deleted
+            files.addAll(removed);
+            // Remove no longer used checksums
+            lastChecksums.remove(file);
+            storedChecksums.remove(file);
+        }
+        return files;
+    }
+
+    /**
+     * Compute a cheksum for the file or directory that consists of the name, length and the last modified date
+     * for a file and its children in case of a directory
+     *
+     * @param file the file or directory
+     * @return a checksum identifying any change
+     */
+    static long checksum(File file)
+    {
+        CRC32 crc = new CRC32();
+        checksum(file, crc);
+        return crc.getValue();
+    }
+
+    private static void checksum(File file, CRC32 crc)
+    {
+        crc.update(file.getName().getBytes());
+        if (file.isFile())
+        {
+            checksum(file.lastModified(), crc);
+            checksum(file.length(), crc);
+        }
+        else if (file.isDirectory())
+        {
+            File[] children = file.listFiles();
+            if (children != null)
+            {
+                for (int i = 0; i < children.length; i++)
+                {
+                    checksum(children[i], crc);
+                }
+            }
+        }
+    }
+
+    private static void checksum(long l, CRC32 crc)
+    {
+        for (int i = 0; i < 8; i++)
+        {
+            crc.update((int) (l & 0x000000ff));
+            l >>= 8;
+        }
+    }
+
+}

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,56 @@
+/**
+ *
+ * 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.listener;
+
+import java.io.File;
+
+/**
+ * Objects implementing this interface are able to directly
+ * install and uninstall supported artifacts.  Artifacts that
+ * are transformed into bundles should use the
+ * {@link ArtifactTransformer} interface instead.
+ *
+ * Note that fileinstall does not keep track of those artifacts
+ * across restarts, so this means that after a restart, existing
+ * artifacts will be reported as new, while any deleted artifact
+ * won't be reported as deleted.
+ */
+public interface ArtifactInstaller extends ArtifactListener {
+
+    /**
+     * Install the artifact
+     *
+     * @param artifact the artifact to be installed
+     */
+    void install(File artifact) throws Exception;
+
+    /**
+     * Update the artifact
+     *
+     * @param artifact the artifact to be updated
+     */
+    void update(File artifact) throws Exception;
+
+    /**
+     * Uninstall the artifact
+     * 
+     * @param artifact the artifact to be uninstalled
+     */
+    void uninstall(File artifact) throws Exception;
+
+}

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,44 @@
+/**
+ *
+ * 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.listener;
+
+import java.io.File;
+
+/**
+ * Interface representing a custom deployment mechanism.
+ * 
+ * Classes must implement one of its sub-interface, either
+ * {@link org.apache.felix.fileinstall.listener.ArtifactTransformer} or
+ * {@link org.apache.felix.fileinstall.listener.ArtifactInstaller}.
+ *
+ */
+public interface ArtifactListener {
+	
+	/**
+     * Returns true if the listener can process the given artifact.
+     *
+     * Error occuring when checking the artifact should be catched
+     * and not be thrown.
+     *
+     * @param artifact the artifact to check
+     * @return <code>true</code> if this listener supports
+     *         the given artifact, <code>false</code> otherwise
+     */
+    boolean canHandle(File artifact);
+
+}

Added: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java?rev=809470&view=auto
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java (added)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java Mon Aug 31 06:40:43 2009
@@ -0,0 +1,35 @@
+/**
+ *
+ * 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.listener;
+
+import java.io.File;
+
+/**
+ * Objects implementing this interface are able to convert certain
+ * kind of artifacts to OSGi bundles.
+ *
+ */
+public interface ArtifactTransformer extends ArtifactListener {
+
+    /**
+     * Process the given file (canHandle returned true previously)
+     * Can return <null> or a pointer to a transformed file.
+     */
+     File transform(File artifact, File tmpDir) throws Exception;
+
+}

Modified: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java?rev=809470&r1=809469&r2=809470&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java (original)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java Mon Aug 31 06:40:43 2009
@@ -19,10 +19,25 @@
 package org.apache.felix.fileinstall.util;
 
 import java.io.IOException;
+import java.io.FileInputStream;
+import java.io.File;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
 import java.util.Dictionary;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.Collections;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import java.util.zip.CRC32;
 import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+import org.osgi.service.log.LogService;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.BundleContext;
 
 public class Util
 {
@@ -30,6 +45,24 @@
     private static final String DELIM_STOP = "}";
 
     /**
+     * Perform substitution on a property set
+     *
+     * @param properties the property set to perform substitution on
+     */
+    public static void performSubstitution(Dictionary properties)
+    {
+        for (Enumeration e = properties.keys(); e.hasMoreElements(); )
+        {
+            String name = (String) e.nextElement();
+            Object value = properties.get(name);
+            properties.put(name,
+                value instanceof String
+                    ? Util.substVars((String) value, name, null, properties)
+                    : value);
+        }
+    }
+
+    /**
      * <p>
      * This method performs property variable substitution on the
      * specified value. If the specified value contains the syntax
@@ -133,35 +166,156 @@
     }
 
     /**
-     * Check if a file is a legitimate Jar file
-     * @param path
-     * @return
+     * Log a message and optional throwable. If there is a log service we use
+     * it, otherwise we log to the console
+     *
+     * @param message
+     *            The message to log
+     * @param e
+     *            The throwable to log
      */
-    public static boolean isValidJar(String path)
+    public static void log(BundleContext context, long debug, String message, Throwable e)
     {
-        JarFile jar = null;
-        try
+        LogService log = getLogService(context);
+        if (log == null)
         {
-            jar = new JarFile(path);
-            return true;
-        }
-        catch (IOException ioe)
-        {
-            return false;
+            System.out.println(message + (e == null ? "" : ": " + e));
+            if (debug > 0 && e != null)
+            {
+                e.printStackTrace(System.out);
+            }
         }
-        finally
+        else
         {
-            if (jar != null)
+            if (e != null)
             {
-                try
+                log.log(LogService.LOG_ERROR, message, e);
+                if (debug > 0 && e != null)
                 {
-                    jar.close();
+                    e.printStackTrace();
                 }
-                catch (IOException e)
-                {
-                    //do nothing
+            }
+            else
+            {
+                log.log(LogService.LOG_INFO, message);
+            }
+        }
+    }
+
+    /**
+     * Answer the Log Service
+     *
+     * @return
+     */
+    private static LogService getLogService(BundleContext context)
+    {
+        ServiceReference ref = context.getServiceReference(LogService.class.getName());
+        if (ref != null)
+        {
+            LogService log = (LogService) context.getService(ref);
+            return log;
+        }
+        return null;
+    }
+
+    /**
+     * Jar up a directory
+     *
+     * @param directory
+     * @param zipName
+     * @throws IOException
+     */
+    public static void jarDir(File directory, File zipName) throws IOException {
+        // create a ZipOutputStream to zip the data to
+        JarOutputStream zos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(zipName)));
+        String path = "";
+        File manFile = new File(directory, JarFile.MANIFEST_NAME);
+        if (manFile.exists()) {
+            byte[] readBuffer = new byte[8192];
+            FileInputStream fis = new FileInputStream(manFile);
+            try {
+                ZipEntry anEntry = new ZipEntry(JarFile.MANIFEST_NAME);
+                zos.putNextEntry(anEntry);
+                int bytesIn = fis.read(readBuffer);
+                while (bytesIn != -1) {
+                    zos.write(readBuffer, 0, bytesIn);
+                    bytesIn = fis.read(readBuffer);
+                }
+            } finally {
+                fis.close();
+            }
+            zos.closeEntry();
+        }
+        zipDir(directory, zos, path, Collections.singleton(JarFile.MANIFEST_NAME));
+        // close the stream
+        zos.close();
+    }
+
+    /**
+     * Zip up a directory path
+     * @param directory
+     * @param zos
+     * @param path
+     * @param exclusions
+     * @throws IOException
+     */
+    public static void zipDir(File directory, ZipOutputStream zos, String path, Set/* <String> */ exclusions) throws IOException {
+        // get a listing of the directory content
+        File[] dirList = directory.listFiles();
+        byte[] readBuffer = new byte[8192];
+        int bytesIn = 0;
+        // loop through dirList, and zip the files
+        for (int i = 0; i < dirList.length; i++) {
+            File f = dirList[i];
+            if (f.isDirectory()) {
+                zipDir(f, zos, path + f.getName() + "/", exclusions);
+                continue;
+            }
+            String entry = path + f.getName();
+            if (!exclusions.contains(entry)) {
+                FileInputStream fis = new FileInputStream(f);
+                try {
+                    ZipEntry anEntry = new ZipEntry(entry);
+                    zos.putNextEntry(anEntry);
+                    bytesIn = fis.read(readBuffer);
+                    while (bytesIn != -1) {
+                        zos.write(readBuffer, 0, bytesIn);
+                        bytesIn = fis.read(readBuffer);
+                    }
+                } finally {
+                    fis.close();
                 }
             }
         }
     }
+
+    /**
+     * Return the latest time at which this file or any child if the file denotes
+     * a directory has been modified
+     *
+     * @param file file or directory to check
+     * @return the latest modification time
+     */
+    public static long getLastModified(File file)
+    {
+        if (file.isFile())
+        {
+            return file.lastModified();
+        }
+        else if (file.isDirectory())
+        {
+            File[] children = file.listFiles();
+            long lastModified = 0;
+            for (int i = 0; i < children.length; i++)
+            {
+                lastModified = Math.max(lastModified, getLastModified(children[i]));
+            }
+            return lastModified;
+        }
+        else
+        {
+            return 0;
+        }
+    }
+
 }

Modified: felix/trunk/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml?rev=809470&r1=809469&r2=809470&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml (original)
+++ felix/trunk/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml Mon Aug 31 06:40:43 2009
@@ -28,6 +28,7 @@
     <AD name="Debug"  id="felix.fileinstall.debug" required="false" type="String" default="-1"/>
     <AD name="Start new bundles?"  id="felix.fileinstall.bundles.new.start" required="false" type="String" default="true"/>
     <AD name="File name filter"  id="felix.fileinstall.filter" required="false" type="String" default=""/>
+    <AD name="Temp directory"  id="felix.fileinstall.tmpdir" required="false" type="String" default="tmp"/>
   </OCD>
   
     <Designate pid="org.apache.felix.fileinstall">