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

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@809470 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java
new file mode 100644
index 0000000..ab67f78
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/Artifact.java
@@ -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;
+    }
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java
new file mode 100644
index 0000000..9cf6511
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/BundleTransformer.java
@@ -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;
+    }
+
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java
new file mode 100644
index 0000000..ddf637a
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/ConfigInstaller.java
@@ -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;
+        }
+    }
+
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
index 7f3069c..34f83bb 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
@@ -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,69 +61,49 @@
     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> */();
 
-    // Represents jars that could not be installed
-    Set/* <Bundle> */ startupFailures = new HashSet();
+    // 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 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)
-        {
-            startBundles = "true".equalsIgnoreCase((String)value);
-        }
-
+        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);
-    }
-
-    /**
-     * Main run loop, will traverse the directory, and then handle the delta
-     * between installed and newly found/lost bundles and configurations.
-     *
-     */
-    public void run()
-    {
-        log("{" + POLL + " (ms) = " + poll + ", "
-                + DIR + " = " + watchedDirectory.getAbsolutePath() + ", "
-                + DEBUG + " = " + debug + ", "
-                + FILTER + " = " + filter + ", "
-                + START_NEW_BUNDLES + " = " + startBundles + "}", null);
-        initializeCurrentManagedBundles();
-        Map currentManagedConfigs = new HashMap(); // location -> Long(time)
 
         FilenameFilter flt;
-        if (filter != null)
+        if (filter != null && filter.length() > 0)
         {
             flt = new FilenameFilter()
             {
@@ -132,15 +116,175 @@
         {
             flt = null;
         }
+        scanner = new Scanner(watchedDirectory, flt);
+    }
+
+    /**
+     * Main run loop, will traverse the directory, and then handle the delta
+     * between installed and newly found/lost bundles and configurations.
+     *
+     */
+    public void run()
+    {
+        log("{" + POLL + " (ms) = " + poll + ", "
+                + DIR + " = " + watchedDirectory.getAbsolutePath() + ", "
+                + DEBUG + " = " + debug + ", "
+                + START_NEW_BUNDLES + " = " + startBundles + ", "
+                + TMPDIR + " = " + tmpDir + ", "
+                + FILTER + " = " + filter + "}", null);
+
+        initializeCurrentManagedBundles();
+
+        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 @@
             }
         }
     }
-    
+
+    ArtifactListener findListener(File artifact, List/* <ArtifactListener> */ listeners)
+    {
+        for (Iterator itL = listeners.iterator(); itL.hasNext();)
+        {
+            ArtifactListener listener = (ArtifactListener) itL.next();
+            if (listener.canHandle(artifact))
+            {
+                return listener;
+            }
+        }
+        return null;
+    }
+
+    boolean transformArtifact(Artifact artifact) {
+        if (artifact.getListener() instanceof ArtifactTransformer)
+        {
+            prepareDir(tmpDir);
+            try
+            {
+                File transformed = ((ArtifactTransformer) artifact.getListener()).transform(artifact.getJaredDirectory(), tmpDir);
+                if (transformed != null)
+                {
+                    artifact.setTransformed(transformed);
+                    return true;
+                }
+            }
+            catch (Exception e)
+            {
+                log("Unable to transform artifact: " + artifact.getPath().getAbsolutePath(), e);
+            }
+            return false;
+        }
+        return true;
+    }
+
+    private void deleteTransformedFile(Artifact artifact) {
+        if (artifact.getTransformed() != null
+                && !artifact.getTransformed().equals(artifact.getPath())
+                && !artifact.getTransformed().delete())
+        {
+            log("Unable to delete transformed artifact: " + artifact.getTransformed().getAbsolutePath(), null);
+        }
+    }
+
+    private void deleteJaredDirectory(Artifact artifact) {
+        if (artifact.getJaredDirectory() != null
+                && !artifact.getJaredDirectory().equals(artifact.getPath())
+                && !artifact.getJaredDirectory().delete())
+        {
+            log("Unable to delete jared artifact: " + artifact.getJaredDirectory().getAbsolutePath(), null);
+        }
+    }
+
     /**
      * 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 
+     *
+     * @param dir
      *            The directory File Install will monitor
      */
