[maven-release-plugin]  copy for tag org.apache.sling.installer.provider.file-1.1.0

git-svn-id: https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.installer.provider.file-1.1.0@1674248 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/file/pom.xml b/file/pom.xml
new file mode 100644
index 0000000..4c3d6b9
--- /dev/null
+++ b/file/pom.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>22</version>
+        <relativePath>../../../parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.sling.installer.provider.file</artifactId>
+    <version>1.1.0</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling File Installer</name>
+    <description> 
+        Installs OSGi bundles and configurations from the file system.
+    </description>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.installer.provider.file-1.1.0</connection>
+        <developerConnection> scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.installer.provider.file-1.1.0</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.installer.provider.file-1.1.0</url>
+    </scm>
+
+    <build>
+        <plugins>
+
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-Activator>
+                            org.apache.sling.installer.provider.file.impl.Activator
+                        </Bundle-Activator>
+                        <!-- 
+                             We need at least 3.1.2 as this allows reading comments from a config
+                         -->
+                        <Import-Package>
+                            org.apache.sling.installer.api;version="[3.1.2,4)",
+                            *
+                        </Import-Package>
+                        <Private-Package>
+                            org.apache.sling.installer.provider.file.impl.*
+                        </Private-Package>
+                        <Embed-Dependency>
+                            org.apache.felix.configadmin;inline="org/apache/felix/cm/file/ConfigurationHandler.*"
+                        </Embed-Dependency>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.installer.core</artifactId>
+            <version>3.5.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.settings</artifactId>
+            <version>1.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+      <!-- We use a class from the config admin implementation to read config files -->
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.configadmin</artifactId>
+            <version>1.2.8</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/Activator.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/Activator.java
new file mode 100644
index 0000000..2b06592
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/Activator.java
@@ -0,0 +1,87 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The <code>Activator</code>
+ */
+public class Activator implements BundleActivator {
+
+    public static final String KEY_DIR = "sling.fileinstall.dir";
+    public static final String KEY_DELAY = "sling.fileinstall.interval";
+    public static final String KEY_WRITEBACK = "sling.fileinstall.writeback";
+
+    /** The services listener will activate the installer. */
+    private ServicesListener servicesListener;
+
+    /**
+     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
+     */
+    public void start(final BundleContext context) {
+        // read initial scan configurations
+        final List<ScanConfiguration> configs = new ArrayList<ScanConfiguration>();
+        final Object dir = getProp(context, KEY_DIR);
+        if ( dir != null ) {
+            Long delay = null;
+            final Object interval = getProp(context, KEY_DELAY);
+            if ( interval != null ) {
+                if ( interval instanceof Number ) {
+                    delay = ((Number)interval).longValue();
+                } else {
+                    delay = Long.valueOf(interval.toString());
+                }
+            }
+            final StringTokenizer st = new StringTokenizer(dir.toString(), ",");
+            while ( st.hasMoreTokens() ) {
+                final ScanConfiguration sc = new ScanConfiguration();
+                sc.directory = st.nextToken();
+                sc.scanInterval = delay;
+
+                configs.add(sc);
+            }
+        }
+        this.servicesListener = new ServicesListener(context, configs);
+    }
+
+    /**
+     * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
+     */
+    public void stop(final BundleContext context) {
+        this.servicesListener.deactivate();
+        this.servicesListener = null;
+    }
+
+    public static Object getProp(final BundleContext bundleContext, final String key) {
+        Object o = bundleContext.getProperty(key);
+        if (o == null) {
+            o = System.getProperty(key);
+            if ( o == null ) {
+                o = System.getProperty(key.toUpperCase().replace('.', '_'));
+            }
+        }
+        return o;
+    }
+}
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileChangesListener.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileChangesListener.java
new file mode 100644
index 0000000..2fd34d5
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileChangesListener.java
@@ -0,0 +1,31 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+import java.io.File;
+import java.util.List;
+
+public interface FileChangesListener {
+
+    void initialSet(List<File> files);
+
+    void updated(List<File> added, List<File> changed, List<File> removed);
+
+    String getScheme();
+}
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileInstaller.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileInstaller.java
new file mode 100644
index 0000000..a646f61
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileInstaller.java
@@ -0,0 +1,234 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.cm.file.ConfigurationHandler;
+import org.apache.sling.installer.api.InstallableResource;
+import org.apache.sling.installer.api.OsgiInstaller;
+import org.apache.sling.installer.api.UpdateHandler;
+import org.apache.sling.installer.api.UpdateResult;
+import org.apache.sling.settings.SlingSettingsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>FileInstaller</code> manages the file installers and
+ * handles updates.
+ *
+ */
+public class FileInstaller
+    implements UpdateHandler {
+
+    /** The scheme we use to register our resources. */
+    public static final String SCHEME_PREFIX = "fileinstall";
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** All active scan configurations. */
+    private final List<ScanConfiguration> scanConfigurations = new ArrayList<ScanConfiguration>();
+
+    /** All monitors. */
+    private final List<FileMonitor> monitors = new ArrayList<FileMonitor>();
+
+    private final boolean writeBack;
+
+    public FileInstaller(final List<ScanConfiguration> configs, final boolean writeBack) {
+        this.writeBack = writeBack;
+        if ( configs != null ) {
+            scanConfigurations.addAll(configs);
+        }
+    }
+
+    public boolean hasConfigurations() {
+        return !this.scanConfigurations.isEmpty();
+    }
+
+    public void start(final OsgiInstaller installer, final SlingSettingsService settings) {
+        for(final ScanConfiguration config : this.scanConfigurations) {
+            String key = config.directory;
+            if ( key.startsWith(settings.getSlingHomePath() + File.separator) ) {
+                key = "${sling.home}" + key.substring(settings.getSlingHomePath().length());
+            }
+            logger.debug("Starting monitor for {}", config.directory);
+            this.monitors.add(new FileMonitor(new File(config.directory),
+                    config.scanInterval, new Installer(installer, settings, config.directory, hash(key))));
+        }
+    }
+
+    public void stop() {
+        for(final FileMonitor monitor : this.monitors) {
+            monitor.stop();
+        }
+        this.monitors.clear();
+
+    }
+
+    public String[] getSchemes() {
+        final String[] schemes = new String[this.monitors.size()];
+        int index = 0;
+
+        for(final FileMonitor m : this.monitors) {
+            schemes[index] = m.getListener().getScheme();
+            index++;
+        }
+
+        return schemes;
+    }
+
+    /**
+     * @see org.apache.sling.installer.api.UpdateHandler#handleRemoval(java.lang.String, java.lang.String, java.lang.String)
+     */
+    public UpdateResult handleRemoval(final String resourceType,
+            final String id,
+            final String url) {
+        if ( !this.writeBack ) {
+            return null;
+        }
+        final int pos = url.indexOf(':');
+        final String path = url.substring(pos + 1);
+        // remove
+        logger.debug("Removal of {}", path);
+        final File file = new File(path);
+        if ( file.exists() ) {
+            file.delete();
+        }
+        return new UpdateResult(url);
+    }
+
+    /**
+     * @see org.apache.sling.installer.api.UpdateHandler#handleUpdate(java.lang.String, java.lang.String, java.lang.String, java.util.Dictionary, Map)
+     */
+    public UpdateResult handleUpdate(final String resourceType,
+            final String id,
+            final String url,
+            final Dictionary<String, Object> dict,
+            final Map<String, Object> attributes) {
+        return this.handleUpdate(resourceType, id, url, null, dict, attributes);
+    }
+
+    /**
+     * @see org.apache.sling.installer.api.UpdateHandler#handleUpdate(java.lang.String, java.lang.String, java.lang.String, java.io.InputStream, Map)
+     */
+    public UpdateResult handleUpdate(final String resourceType,
+            final String id,
+            final String url,
+            final InputStream is,
+            final Map<String, Object> attributes) {
+        return this.handleUpdate(resourceType, id, url, is, null, attributes);
+    }
+
+    /**
+     * Internal implementation of update handling
+     */
+    private UpdateResult handleUpdate(final String resourceType,
+            final String id,
+            final String url,
+            final InputStream is,
+            final Dictionary<String, Object> dict,
+            final Map<String, Object> attributes) {
+        if ( !this.writeBack ) {
+            return null;
+        }
+
+        // we only handle add/update of configs for now
+        if ( !resourceType.equals(InstallableResource.TYPE_CONFIG) ) {
+            return null;
+        }
+
+        try {
+            final String path;
+            final String prefix;
+            if ( url != null ) {
+                // update
+                final int pos = url.indexOf(':');
+                final String oldPath = url.substring(pos + 1);
+                prefix = url.substring(0, pos);
+                // ensure extension 'config'
+                if ( !oldPath.endsWith(".config") ) {
+                    final File file = new File(oldPath);
+                    if ( file.exists() ) {
+                        file.delete();
+                    }
+                    final int lastDot = oldPath.lastIndexOf('.');
+                    final int lastSlash = oldPath.lastIndexOf('/');
+                    if ( lastDot <= lastSlash ) {
+                        path = oldPath + ".config";
+                    } else {
+                        path = oldPath.substring(0, lastDot) + ".config";
+                    }
+                } else {
+                    path = oldPath;
+                }
+                logger.debug("Update of {} at {}", resourceType, path);
+            } else {
+                // add
+                final FileMonitor first = this.monitors.get(0);
+                path = first.getRoot().getAbsolutePath() + '/' + id + ".config";
+                prefix = first.getListener().getScheme();
+                logger.debug("Add of {} at {}", resourceType, path);
+            }
+
+            final File file = new File(path);
+            file.getParentFile().mkdirs();
+            final FileOutputStream fos = new FileOutputStream(file);
+            try {
+                fos.write("# Configuration created by Apache Sling File Installer\n".getBytes("UTF-8"));
+                ConfigurationHandler.write(fos, dict);
+            } finally {
+                try {
+                    fos.close();
+                } catch (final IOException ignore) {}
+            }
+
+            final UpdateResult result = new UpdateResult(prefix + ':' + path);
+            result.setResourceIsMoved(true);
+            return result;
+        } catch (final IOException e) {
+            logger.error("Unable to add/update resource " + resourceType + ':' + id, e);
+            return null;
+        }
+    }
+
+    /**
+     * Hash the string
+     */
+    private static String hash(final String value) {
+        try {
+            final MessageDigest d = MessageDigest.getInstance("MD5");
+            d.update(value.getBytes("UTF-8"));
+            final BigInteger bigInt = new BigInteger(1, d.digest());
+            return new String(bigInt.toString(16));
+        } catch (final Exception ignore) {
+            // if anything goes wrong we just return the value
+            return value;
+        }
+    }
+}
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileMonitor.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileMonitor.java
new file mode 100644
index 0000000..085d07e
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/FileMonitor.java
@@ -0,0 +1,287 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class is a monitor for the file system
+ * that periodically checks for changes.
+ */
+public class FileMonitor extends TimerTask {
+
+    /** The logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final Timer timer = new Timer();
+    private boolean stop = false;
+    private boolean stopped = true;
+
+    private final Monitorable root;
+
+    private final FileChangesListener listener;
+
+    /**
+     * Creates a new instance of this class.
+     * @param interval The interval between executions of the task, in milliseconds.
+     */
+    public FileMonitor(final File rootDir, final Long interval, final FileChangesListener listener) {
+        this.listener = listener;
+        this.root = new Monitorable(rootDir);
+        createStatus(this.root);
+        final List<File> files = new ArrayList<File>();
+        collect(this.root.file, files);
+        this.listener.initialSet(files);
+        logger.debug("Starting file monitor for {} with an interval of {}ms", this.root.file, interval);
+        timer.schedule(this, 0, (interval != null ? interval : 5000));
+    }
+
+    public File getRoot() {
+        return this.root.file;
+    }
+
+    public FileChangesListener getListener() {
+        return this.listener;
+    }
+
+    private void collect(final File file, final List<File> files) {
+        if ( file.exists() ) {
+            if ( file.isDirectory() ) {
+                final File[] children = file.listFiles();
+                if ( children != null ) {
+                    for(final File child : children ) {
+                        collect(child, files);
+                    }
+                }
+            } else {
+                files.add(file);
+            }
+        }
+    }
+
+    private void collectDeleted(final Monitorable m, final List<File> files) {
+        if ( m.status instanceof DirStatus ) {
+            for(final Monitorable child : ((DirStatus)m.status).children ) {
+                collectDeleted(child, files);
+            }
+        } else {
+            files.add(m.file);
+        }
+    }
+
+    private final static class Collector {
+        public final List<File> added = new ArrayList<File>();
+        public final List<File> removed = new ArrayList<File>();
+        public final List<File> changed = new ArrayList<File>();
+    }
+
+    /**
+     * Stop periodically executing this task. If the task is currently executing it
+     * will never be run again after the current execution, otherwise it will simply
+     * never run (again).
+     */
+    void stop() {
+        synchronized (timer) {
+            if (!stop) {
+                stop = true;
+                cancel();
+                timer.cancel();
+            }
+
+            boolean interrupted = false;
+            while (!stopped) {
+                try {
+                    timer.wait();
+                }
+                catch (InterruptedException e) {
+                    interrupted = true;
+                }
+            }
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        logger.debug("Stopped file monitor for {}", this.root.file);
+    }
+
+    /**
+     * @see java.util.TimerTask#run()
+     */
+    public void run() {
+        synchronized (timer) {
+            stopped = false;
+            if (stop) {
+                stopped = true;
+                timer.notifyAll();
+                return;
+            }
+        }
+        synchronized ( this ) {
+            try {
+                final Collector c = new Collector();
+                this.check(this.root, c);
+                this.listener.updated(c.added, c.changed, c.removed);
+            } catch (Exception e) {
+                // ignore this
+            }
+        }
+        synchronized (timer) {
+            stopped = true;
+            timer.notifyAll();
+        }
+    }
+
+    /**
+     * Check the monitorable
+     * @param monitorable The monitorable to check
+     * @param localEA The event admin
+     */
+    private void check(final Monitorable monitorable, final Collector collector) {
+        logger.debug("Checking {}", monitorable.file);
+        // if the file is non existing, check if it has been readded
+        if ( monitorable.status instanceof NonExistingStatus ) {
+            if ( monitorable.file.exists() ) {
+                // new file and reset status
+                createStatus(monitorable);
+                final List<File> files = new ArrayList<File>();
+                collect(monitorable.file, files);
+                for(final File file : files ) {
+                    collector.added.add(file);
+                }
+            }
+        } else {
+            // check if the file has been removed
+            if ( !monitorable.file.exists() ) {
+                // removed file and update status
+                final List<File> files = new ArrayList<File>();
+                collectDeleted(monitorable, files);
+                for(final File file : files ) {
+                    collector.removed.add(file);
+                }
+                monitorable.status = NonExistingStatus.SINGLETON;
+            } else {
+                // check for changes
+                final FileStatus fs = (FileStatus)monitorable.status;
+                boolean changed = false;
+                if ( fs.lastModified < monitorable.file.lastModified() ) {
+                    fs.lastModified = monitorable.file.lastModified();
+                    // changed
+                    if ( monitorable.file.isFile() ) {
+                        collector.changed.add(monitorable.file);
+                    }
+                    changed = true;
+                }
+                if ( fs instanceof DirStatus ) {
+                    // directory
+                    final DirStatus ds = (DirStatus)fs;
+                    for(int i=0; i<ds.children.length; i++) {
+                        check(ds.children[i], collector);
+                    }
+                    // if the dir changed we have to update
+                    if ( changed ) {
+                        // and now update
+                        final File[] files = monitorable.file.listFiles();
+                        if (files != null) {
+                            final Monitorable[] children = new Monitorable[files.length];
+                            for (int i = 0; i < files.length; i++) {
+                                // search in old list
+                                for (int m = 0; m < ds.children.length; m++) {
+                                    if (ds.children[m].file.equals(files[i])) {
+                                        children[i] = ds.children[m];
+                                        break;
+                                    }
+                                }
+                                if (children[i] == null) {
+                                    children[i] = new Monitorable(files[i]);
+                                    children[i].status = NonExistingStatus.SINGLETON;
+                                    check(children[i], collector);
+                                }
+                            }
+                            ds.children = children;
+                        } else {
+                            ds.children = new Monitorable[0];
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Create a status object for the monitorable
+     */
+    private static void createStatus(final Monitorable monitorable) {
+        if ( !monitorable.file.exists() ) {
+            monitorable.status = NonExistingStatus.SINGLETON;
+        } else if ( monitorable.file.isFile() ) {
+            monitorable.status = new FileStatus(monitorable.file);
+        } else {
+            monitorable.status = new DirStatus(monitorable.file);
+        }
+    }
+
+    /** The monitorable to hold the resource path, the file and the status. */
+    private static final class Monitorable {
+        public final File   file;
+        public Object status;
+
+        public Monitorable(final File file) {
+            this.file = file;
+        }
+    }
+
+    /** Status for files. */
+    private static class FileStatus {
+        public long lastModified;
+        public FileStatus(final File file) {
+            this.lastModified = file.lastModified();
+        }
+    }
+
+    /** Status for directories. */
+    private static final class DirStatus extends FileStatus {
+        public Monitorable[] children;
+
+        public DirStatus(final File dir) {
+            super(dir);
+            final File[] files = dir.listFiles();
+            if (files != null) {
+                this.children = new Monitorable[files.length];
+                for (int i = 0; i < files.length; i++) {
+                    this.children[i] = new Monitorable(files[i]);
+                    FileMonitor.createStatus(this.children[i]);
+                }
+            } else {
+                this.children = new Monitorable[0];
+            }
+        }
+    }
+
+    /** Status for non existing files. */
+    private static final class NonExistingStatus {
+        public static NonExistingStatus SINGLETON = new NonExistingStatus();
+    }
+}
\ No newline at end of file
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/Installer.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/Installer.java
new file mode 100644
index 0000000..1b05e78
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/Installer.java
@@ -0,0 +1,196 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.installer.api.InstallableResource;
+import org.apache.sling.installer.api.OsgiInstaller;
+import org.apache.sling.settings.SlingSettingsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>Installer</code> is the service calling the
+ * OSGi installer
+ *
+ */
+public class Installer
+    implements FileChangesListener {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The OSGi installer service. */
+    private final OsgiInstaller installer;
+
+    /** The settings service. */
+    private final Set<String> activeRunModes;
+
+    /** The scheme to use. */
+    private final String scheme;
+
+    /** Prefix. */
+    private final String prefix;
+
+    public Installer(final OsgiInstaller installer,
+            final SlingSettingsService settings,
+            final String root,
+            final String id) {
+        this.scheme = FileInstaller.SCHEME_PREFIX + id;
+        this.installer = installer;
+        this.activeRunModes = settings.getRunModes();
+        this.prefix = new File(root).getAbsolutePath() + File.separator;
+    }
+
+    /**
+     * @see org.apache.sling.installer.provider.file.impl.FileChangesListener#getScheme()
+     */
+    public String getScheme() {
+        return this.scheme;
+    }
+
+    /**
+     * @see org.apache.sling.installer.provider.file.impl.FileChangesListener#initialSet(java.util.List)
+     */
+    public void initialSet(final List<File> files) {
+        logger.debug("Initial set for {}", this.scheme);
+        final List<InstallableResource> resources = new ArrayList<InstallableResource>();
+        for(final File f : files) {
+            logger.debug("Initial file {}", f);
+            final InstallableResource resource = this.createResource(f);
+            if ( resource != null ) {
+                resources.add(resource);
+            }
+        }
+        this.installer.registerResources(this.scheme, resources.toArray(new InstallableResource[resources.size()]));
+    }
+
+    /**
+     * @see org.apache.sling.installer.provider.file.impl.FileChangesListener#updated(java.util.List, java.util.List, java.util.List)
+     */
+    public void updated(List<File> added, List<File> changed, List<File> removed) {
+        final List<InstallableResource> updated;
+        if ( (added != null && added.size() > 0) || (changed != null && changed.size() > 0) ) {
+            updated = new ArrayList<InstallableResource>();
+            if ( added != null ) {
+                for(final File f : added) {
+                    logger.debug("Added file {}", f);
+                    final InstallableResource resource = this.createResource(f);
+                    if ( resource != null ) {
+                        updated.add(resource);
+                    }
+                }
+            }
+            if ( changed != null ) {
+                for(final File f : changed) {
+                    logger.debug("Changed file {}", f);
+                    final InstallableResource resource = this.createResource(f);
+                    if ( resource != null ) {
+                        updated.add(resource);
+                    }
+                }
+            }
+        } else {
+            updated = null;
+        }
+        final String[] removedUrls;
+        if ( removed != null && removed.size() > 0 ) {
+            removedUrls = new String[removed.size()];
+            int index = 0;
+            for(final File f : removed) {
+                removedUrls[index] = f.getAbsolutePath();
+                logger.debug("Removed file {}", removedUrls[index]);
+                index++;
+            }
+        } else {
+            removedUrls = null;
+        }
+        if ( updated != null || removedUrls != null ) {
+            this.installer.updateResources(this.scheme,
+                    updated == null ? null : updated.toArray(new InstallableResource[updated.size()]), removedUrls);
+        }
+    }
+
+    private InstallableResource createResource(final File file) {
+        try {
+            // check for run modes
+            final String name = file.getAbsolutePath().substring(this.prefix.length()).replace(File.separatorChar, '/');
+            boolean isActive = true;
+            Integer prio = null;
+            final int pos = name.indexOf('/');
+            if ( pos != -1 && name.startsWith("install.") ) {
+                final String runModes = name.substring(8, pos);
+                final int activeModes = this.isActive(runModes);
+                if ( activeModes > 0 ) {
+                    prio = InstallableResource.DEFAULT_PRIORITY + activeModes;
+                } else {
+                    isActive = false;
+                }
+            }
+            if ( isActive ) {
+                final InputStream is = new FileInputStream(file);
+                final String digest = String.valueOf(file.lastModified());
+                // if this is a bundle check for start level directory!
+                final Dictionary<String, Object> dict = new Hashtable<String, Object>();
+                if ( file.getName().endsWith(".jar") || file.getName().endsWith(".war") ) {
+                    final String parentName = file.getParentFile().getName();
+                    try {
+                        final int startLevel = Integer.valueOf(parentName);
+                        if ( startLevel > 0 ) {
+                            dict.put(InstallableResource.BUNDLE_START_LEVEL, startLevel);
+                        }
+                    } catch (NumberFormatException nfe) {
+                        // ignore this
+                    }
+                }
+                dict.put(InstallableResource.RESOURCE_URI_HINT, file.toURI().toString());
+                return new InstallableResource(file.getAbsolutePath(), is, dict, digest,
+                    null, prio);
+            } else {
+                logger.info("Ignoring inactive resource at {}", file);
+            }
+
+        } catch (IOException io) {
+            logger.error("Unable to read file " + file, io);
+        }
+        return null;
+    }
+
+    private int isActive(final String runModesString) {
+        final String[] runModes = runModesString.split("\\.");
+        boolean active = true;
+        for(final String mode : runModes) {
+            if ( !activeRunModes.contains(mode) ) {
+                active = false;
+                break;
+            }
+        }
+
+        return active ? runModes.length : 0;
+    }
+}
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/ScanConfiguration.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/ScanConfiguration.java
new file mode 100644
index 0000000..0e82304
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/ScanConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+/**
+ * A scan configuration for the file install.
+ */
+public class ScanConfiguration {
+
+    public String directory;
+
+    public Long   scanInterval;
+}
diff --git a/file/src/main/java/org/apache/sling/installer/provider/file/impl/ServicesListener.java b/file/src/main/java/org/apache/sling/installer/provider/file/impl/ServicesListener.java
new file mode 100644
index 0000000..90d016a
--- /dev/null
+++ b/file/src/main/java/org/apache/sling/installer/provider/file/impl/ServicesListener.java
@@ -0,0 +1,200 @@
+/*
+ * 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.sling.installer.provider.file.impl;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import org.apache.sling.installer.api.OsgiInstaller;
+import org.apache.sling.installer.api.UpdateHandler;
+import org.apache.sling.settings.SlingSettingsService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ServicesListener</code> listens for the required services
+ * and starts/stops the scanners based on the availability of the
+ * services.
+ */
+public class ServicesListener {
+
+    /** The logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The name of the installer service. */
+    private static final String INSTALLER_SERVICE_NAME = OsgiInstaller.class.getName();
+
+    /** The name of the settings service. */
+    private static final String SETTINGS_SERVICE_NAME = SlingSettingsService.class.getName();
+
+    /** The bundle context. */
+    private final BundleContext bundleContext;
+
+    /** The listener for the installer. */
+    private final Listener installerListener;
+
+    /** The listener for the settings service. */
+    private final Listener settingsListener;
+
+    /** The file installer. */
+    private final FileInstaller installer;
+
+    /** Service registration. */
+    private ServiceRegistration registration;
+
+    private boolean running = false;
+
+    public ServicesListener(final BundleContext bundleContext,
+            final List<ScanConfiguration> configs) {
+        this.bundleContext = bundleContext;
+        boolean writeBack = true;
+        final Object writeBackObj = Activator.getProp(this.bundleContext, Activator.KEY_WRITEBACK);
+        if ( writeBackObj != null && "false".equalsIgnoreCase(writeBackObj.toString())) {
+            writeBack = false;
+        }
+        this.installer = new FileInstaller(configs, writeBack);
+        this.installerListener = new Listener(INSTALLER_SERVICE_NAME);
+        this.settingsListener = new Listener(SETTINGS_SERVICE_NAME);
+        this.installerListener.start();
+        this.settingsListener.start();
+    }
+
+    public synchronized void notifyChange() {
+        final boolean shouldRun = this.installer.hasConfigurations();
+        if ( (shouldRun && !running) || (!shouldRun && running) ) {
+            final OsgiInstaller installer = (OsgiInstaller)this.installerListener.getService();
+            final SlingSettingsService settings = (SlingSettingsService)this.settingsListener.getService();
+            if ( installer != null && settings != null && !running ) {
+                logger.debug("Starting scanner");
+                this.startScanner(installer, settings);
+            } else if ( running && (installer == null || settings == null) ) {
+                logger.debug("Stopping scanner");
+                this.stopScanner();
+            }
+        }
+    }
+
+    /**
+     * Deactivate this listener.
+     */
+    public void deactivate() {
+        this.installerListener.deactivate();
+        this.stopScanner();
+    }
+
+    /** Vendor of all registered services. */
+    public static final String VENDOR = "The Apache Software Foundation";
+
+    private void startScanner(final OsgiInstaller installer, final SlingSettingsService settings) {
+        if ( !running ) {
+            this.installer.start(installer, settings);
+            final Dictionary<String, Object> props = new Hashtable<String, Object>();
+            props.put(Constants.SERVICE_DESCRIPTION, "Apache Sling File Installer Controller Service");
+            props.put(Constants.SERVICE_VENDOR, VENDOR);
+            props.put(UpdateHandler.PROPERTY_SCHEMES, this.installer.getSchemes());
+
+            this.registration = this.bundleContext.registerService(UpdateHandler.class.getName(),
+                    this.installer, props);
+            running = true;
+        }
+    }
+
+    private void stopScanner() {
+        if ( running ) {
+            if ( this.registration != null ) {
+                this.registration.unregister();
+                this.registration = null;
+            }
+            this.installer.stop();
+            running = false;
+        }
+    }
+
+    protected final class Listener implements ServiceListener {
+
+        private final String serviceName;
+
+        private ServiceReference reference;
+        private Object service;
+
+        public Listener(final String serviceName) {
+            this.serviceName = serviceName;
+        }
+
+        public void start() {
+            this.retainService();
+            try {
+                bundleContext.addServiceListener(this, "("
+                        + Constants.OBJECTCLASS + "=" + serviceName + ")");
+            } catch (final InvalidSyntaxException ise) {
+                // this should really never happen
+                throw new RuntimeException("Unexpected exception occured.", ise);
+            }
+        }
+
+        public void deactivate() {
+            bundleContext.removeServiceListener(this);
+        }
+
+        public synchronized Object getService() {
+            return this.service;
+        }
+        private synchronized void retainService() {
+            if ( this.reference == null ) {
+                this.reference = bundleContext.getServiceReference(this.serviceName);
+                if ( this.reference != null ) {
+                    this.service = bundleContext.getService(this.reference);
+                    if ( this.service == null ) {
+                        this.reference = null;
+                    } else {
+                        notifyChange();
+                    }
+                }
+            }
+        }
+
+        private synchronized void releaseService() {
+            if ( this.reference != null ) {
+                this.service = null;
+                bundleContext.ungetService(this.reference);
+                this.reference = null;
+                notifyChange();
+            }
+        }
+
+        /**
+         * @see org.osgi.framework.ServiceListener#serviceChanged(org.osgi.framework.ServiceEvent)
+         */
+        public void serviceChanged(ServiceEvent event) {
+            if (event.getType() == ServiceEvent.REGISTERED && this.service == null ) {
+                this.retainService();
+            } else if ( event.getType() == ServiceEvent.UNREGISTERING && this.service != null ) {
+                this.releaseService();
+            }
+        }
+    }
+}