-    private void prepareWatchedDir(File watchedDirectory)
+    private void prepareDir(File dir)
     {
-        if (!watchedDirectory.exists() && !watchedDirectory.mkdirs())
+        if (!dir.exists() && !dir.mkdirs())
         {
             log("Cannot create folder "
-                + watchedDirectory
+                + dir
                 + ". Is the folder write-protected?", null);
-            throw new RuntimeException("Cannot create folder: " + watchedDirectory);
+            throw new RuntimeException("Cannot create folder: " + dir);
         }
 
-        if (!watchedDirectory.isDirectory())
+        if (!dir.isDirectory())
         {
-            log("Cannot watch "
-                + watchedDirectory
+            log("Cannot use "
+                + dir
                 + " because it's not a directory", null);
             throw new RuntimeException(
-                "Cannot start FileInstall to watch something that is not a directory");
-        }
-    }
-
-    /**
-     * 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
-        {
-            // 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(); )
-            {
-                String path = (String) e.next();
-                File f = new File(path);
-
-                if (!current.containsKey(path))
-                {
-                    // 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);
-                    }
-                }
-                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();)
-            {
-                String path = (String) e.next();
-                File f = new File(path);
-                if (deleteConfig(f))
-                {
-                    current.remove(path);
-                }
-            }
-        }
-        catch (Exception ee)
-        {
-            log("Processing config: ", ee);
-        }
-    }
-
-    /**
-     * 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)
-        {
-            if (debug != 0 && !reported)
-            {
-                log("Can't find a Configuration Manager, configurations do not work",
-                    null);
-                reported = true;
-            }
-            return false;
-        }
-
-        Properties p = new Properties();
-        InputStream in = new FileInputStream(f);
-        try
-        {
-            p.load(in);
-        }
-        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.
-     *
-     * @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)
-        {
-            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;
-        }
-    }
-    
-    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
-        {
-            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);
+                "Cannot start FileInstall using something that is not a directory");
         }
     }
 
@@ -444,82 +390,7 @@
      */
     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);
-            }
-        }
-    }
-
-    /**
-     * Answer the Log Service
-     *
-     * @return
-     */
-    LogService getLogService()
-    {
-        ServiceReference ref = context.getServiceReference(LogService.class.getName());
-        if (ref != null)
-        {
-            LogService log = (LogService) context.getService(ref);
-            return log;
-        }
-        return null;
-    }
-
-    /**
-     * 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
-     */
-    void traverse(Map/* <String, Jar> */ jars, Set configs, File jardir, FilenameFilter filter)
-    {
-        String list[] = jardir.list(filter);
-        if (list == 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);
-            }
-        }
+        Util.log(context, debug, message, e);
     }
 
     /**
@@ -530,23 +401,10 @@
      */
     boolean isFragment(Bundle bundle)
     {
-        PackageAdmin padmin;
-        if (FileInstall.padmin == null)
+        PackageAdmin padmin = FileInstall.getPackageAdmin();
+        if (padmin != null)
         {
-            return false;
-        }
-
-        try
-        {
-            padmin = (PackageAdmin) FileInstall.padmin.waitForService(10000);
-            if (padmin != null)
-            {
-                return padmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
-            }
-        }
-        catch (InterruptedException e)
-        {
-            // stupid exception
+            return padmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
         }
         return false;
     }
@@ -556,24 +414,20 @@
      */
     void refresh()
     {
-        PackageAdmin padmin;
-        try
+        PackageAdmin padmin = FileInstall.getPackageAdmin();
+        if (padmin != null)
         {
-            padmin = (PackageAdmin) FileInstall.padmin.waitForService(10000);
             padmin.refreshPackages(null);
         }
-        catch (InterruptedException e)
-        {
-            Thread.currentThread().interrupt();
-        }
     }
 
     /**
-     * Answer the long from a property.
+     * Retrieve a property as a long.
      *
-     * @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 long or the default value
      */
     long getLong(Dictionary properties, String property, long dflt)
     {
@@ -592,6 +446,42 @@
         return dflt;
     }
 
+    /**
+     * 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
+     */
+    File getFile(Dictionary properties, String property, File dflt)
+    {
+        String value = (String) properties.get(property);
+        if (value != null)
+        {
+            return new File(value);
+        }
+        return dflt;
+    }
+
+    /**
+     * Retrieve a property as a boolan.
+     *
+     * @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
+     */
+    boolean getBoolean(Dictionary properties, String property, boolean dflt)
+    {
+        String value = (String) properties.get(property);
+        if (value != null)
+        {
+            return Boolean.parseBoolean(value);
+        }
+        return dflt;
+    }
+
     public void close()
     {
         interrupt();
@@ -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())
+            File path = artifact.getPath();
+            // If the listener is an installer, ask for an update
+            if (artifact.getListener() instanceof ArtifactInstaller)
             {
-                return null; // Don't attempt to install it; nothing has changed.
+                ((ArtifactInstaller) artifact.getListener()).install(path);
             }
-            File file = new File(path);
-            InputStream in = new FileInputStream(file);
-            try
+            // else we need to ask for an update on the bundle
+            else if (artifact.getListener() instanceof ArtifactTransformer)
             {
-                // Some users wanted the location to be a URI (See FELIX-1269)
-                final String location = file.toURI().normalize().toString();
-                bundle = context.installBundle(location, in);
+                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());
             }
-            finally
-            {
-                in.close();
-            }
+            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)
+            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)
             {
-            	log("Failed to uninstall bundle: "
-                    + jar.getPath() + " with id: "
-                    + old.getBundleId()
-                    + ". The bundle has already been uninstalled", null);
-            	return null;
+                ((ArtifactInstaller) artifact.getListener()).uninstall(path);
             }
-            bundle.uninstall();
+            // 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();
+            }
             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)
+            File path = artifact.getPath();
+            // If the listener is an installer, ask for an update
+            if (artifact.getListener() instanceof ArtifactInstaller)
             {
-            	log("Failed to update bundle: "
-                    + jar.getPath() + " with ID "
-                    + jar.getBundleId()
-                    + ". The bundle has been uninstalled", null);
-            	return null;
+                ((ArtifactInstaller) artifact.getListener()).update(path);
             }
-            bundle.update(in);
-            startupFailures.remove(bundle);
-            jar.setLastModified(bundle.getLastModified());
-            log("Updated " + jar.getPath(), null);
-            return bundle;
-        }
-        catch (Exception e)
-        {
-            log("Failed to update bundle " + jar.getPath(), e);
-        }
-        finally
-        {
-            if (in != null)
+            // 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();
                 }
-                catch (IOException e)
-                {
-                }
             }
+            startupFailures.remove(bundle);
+            artifact.setLastModified(Util.getLastModified(path));
+            log("Updated " + path, null);
         }
-        return null;
+        catch (Exception e)
+        {
+            log("Failed to update artifact " + artifact.getPath(), e);
+        }
+        return bundle;
+    }
+
+    private void start(Collection/* <Bundle> */ bundles)
+    {
+        for (Iterator b = bundles.iterator(); b.hasNext(); )
+        {
+            start((Bundle) b.next());
+        }
     }
 
     private void start(Bundle bundle)
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java
index da31be4..6238f64 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/FileInstall.java
@@ -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
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java
new file mode 100644
index 0000000..0be8deb
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/Scanner.java
@@ -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;
+        }
+    }
+
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java
new file mode 100644
index 0000000..0a9de7d
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactInstaller.java
@@ -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;
+
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java
new file mode 100644
index 0000000..17b9e1a
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactListener.java
@@ -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);
+
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java
new file mode 100644
index 0000000..a056fdc
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/listener/ArtifactTransformer.java
@@ -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;
+
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java
index a81f376..0a8c651 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/util/Util.java
@@ -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
+     * 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 void log(BundleContext context, long debug, String message, Throwable e)
+    {
+        LogService log = getLogService(context);
+        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);
+            }
+        }
+    }
+
+    /**
+     * Answer the Log Service
+     *
      * @return
      */
-    public static boolean isValidJar(String path)
+    private static LogService getLogService(BundleContext context)
     {
-        JarFile jar = null;
-        try
+        ServiceReference ref = context.getServiceReference(LogService.class.getName());
+        if (ref != null)
         {
-            jar = new JarFile(path);
-            return true;
+            LogService log = (LogService) context.getService(ref);
+            return log;
         }
-        catch (IOException ioe)
-        {
-            return false;
-        }
-        finally
-        {
-            if (jar != null)
-            {
-                try
-                {
-                    jar.close();
+        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);
                 }
-                catch (IOException e)
-                {
-                    //do nothing
+            } 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;
+        }
+    }
+
 }
diff --git a/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml b/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml
index 6cf2117..1acfd0f 100644
--- a/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml
+++ b/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml
@@ -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">
diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/BundleTransformerTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/BundleTransformerTest.java
new file mode 100644
index 0000000..8f64573
--- /dev/null
+++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/BundleTransformerTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 junit.framework.TestCase;
+
+/**
+ * Test for the BundleTransformer
+ */
+public class BundleTransformerTest extends TestCase
+{
+
+    public void testCanRecognizeInvalidJar()
+    {
+        assertFalse(new BundleTransformer().canHandle(new File("src/test/resources/watched/firstjar.jar")));
+        assertFalse(new BundleTransformer().canHandle(new File("src/test/resources/watched/notexistentfile.jar")));
+    }
+
+}
diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/ConfigInstallerTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/ConfigInstallerTest.java
new file mode 100644
index 0000000..c1d8ce7
--- /dev/null
+++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/ConfigInstallerTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.util.Hashtable;
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+import org.easymock.MockControl;
+import org.easymock.ArgumentsMatcher;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Bundle;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.Configuration;
+
+/**
+ * Tests for ConfigInstaller
+ */
+public class ConfigInstallerTest extends TestCase {
+
+    MockControl mockBundleContextControl;
+    BundleContext mockBundleContext;
+    MockControl mockBundleControl;
+    Bundle mockBundle;
+    MockControl mockConfigurationAdminControl;
+    ConfigurationAdmin mockConfigurationAdmin;
+    MockControl mockConfigurationControl;
+    Configuration mockConfiguration;
+
+    protected void setUp() throws Exception
+    {
+        super.setUp();
+        mockBundleContextControl = MockControl.createControl( BundleContext.class );
+        mockBundleContext = ( BundleContext ) mockBundleContextControl.getMock();
+        mockBundleControl = MockControl.createControl( Bundle.class );
+        mockBundle = ( Bundle ) mockBundleControl.getMock();
+        mockConfigurationAdminControl = MockControl.createControl( ConfigurationAdmin.class );
+        mockConfigurationAdmin = ( ConfigurationAdmin ) mockConfigurationAdminControl.getMock();
+        mockConfigurationControl = MockControl.createControl( Configuration.class );
+        mockConfiguration = ( Configuration ) mockConfigurationControl.getMock();
+    }
+
+
+    public void testParsePidWithoutFactoryPid()
+    {
+        mockBundleContextControl.replay();
+        ConfigInstaller ci = new ConfigInstaller(null);
+
+        String path = "pid.cfg";
+        assertEquals( "Pid without Factory Pid calculated", "pid", ci.parsePid( path )[0] );
+        assertEquals( "Pid without Factory Pid calculated", null, ci.parsePid( path )[1] );
+    }
+
+
+    public void testParsePidWithFactoryPid()
+    {
+        mockBundleContextControl.replay();
+        ConfigInstaller ci = new ConfigInstaller(null);
+
+        String path = "factory-pid.cfg";
+        assertEquals( "Pid with Factory Pid calculated", "factory", ci.parsePid( path )[0] );
+        assertEquals( "Pid with Factory Pid calculated", "pid", ci.parsePid( path )[1] );
+    }
+
+    public void testGetNewFactoryConfiguration() throws Exception
+    {
+        mockConfigurationControl.replay();
+        mockConfigurationAdmin.listConfigurations( null );
+        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockConfigurationAdminControl.setReturnValue( null );
+        mockConfigurationAdmin.createFactoryConfiguration( "pid", null );
+        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
+        mockConfigurationAdminControl.replay();
+        mockBundleContext.createFilter( "" );
+        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockBundleContextControl.setReturnValue( null );
+        mockBundleContextControl.replay();
+
+        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
+        ConfigInstaller ci = new ConfigInstaller( mockBundleContext );
+
+        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid", "factoryPid" ) );
+
+        mockConfigurationAdminControl.verify();
+        mockConfigurationControl.verify();
+        mockBundleContextControl.verify();
+    }
+
+
+    public void testGetExistentFactoryConfiguration() throws Exception
+    {
+        mockConfigurationControl.replay();
+        mockConfigurationAdmin.listConfigurations( null );
+        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockConfigurationAdminControl.setReturnValue( null );
+        mockConfigurationAdmin.createFactoryConfiguration( "pid", null );
+        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
+        mockConfigurationAdminControl.replay();
+        mockBundleContext.createFilter( "" );
+        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockBundleContextControl.setReturnValue( null );
+        mockBundleContextControl.replay();
+
+        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
+        ConfigInstaller ci = new ConfigInstaller( mockBundleContext );
+
+        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid", "factoryPid" ) );
+
+        mockConfigurationAdminControl.verify();
+        mockConfigurationControl.verify();
+        mockBundleContextControl.verify();
+    }
+
+
+    public void testGetExistentNoFactoryConfiguration() throws Exception
+    {
+        mockConfigurationControl.replay();
+        mockConfigurationAdmin.listConfigurations( null );
+        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockConfigurationAdminControl.setReturnValue( null );
+        mockConfigurationAdmin.getConfiguration( "pid", null );
+        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
+        mockConfigurationAdminControl.replay();
+        mockBundleContext.createFilter( "" );
+        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockBundleContextControl.setReturnValue( null );
+        mockBundleContextControl.replay();
+
+        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
+        ConfigInstaller ci = new ConfigInstaller( mockBundleContext );
+
+        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid", null ) );
+
+        mockConfigurationAdminControl.verify();
+        mockConfigurationControl.verify();
+        mockBundleContextControl.verify();
+    }
+
+
+    public void testDeleteConfig() throws Exception
+    {
+        mockConfiguration.delete();
+        mockConfigurationControl.replay();
+        mockConfigurationAdmin.listConfigurations( null );
+        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockConfigurationAdminControl.setReturnValue( null );
+        mockConfigurationAdmin.getConfiguration( "pid", null );
+        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
+        mockConfigurationAdminControl.replay();
+        mockBundleContext.createFilter( "" );
+        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockBundleContextControl.setReturnValue( null );
+        mockBundleContextControl.replay();
+
+        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
+        ConfigInstaller ci = new ConfigInstaller( mockBundleContext );
+
+        assertTrue( ci.deleteConfig( new File( "pid.cfg" ) ) );
+
+        mockConfigurationAdminControl.verify();
+        mockConfigurationControl.verify();
+        mockBundleContextControl.verify();
+    }
+
+
+    public void testSetConfiguration() throws Exception
+    {
+        mockConfiguration.getBundleLocation();
+        mockConfigurationControl.setReturnValue( null );
+        mockConfiguration.update( new Hashtable() );
+        mockConfigurationControl.setMatcher( new ArgumentsMatcher()
+        {
+            public boolean matches( Object[] expected, Object[] actual )
+            {
+                return ( actual.length == 1 ) && ( (Dictionary) actual[0] ).get( "testkey" ).equals( "testvalue" );
+            }
+
+
+            public String toString( Object[] arg0 )
+            {
+                return arg0.toString();
+            }
+        } );
+        mockConfigurationControl.replay();
+        mockConfigurationAdmin.listConfigurations( null );
+        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockConfigurationAdminControl.setReturnValue( null );
+        mockConfigurationAdmin.getConfiguration( "firstcfg", null );
+        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
+        mockConfigurationAdminControl.replay();
+        mockBundleContext.createFilter( "" );
+        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
+        mockBundleContextControl.setReturnValue( null );
+        mockBundleContextControl.replay();
+
+        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
+        ConfigInstaller ci = new ConfigInstaller( mockBundleContext );
+
+        assertTrue( ci.setConfig( new File( "src/test/resources/watched/firstcfg.cfg" ) ) );
+
+        mockConfigurationAdminControl.verify();
+        mockConfigurationControl.verify();
+        mockBundleContextControl.verify();
+    }
+
+
+}
diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/DirectoryWatcherTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/DirectoryWatcherTest.java
index 0326ef5..3dd9116 100644
--- a/fileinstall/src/test/java/org/apache/felix/fileinstall/DirectoryWatcherTest.java
+++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/DirectoryWatcherTest.java
@@ -21,18 +21,13 @@
 
 import java.io.File;
 import java.util.Dictionary;
-import java.util.HashSet;
 import java.util.Hashtable;
-import java.util.Set;
 
 import junit.framework.TestCase;
 
-import org.easymock.ArgumentsMatcher;
 import org.easymock.MockControl;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
-import org.osgi.service.cm.Configuration;
-import org.osgi.service.cm.ConfigurationAdmin;
 import org.osgi.service.packageadmin.PackageAdmin;
 
 
@@ -51,10 +46,6 @@
     PackageAdmin mockPackageAdmin;
     MockControl mockBundleControl;
     Bundle mockBundle;
-    MockControl mockConfigurationAdminControl;
-    ConfigurationAdmin mockConfigurationAdmin;
-    MockControl mockConfigurationControl;
-    Configuration mockConfiguration;
 
 
     protected void setUp() throws Exception
@@ -66,10 +57,6 @@
         mockPackageAdmin = ( PackageAdmin ) mockPackageAdminControl.getMock();
         mockBundleControl = MockControl.createControl( Bundle.class );
         mockBundle = ( Bundle ) mockBundleControl.getMock();
-        mockConfigurationAdminControl = MockControl.createControl( ConfigurationAdmin.class );
-        mockConfigurationAdmin = ( ConfigurationAdmin ) mockConfigurationAdminControl.getMock();
-        mockConfigurationControl = MockControl.createControl( Configuration.class );
-        mockConfiguration = ( Configuration ) mockConfigurationControl.getMock();
     }
 
 
@@ -102,12 +89,60 @@
     }
 
 
+    public void testGetBooleanWithNonExistentProperty()
+    {
+        mockBundleContextControl.replay();
+        dw = new DirectoryWatcher( props, mockBundleContext );
+        assertEquals( "getBoolean gives the default value for non-existing properties", true, dw.getBoolean( props, TEST, true ) );
+    }
+
+
+    public void testGetBooleanWithExistentProperty()
+    {
+        props.put( TEST, "true" );
+        mockBundleContextControl.replay();
+        dw = new DirectoryWatcher( props, mockBundleContext );
+        assertEquals( "getBoolean retrieves the right property value", true, dw.getBoolean( props, TEST, false ) );
+    }
+
+
+    public void testGetBooleanWithIncorrectValue()
+    {
+        props.put( TEST, "incorrect" );
+
+        mockBundleContext.getServiceReference( "org.osgi.service.log.LogService" );
+        mockBundleContextControl.setReturnValue( null );
+        mockBundleContextControl.replay();
+        dw = new DirectoryWatcher( props, mockBundleContext );
+        assertEquals( "getBoolean retrieves the right property value", false, dw.getBoolean( props, TEST, true ) );
+    }
+
+
+    public void testGetFileWithNonExistentProperty()
+    {
+        mockBundleContextControl.replay();
+        dw = new DirectoryWatcher( props, mockBundleContext );
+        assertEquals( "getFile gives the default value for non-existing properties", new File("tmp"), dw.getFile( props, TEST, new File("tmp") ) );
+    }
+
+
+    public void testGetFileWithExistentProperty()
+    {
+        props.put( TEST, "test" );
+        mockBundleContextControl.replay();
+        dw = new DirectoryWatcher( props, mockBundleContext );
+        assertEquals( "getBoolean retrieves the right property value", new File("test"), dw.getFile( props, TEST, new File("tmp") ) );
+    }
+
+
     public void testParameterAfterInitialization()
     {
         props.put( DirectoryWatcher.POLL, "500" );
         props.put( DirectoryWatcher.DEBUG, "1" );
         props.put( DirectoryWatcher.START_NEW_BUNDLES, "false" );
         props.put( DirectoryWatcher.DIR, new File( "src/test/resources" ).getAbsolutePath() );
+        props.put( DirectoryWatcher.TMPDIR, new File( "src/test/resources" ).getAbsolutePath() );
+        props.put( DirectoryWatcher.FILTER, ".*\\.cfg" );
         mockBundleContextControl.replay();
         dw = new DirectoryWatcher( props, mockBundleContext );
 
@@ -115,7 +150,10 @@
         assertEquals( "DEBUG parameter correctly read", 1l, dw.debug );
         assertTrue( "DIR parameter correctly read", dw.watchedDirectory.getAbsolutePath().endsWith(
             "src" + File.separatorChar + "test" + File.separatorChar + "resources" ) );
+        assertTrue( "TMPDIR parameter correctly read", dw.tmpDir.getAbsolutePath().endsWith(
+            "src" + File.separatorChar + "test" + File.separatorChar + "resources" ) );
         assertEquals( "START_NEW_BUNDLES parameter correctly read", false, dw.startBundles );
+        assertEquals( "FILTER parameter correctly read", ".*\\.cfg", dw.filter );
     }
 
 
@@ -125,30 +163,14 @@
         mockBundleContextControl.replay();
         dw = new DirectoryWatcher( props, mockBundleContext );
 
+        assertTrue( "DIR parameter correctly read", dw.watchedDirectory.getAbsolutePath().endsWith(
+            "src" + File.separatorChar + "test" + File.separatorChar + "resources" ) );
         assertEquals( "Default POLL parameter correctly read", 2000l, dw.poll );
         assertEquals( "Default DEBUG parameter correctly read", -1l, dw.debug );
+        assertTrue( "Default TMPDIR parameter correctly read", dw.tmpDir.getAbsolutePath().endsWith(
+            File.separatorChar + "tmp" ) );
         assertEquals( "Default START_NEW_BUNDLES parameter correctly read", true, dw.startBundles );
-    }
-
-
-    public void testParsePidWithoutFactoryPid()
-    {
-        mockBundleContextControl.replay();
-        dw = new DirectoryWatcher( props, mockBundleContext );
-        String path = "pid.cfg";
-        assertEquals( "Pid without Factory Pid calculated", "pid", dw.parsePid( path )[0] );
-        assertEquals( "Pid without Factory Pid calculated", null, dw.parsePid( path )[1] );
-    }
-
-
-    public void testParsePidWithFactoryPid()
-    {
-        mockBundleContextControl.replay();
-        dw = new DirectoryWatcher( props, mockBundleContext );
-
-        String path = "factory-pid.cfg";
-        assertEquals( "Pid with Factory Pid calculated", "factory", dw.parsePid( path )[0] );
-        assertEquals( "Pid with Factory Pid calculated", "pid", dw.parsePid( path )[1] );
+        assertEquals( "Default FILTER parameter correctly read", null, dw.filter );
     }
 
 
@@ -173,145 +195,4 @@
     }
 
 
-    public void testGetNewFactoryConfiguration() throws Exception
-    {
-        mockConfigurationControl.replay();
-        mockConfigurationAdmin.listConfigurations( null );
-        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockConfigurationAdminControl.setReturnValue( null );
-        mockConfigurationAdmin.createFactoryConfiguration( "pid", null );
-        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
-        mockConfigurationAdminControl.replay();
-        mockBundleContext.createFilter( "" );
-        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockBundleContextControl.setReturnValue( null );
-        mockBundleContextControl.replay();
-
-        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
-        dw = new DirectoryWatcher( props, mockBundleContext );
-
-        assertEquals( "Factory configuration retrieved", mockConfiguration, dw.getConfiguration( "pid", "factoryPid" ) );
-
-        mockConfigurationAdminControl.verify();
-        mockConfigurationControl.verify();
-        mockBundleContextControl.verify();
-    }
-
-
-    public void testGetExistentFactoryConfiguration() throws Exception
-    {
-        mockConfigurationControl.replay();
-        mockConfigurationAdmin.listConfigurations( null );
-        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockConfigurationAdminControl.setReturnValue( null );
-        mockConfigurationAdmin.createFactoryConfiguration( "pid", null );
-        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
-        mockConfigurationAdminControl.replay();
-        mockBundleContext.createFilter( "" );
-        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockBundleContextControl.setReturnValue( null );
-        mockBundleContextControl.replay();
-
-        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
-        dw = new DirectoryWatcher( props, mockBundleContext );
-
-        assertEquals( "Factory configuration retrieved", mockConfiguration, dw.getConfiguration( "pid", "factoryPid" ) );
-
-        mockConfigurationAdminControl.verify();
-        mockConfigurationControl.verify();
-        mockBundleContextControl.verify();
-    }
-
-
-    public void testGetExistentNoFactoryConfiguration() throws Exception
-    {
-        mockConfigurationControl.replay();
-        mockConfigurationAdmin.listConfigurations( null );
-        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockConfigurationAdminControl.setReturnValue( null );
-        mockConfigurationAdmin.getConfiguration( "pid", null );
-        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
-        mockConfigurationAdminControl.replay();
-        mockBundleContext.createFilter( "" );
-        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockBundleContextControl.setReturnValue( null );
-        mockBundleContextControl.replay();
-
-        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
-        dw = new DirectoryWatcher( props, mockBundleContext );
-
-        assertEquals( "Factory configuration retrieved", mockConfiguration, dw.getConfiguration( "pid", null ) );
-
-        mockConfigurationAdminControl.verify();
-        mockConfigurationControl.verify();
-        mockBundleContextControl.verify();
-    }
-
-
-    public void testDeleteConfig() throws Exception
-    {
-        mockConfiguration.delete();
-        mockConfigurationControl.replay();
-        mockConfigurationAdmin.listConfigurations( null );
-        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockConfigurationAdminControl.setReturnValue( null );
-        mockConfigurationAdmin.getConfiguration( "pid", null );
-        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
-        mockConfigurationAdminControl.replay();
-        mockBundleContext.createFilter( "" );
-        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockBundleContextControl.setReturnValue( null );
-        mockBundleContextControl.replay();
-
-        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
-        dw = new DirectoryWatcher( props, mockBundleContext );
-
-        assertTrue( dw.deleteConfig( new File( "pid.cfg" ) ) );
-
-        mockConfigurationAdminControl.verify();
-        mockConfigurationControl.verify();
-        mockBundleContextControl.verify();
-    }
-
-
-    public void testSetConfiguration() throws Exception
-    {
-        mockConfiguration.getBundleLocation();
-        mockConfigurationControl.setReturnValue( null );
-        mockConfiguration.update( new Hashtable() );
-        mockConfigurationControl.setMatcher( new ArgumentsMatcher()
-        {
-            public boolean matches( Object[] expected, Object[] actual )
-            {
-                return ( actual.length == 1 ) && ( ( Dictionary ) actual[0] ).get( "testkey" ).equals( "testvalue" );
-            }
-
-
-            public String toString( Object[] arg0 )
-            {
-                return arg0.toString();
-            }
-        } );
-        mockConfigurationControl.replay();
-        mockConfigurationAdmin.listConfigurations( null );
-        mockConfigurationAdminControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockConfigurationAdminControl.setReturnValue( null );
-        mockConfigurationAdmin.getConfiguration( "firstcfg", null );
-        mockConfigurationAdminControl.setReturnValue( mockConfiguration );
-        mockConfigurationAdminControl.replay();
-        mockBundleContext.createFilter( "" );
-        mockBundleContextControl.setMatcher( MockControl.ALWAYS_MATCHER );
-        mockBundleContextControl.setReturnValue( null );
-        mockBundleContextControl.replay();
-
-        FileInstall.cmTracker = new MockServiceTracker( mockBundleContext, mockConfigurationAdmin );
-        dw = new DirectoryWatcher( props, mockBundleContext );
-
-        assertTrue( dw.setConfig( new File( "src/test/resources/watched/firstcfg.cfg" ) ) );
-
-        mockConfigurationAdminControl.verify();
-        mockConfigurationControl.verify();
-        mockBundleContextControl.verify();
-    }
-
 }
diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/util/UtilTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/util/UtilTest.java
index ca68a37..291349e 100644
--- a/fileinstall/src/test/java/org/apache/felix/fileinstall/util/UtilTest.java
+++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/util/UtilTest.java
@@ -53,9 +53,4 @@
         assertEquals("${a", Util.substVars("${a", "b", null, new Hashtable()));
     }
 
-    public void testCanRecognizeInvalidJar()
-    {
-        assertFalse(Util.isValidJar("src/test/resources/watched/firstjar.jar"));
-        assertFalse(Util.isValidJar("src/test/resources/watched/notexistentfile.jar"));
-    }
 }
\ No newline at end of file