You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:38:48 UTC

[sling-org-apache-sling-fsresource] 15/29: SLING-6537 FileVault XML support

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.fsresource-2.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-fsresource.git

commit 1816787009b9791647fb67e72711a63fa31c9851
Author: Stefan Seifert <ss...@apache.org>
AuthorDate: Mon Mar 6 21:48:18 2017 +0000

    SLING-6537 FileVault XML support
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/fsresource@1785771 13f79535-47bb-0310-9956-ffa450edef68
---
 .../fsprovider/internal/ContentFileExtensions.java |   3 +-
 .../sling/fsprovider/internal/FileMonitor.java     |  98 ++++++---
 .../apache/sling/fsprovider/internal/FsMode.java   |  41 ++++
 .../fsprovider/internal/FsResourceProvider.java    | 147 +++++++++----
 .../internal/InitialContentImportOptions.java      |  76 +++++++
 .../fsprovider/internal/mapper/ContentFile.java    |  53 +++--
 .../internal/mapper/ContentFileResourceMapper.java |  16 +-
 .../internal/mapper/FileVaultResourceMapper.java   | 201 ++++++++++++++++++
 .../fsprovider/internal/mapper/jcr/FsNode.java     |  20 +-
 .../internal/mapper/jcr/FsNodeIterator.java        |   9 +-
 .../internal/parser/ContentFileParserUtil.java     |   6 +-
 .../sling/fsprovider/internal/FileMonitorTest.java |  60 ++----
 .../fsprovider/internal/FileVaultContentTest.java  | 147 +++++++++++++
 .../internal/FileVaultFileMonitorTest.java         | 232 +++++++++++++++++++++
 .../internal/InitialContentImportOptionsTest.java  |  59 ++++++
 .../fsprovider/internal/JcrXmlContentTest.java     |   5 +-
 .../sling/fsprovider/internal/JsonContentTest.java |   7 +-
 .../sling/fsprovider/internal/TestUtils.java       |  28 +++
 .../vaultfs-test/META-INF/vault/filter.xml         |   4 +-
 .../jcr_root/content/dam/talk.png/.content.xml     |   2 -
 20 files changed, 1053 insertions(+), 161 deletions(-)

diff --git a/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java b/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java
index 5097750..f9cacec 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java
@@ -41,8 +41,9 @@ public final class ContentFileExtensions {
      * @return Content file name suffix or null if not a context file.
      */
     public String getSuffix(File file) {
+        String fileName = "/" + file.getName();
         for (String suffix : contentFileSuffixes) {
-            if (StringUtils.endsWith(file.getName(), suffix)) {
+            if (StringUtils.endsWith(fileName, suffix)) {
                 return suffix;
             }
         }
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java b/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
index 7d77482..319e712 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
@@ -18,6 +18,8 @@
  */
 package org.apache.sling.fsprovider.internal;
 
+import static org.apache.jackrabbit.vault.util.Constants.ROOT_DIR;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
@@ -26,6 +28,7 @@ import java.util.Timer;
 import java.util.TimerTask;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.vault.util.PlatformNameFormat;
 import org.apache.sling.api.resource.observation.ResourceChange;
 import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
 import org.apache.sling.fsprovider.internal.mapper.ContentFile;
@@ -50,7 +53,7 @@ public final class FileMonitor extends TimerTask {
     private final Monitorable root;
 
     private final FsResourceProvider provider;
-    
+    private final FsMode fsMode;
     private final ContentFileExtensions contentFileExtensions;
     private final ContentFileCache contentFileCache;
 
@@ -59,12 +62,19 @@ public final class FileMonitor extends TimerTask {
      * @param provider The resource provider.
      * @param interval The interval between executions of the task, in milliseconds.
      */
-    public FileMonitor(final FsResourceProvider provider, final long interval,
+    public FileMonitor(final FsResourceProvider provider, final long interval, FsMode fsMode,
             final ContentFileExtensions contentFileExtensions, final ContentFileCache contentFileCache) {
         this.provider = provider;
+        this.fsMode = fsMode;
         this.contentFileExtensions = contentFileExtensions;
         this.contentFileCache = contentFileCache;
-        this.root = new Monitorable(this.provider.getProviderRoot(), this.provider.getRootFile(), null);
+        
+        File rootFile = this.provider.getRootFile();
+        if (fsMode == FsMode.FILEVAULT_XML) {
+            rootFile = new File(this.provider.getRootFile(), ROOT_DIR + PlatformNameFormat.getPlatformPath(this.provider.getProviderRoot()));
+        }
+        this.root = new Monitorable(this.provider.getProviderRoot(), rootFile, null);
+        
         createStatus(this.root, contentFileExtensions, contentFileCache);
         log.debug("Starting file monitor for {} with an interval of {}ms", this.root.file, interval);
         timer.schedule(this, 0, interval);
@@ -142,6 +152,13 @@ public final class FileMonitor extends TimerTask {
                 // new file and reset status
                 createStatus(monitorable, contentFileExtensions, contentFileCache);
                 sendEvents(monitorable, ChangeType.ADDED, reporter);
+                final FileStatus fs = (FileStatus)monitorable.status;
+                if ( fs instanceof DirStatus ) {
+                    final DirStatus ds = (DirStatus)fs;
+                    // remove monitorables for new folder and update folder children to send events for directory contents
+                    ds.children = new Monitorable[0];
+                    checkDirStatusChildren(monitorable, reporter);
+                }
             }
         } else {
             // check if the file has been removed
@@ -170,45 +187,50 @@ public final class FileMonitor extends TimerTask {
                     // 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(monitorable.path + '/' + files[i].getName(), files[i],
-                                            contentFileExtensions.getSuffix(files[i]));
-                                    children[i].status = NonExistingStatus.SINGLETON;
-                                    check(children[i], reporter);
-                                }
-                            }
-                            ds.children = children;
-                        } else {
-                            ds.children = new Monitorable[0];
-                        }
+                        checkDirStatusChildren(monitorable, reporter);
                     }
                 }
             }
         }
     }
+    
+    private void checkDirStatusChildren(final Monitorable dirMonitorable, final ObservationReporter reporter) {
+        final DirStatus ds = (DirStatus)dirMonitorable.status;
+        final File[] files = dirMonitorable.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(dirMonitorable.path + '/' + files[i].getName(), files[i],
+                            contentFileExtensions.getSuffix(files[i]));
+                    children[i].status = NonExistingStatus.SINGLETON;
+                    check(children[i], reporter);
+                }
+            }
+            ds.children = children;
+        } else {
+            ds.children = new Monitorable[0];
+        }
+    }
 
     /**
      * Send the event async via the event admin.
      */
     private void sendEvents(final Monitorable monitorable, final ChangeType changeType, final ObservationReporter reporter) {
         if (log.isDebugEnabled()) {
-            log.debug("Detected change for resource {} : {}", monitorable.path, changeType);
+            log.debug("Detected change for resource {} : {}", transformPath(monitorable.path), changeType);
         }
 
         List<ResourceChange> changes = null;
         for (final ObserverConfiguration config : reporter.getObserverConfigurations()) {
-            if (config.matches(monitorable.path)) {
+            if (config.matches(transformPath(monitorable.path))) {
                 if (changes == null) {
                     changes = collectResourceChanges(monitorable, changeType);
                 }
@@ -224,6 +246,20 @@ public final class FileMonitor extends TimerTask {
         }
     }
     
+    /**
+     * Transform path for resource event.
+     * @param path Path
+     * @return Transformed path
+     */
+    private String transformPath(String path) {
+        if (fsMode == FsMode.FILEVAULT_XML) {
+            return PlatformNameFormat.getRepositoryPath(path);
+        }
+        else {
+            return path;
+        }
+    }
+    
     @SuppressWarnings("unchecked")
     private List<ResourceChange> collectResourceChanges(final Monitorable monitorable, final ChangeType changeType) {
         List<ResourceChange> changes = new ArrayList<>();
@@ -233,15 +269,15 @@ public final class FileMonitor extends TimerTask {
                 Map<String,Object> content = (Map<String,Object>)contentFile.getContent();
                 // we cannot easily report the diff of resource changes between two content files
                 // so we simulate a removal of the toplevel node and then add all nodes contained in the current content file again.
-                changes.add(buildContentResourceChange(ChangeType.REMOVED,  monitorable.path));
-                addContentResourceChanges(changes, ChangeType.ADDED, content, monitorable.path);
+                changes.add(buildContentResourceChange(ChangeType.REMOVED,  transformPath(monitorable.path)));
+                addContentResourceChanges(changes, ChangeType.ADDED, content, transformPath(monitorable.path));
             }
             else {
-                addContentResourceChanges(changes, changeType, (Map<String,Object>)contentFile.getContent(), monitorable.path);
+                addContentResourceChanges(changes, changeType, (Map<String,Object>)contentFile.getContent(), transformPath(monitorable.path));
             }
         }
         else {
-            changes.add(buildContentResourceChange(changeType, monitorable.path));
+            changes.add(buildContentResourceChange(changeType, transformPath(monitorable.path)));
         }
         return changes;
     }
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/FsMode.java b/src/main/java/org/apache/sling/fsprovider/internal/FsMode.java
new file mode 100644
index 0000000..15af91f
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsMode.java
@@ -0,0 +1,41 @@
+/*
+ * 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.fsprovider.internal;
+
+/**
+ * Different modes for Filesystem provider support and filesystem layouts.
+ */
+public enum FsMode {
+    
+    /**
+     * Sling-Initial-Content filesystem layout, with full support for JSON and jcr.xml content files.
+     */
+    INITIAL_CONTENT,
+    
+    /**
+     * Sling-Initial-Content filesystem layout, support only files and folders (classic mode).
+     */
+    INITIAL_CONTENT_FILES_FOLDERS,
+
+    /**
+     * FileVault XML format (expanded content package).
+     */
+    FILEVAULT_XML
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
index d214987..2b55028 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
@@ -24,13 +24,16 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import static org.apache.jackrabbit.vault.util.Constants.DOT_CONTENT_XML;
 
 import org.apache.commons.collections.IteratorUtils;
 import org.apache.commons.collections.Predicate;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.fsprovider.internal.mapper.ContentFileResourceMapper;
 import org.apache.sling.fsprovider.internal.mapper.FileResourceMapper;
+import org.apache.sling.fsprovider.internal.mapper.FileVaultResourceMapper;
 import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 import org.apache.sling.fsprovider.internal.parser.ContentFileTypes;
 import org.apache.sling.spi.resource.provider.ObservationReporter;
@@ -47,6 +50,7 @@ import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.Designate;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.Option;
 
 /**
  * The <code>FsResourceProvider</code> is a resource provider which maps
@@ -106,17 +110,25 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
                 "filesystem resources are mapped in. This property must not be an empty string.")
         String provider_root();
         
-        @AttributeDefinition(name = "Mount json",
-                description = "Mount .json files as content in the resource hierarchy.")
-        boolean provider_json_content();
-       
-        @AttributeDefinition(name = "Mount jcr.xml",
-                description = "Mount .jcr.xml files as content in the resource hierarchy.")
-        boolean provider_jcrxml_content();
+        @AttributeDefinition(name = "Filesystem layout",
+                description = "Filesystem layout mode for files, folders and content.",
+                options={
+                        @Option(value="INITIAL_CONTENT", label="INITIAL_CONTENT - "
+                                + "Sling-Initial-Content filesystem layout, with full support for JSON and jcr.xml content files."),
+                        @Option(value="INITIAL_CONTENT_FILES_FOLDERS", label="INITIAL_CONTENT_FILES_FOLDERS - "
+                                + "Sling-Initial-Content filesystem layout, support only files and folders (classic mode)."),
+                        @Option(value="FILEVAULT_XML", label="FILEVAULT_XML - "
+                                + "FileVault XML format (expanded content package)."),
+                })
+        FsMode provider_fs_mode() default FsMode.INITIAL_CONTENT;
+        
+        @AttributeDefinition(name = "Init. Content Options",
+                description = "Import options for Sling-Initial-Content filesystem layout. Supported options: overwrite, ignoreImportProviders.")
+        String provider_initial_content_import_options();
         
         @AttributeDefinition(name = "Cache Size",
                 description = "Max. number of content files cached in memory.")
-        int provider_cache_size() default 1000;
+        int provider_cache_size() default 10000;
 
         /**
          * Internal Name hint for web console.
@@ -134,8 +146,10 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
     private FileMonitor monitor;
     
     // maps filesystem to resources
+    private FsMode fsMode;
     private FsResourceMapper fileMapper;
     private FsResourceMapper contentFileMapper;
+    private FileVaultResourceMapper fileVaultMapper;
     
     // if true resources from filesystem are only "overlayed" to JCR resources, serving JCR as fallback within the same path
     private boolean overlayParentResourceProvider;
@@ -160,12 +174,30 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
             final Resource parent) {
         
         ResourceResolver resolver = ctx.getResourceResolver();
-        Resource rsrc = contentFileMapper.getResource(resolver, path);
-        if (rsrc == null) {
-            rsrc = fileMapper.getResource(resolver, path);
+        
+        boolean askParentResourceProvider;
+        Resource rsrc = null;
+
+        if (fsMode == FsMode.FILEVAULT_XML) {
+            // filevault: check if path matches, if not fallback to parent resource provider
+            if (fileVaultMapper.pathMatches(path)) {
+                askParentResourceProvider = false;
+                rsrc = fileVaultMapper.getResource(resolver, path);
+            }
+            else {
+                askParentResourceProvider = true;
+            }
+        }
+        else {
+            // Sling-Initial-Content: mount folder/files an content files
+            askParentResourceProvider = this.overlayParentResourceProvider;
+            rsrc = contentFileMapper.getResource(resolver, path);
+            if (rsrc == null) {
+                rsrc = fileMapper.getResource(resolver, path);
+            }
         }
         
-        if (this.overlayParentResourceProvider) {
+        if (askParentResourceProvider) {
             // make sure directory resources from parent resource provider have higher precedence than from this provider
             // this allows properties like sling:resourceSuperType to take effect
             if ( rsrc == null || rsrc.getResourceMetadata().containsKey(RESOURCE_METADATA_FILE_DIRECTORY) ) {
@@ -181,7 +213,7 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
             	}        	
             }
         }
-
+        
         return rsrc;
     }
 
@@ -195,24 +227,48 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
         
         List<Iterator<Resource>> allChildren = new ArrayList<>();
         Iterator<Resource> children;
+        boolean askParentResourceProvider;
         
-        children = contentFileMapper.getChildren(resolver, parent);
-        if (children != null) {
-            allChildren.add(children);
+        if (fsMode == FsMode.FILEVAULT_XML) {
+            // filevault: always ask provider, it checks itself if children matches the filters
+            askParentResourceProvider = true;
+            children = fileVaultMapper.getChildren(resolver, parent);
+            if (children != null) {
+                allChildren.add(children);
+            }
         }
-        
-        children = fileMapper.getChildren(resolver, parent);
-        if (children != null) {
-            allChildren.add(children);
+        else {
+            // Sling-Initial-Content: get all matchind folders/files and content files
+            askParentResourceProvider = this.overlayParentResourceProvider;
+            children = contentFileMapper.getChildren(resolver, parent);
+            if (children != null) {
+                allChildren.add(children);
+            }
+            children = fileMapper.getChildren(resolver, parent);
+            if (children != null) {
+                allChildren.add(children);
+            }
         }
         
     	// get children from from shadowed provider
-        if (this.overlayParentResourceProvider) {
+        if (askParentResourceProvider) {
         	final ResourceProvider parentResourceProvider = ctx.getParentResourceProvider();
         	if (parentResourceProvider != null) {
         		children = parentResourceProvider.listChildren(ctx.getParentResolveContext(), parent);
                 if (children != null) {
-                    allChildren.add(children);
+                    if (fsMode == FsMode.FILEVAULT_XML) {
+                        // filevault: include all children from parent resource provider that do not match the filters
+                        allChildren.add(IteratorUtils.filteredIterator(children, new Predicate() {
+                            @Override
+                            public boolean evaluate(Object object) {
+                                Resource child = (Resource)object;
+                                return !fileVaultMapper.pathMatches(child.getPath());
+                            }
+                        }));
+                    }
+                    else {
+                        allChildren.add(children);
+                    }
                 }
         	}
         }
@@ -239,39 +295,54 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
     // ---------- SCR Integration
     @Activate
     protected void activate(BundleContext bundleContext, final Config config) {
+        fsMode = config.provider_fs_mode();
         String providerRoot = config.provider_root();
-        if (providerRoot == null || providerRoot.length() == 0) {
+        if (StringUtils.isBlank(providerRoot)) {
             throw new IllegalArgumentException("provider.root property must be set");
         }
 
         String providerFileName = config.provider_file();
-        if (providerFileName == null || providerFileName.length() == 0) {
+        if (StringUtils.isBlank(providerFileName)) {
             throw new IllegalArgumentException("provider.file property must be set");
         }
 
         this.providerRoot = providerRoot;
         this.providerFile = getProviderFile(providerFileName, bundleContext);
-        this.overlayParentResourceProvider = true;
+        this.overlayParentResourceProvider = false;
         
+        InitialContentImportOptions options = new InitialContentImportOptions(config.provider_initial_content_import_options());
+                
         List<String> contentFileSuffixes = new ArrayList<>();
-        if (config.provider_json_content()) {
-            contentFileSuffixes.add(ContentFileTypes.JSON_SUFFIX);
-            this.overlayParentResourceProvider = false;
+        if (fsMode == FsMode.FILEVAULT_XML) {
+            contentFileSuffixes.add("/" + DOT_CONTENT_XML);
+        }
+        else if (fsMode == FsMode.INITIAL_CONTENT_FILES_FOLDERS) {
+            overlayParentResourceProvider = true;
         }
-        if (config.provider_jcrxml_content()) {
-            contentFileSuffixes.add(ContentFileTypes.JCR_XML_SUFFIX);
-            this.overlayParentResourceProvider = false;
+        else if (fsMode == FsMode.INITIAL_CONTENT) {
+            overlayParentResourceProvider = !options.isOverwrite();
+            if (!options.getIgnoreImportProviders().contains("json")) {
+                contentFileSuffixes.add(ContentFileTypes.JSON_SUFFIX);
+            }
+            if (!options.getIgnoreImportProviders().contains("jcr.xml")) {
+                contentFileSuffixes.add(ContentFileTypes.JCR_XML_SUFFIX);
+            }
         }
         ContentFileExtensions contentFileExtensions = new ContentFileExtensions(contentFileSuffixes);
         
         this.contentFileCache = new ContentFileCache(config.provider_cache_size());
-        this.fileMapper = new FileResourceMapper(this.providerRoot, this.providerFile, contentFileExtensions);
-        this.contentFileMapper = new ContentFileResourceMapper(this.providerRoot, this.providerFile,
-                contentFileExtensions, this.contentFileCache);
+        if (fsMode == FsMode.FILEVAULT_XML) {
+            this.fileVaultMapper = new FileVaultResourceMapper(this.providerFile, this.contentFileCache);
+        }
+        else {
+            this.fileMapper = new FileResourceMapper(this.providerRoot, this.providerFile, contentFileExtensions);
+            this.contentFileMapper = new ContentFileResourceMapper(this.providerRoot, this.providerFile,
+                    contentFileExtensions, this.contentFileCache);
+        }
         
         // start background monitor if check interval is higher than 100
-        if ( config.provider_checkinterval() > 100 ) {
-            this.monitor = new FileMonitor(this, config.provider_checkinterval(),
+        if (config.provider_checkinterval() > 100) {
+            this.monitor = new FileMonitor(this, config.provider_checkinterval(), fsMode,
                     contentFileExtensions, this.contentFileCache);
         }
     }
@@ -287,10 +358,12 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
         this.overlayParentResourceProvider = false;
         this.fileMapper = null;
         this.contentFileMapper = null;
+        this.fileVaultMapper = null;
         if (this.contentFileCache != null) {
             this.contentFileCache.clear();
             this.contentFileCache = null;
         }
+        this.fsMode = null;
     }
 
     File getRootFile() {
@@ -332,7 +405,7 @@ public final class FsResourceProvider extends ResourceProvider<Object> {
 
     public ObservationReporter getObservationReporter() {
         final ProviderContext ctx = this.getProviderContext();
-        if ( ctx != null ) {
+        if (ctx != null) {
             return ctx.getObservationReporter();
         }
         return null;
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/InitialContentImportOptions.java b/src/main/java/org/apache/sling/fsprovider/internal/InitialContentImportOptions.java
new file mode 100644
index 0000000..5353d0f
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/InitialContentImportOptions.java
@@ -0,0 +1,76 @@
+/*
+ * 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.fsprovider.internal;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+
+class InitialContentImportOptions {
+
+    /**
+     * The overwrite directive specifying if content should be overwritten or
+     * just initially added.
+     */
+    private static final String OVERWRITE_DIRECTIVE = "overwrite";
+
+    /**
+     * The ignore content readers directive specifying whether the available ContentReaders
+     * should be used during content loading.
+     */
+    private static final String IGNORE_CONTENT_READERS_DIRECTIVE = "ignoreImportProviders";
+
+    
+    private final boolean overwrite;
+    private final Set<String> ignoreImportProviders;
+    
+    public InitialContentImportOptions(String optionsString) {
+        Map<String,String> options = parseOptions(optionsString);
+        overwrite = BooleanUtils.toBoolean(options.get(OVERWRITE_DIRECTIVE));
+        ignoreImportProviders = new HashSet<>(Arrays.asList(StringUtils.split(StringUtils.defaultString(options.get(IGNORE_CONTENT_READERS_DIRECTIVE)))));
+    }
+    
+    private static Map<String,String> parseOptions(String optionsString) {
+        Map<String,String> options = new HashMap<>();
+        String[] optionsList = StringUtils.split(optionsString, ";");
+        if (optionsList != null) {
+            for (String keyValueString : optionsList) {
+                String[] keyValue = StringUtils.splitByWholeSeparator(keyValueString, ":=");
+                if (keyValue.length == 2) {
+                    options.put(StringUtils.trim(keyValue[0]), StringUtils.trim(keyValue[1]));
+                }
+            }
+        }
+        return options;
+    }
+
+    public boolean isOverwrite() {
+        return overwrite;
+    }
+
+    public Set<String> getIgnoreImportProviders() {
+        return ignoreImportProviders;
+    }
+    
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
index d31a851..27e09d5 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
@@ -19,8 +19,11 @@
 package org.apache.sling.fsprovider.internal.mapper;
 
 import java.io.File;
+import java.util.Iterator;
 import java.util.Map;
 
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.collections.Predicate;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 
@@ -51,19 +54,6 @@ public final class ContentFile {
     }
 
     /**
-     * @param file File with content fragment
-     * @param path Root path of the content file
-     * @param subPath Relative path addressing content fragment inside file
-     * @param contentFileCache Content file cache
-     * @param content Content
-     */
-    public ContentFile(File file, String path, String subPath, ContentFileCache contentFileCache, Object content) {
-        this(file, path, subPath, contentFileCache);
-        this.contentInitialized = true;
-        this.content = content;
-    }
-
-    /**
      * @return File with content fragment
      */
     public File getFile() {
@@ -129,14 +119,47 @@ public final class ContentFile {
     }
     
     /**
+     * @return Child maps.
+     */
+    @SuppressWarnings("unchecked")
+    public Iterator<Map.Entry<String,Map<String,Object>>> getChildren() {
+        if (!isResource()) {
+            return IteratorUtils.emptyIterator();
+        }
+        return IteratorUtils.filteredIterator(((Map)getContent()).entrySet().iterator(), new Predicate() {
+            @Override
+            public boolean evaluate(Object object) {
+                Map.Entry<String,Object> entry = (Map.Entry<String,Object>)object;
+                return entry.getValue() instanceof Map;
+            }
+        });
+    }
+    
+    /**
      * Navigate to another sub path position in content file.
-     * @param newSubPath New sub path
+     * @param newSubPath New sub path related to root path of content file
      * @return Content file
      */
-    public ContentFile navigateTo(String newSubPath) {
+    public ContentFile navigateToAbsolute(String newSubPath) {
         return new ContentFile(file, path, newSubPath, contentFileCache);
     }
         
+    /**
+     * Navigate to another sub path position in content file.
+     * @param newSubPath New sub path relative to current sub path in content file
+     * @return Content file
+     */
+    public ContentFile navigateToRelative(String newSubPath) {
+        String absoluteSubPath;
+        if (newSubPath == null) {
+            absoluteSubPath = this.subPath;
+        }
+        else {
+            absoluteSubPath = (this.subPath != null ? this.subPath + "/" : "") + newSubPath;
+        }
+        return new ContentFile(file, path, absoluteSubPath, contentFileCache);
+    }
+        
     @SuppressWarnings("unchecked")
     private static Object getDeepContent(Object object, String subPath) {
         if (object == null) {
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
index e6edd04..09b6ffe 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
@@ -106,18 +106,10 @@ public final class ContentFileResourceMapper implements FsResourceMapper {
         // get child resources from content fragments in content file
         List<ContentFile> children = new ArrayList<>();
         if (parentContentFile.hasContent() && parentContentFile.isResource()) {
-            Map<String,Object> content = (Map<String,Object>)parentContentFile.getContent();
-            for (Map.Entry<String, Object> entry: content.entrySet()) {
-                if (entry.getValue() instanceof Map) {
-                    String subPath;
-                    if (parentContentFile.getSubPath() == null) {
-                        subPath = entry.getKey();
-                    }
-                    else {
-                        subPath = parentContentFile.getSubPath() + "/" + entry.getKey();
-                    }
-                    children.add(new ContentFile(parentContentFile.getFile(), parentContentFile.getPath(), subPath, contentFileCache, entry.getValue()));
-                }
+            Iterator<Map.Entry<String,Map<String,Object>>> childMaps = parentContentFile.getChildren();
+            while (childMaps.hasNext()) {
+                Map.Entry<String,Map<String,Object>> entry = childMaps.next();
+                children.add(parentContentFile.navigateToRelative(entry.getKey()));
             }
         }
         if (children.isEmpty()) {
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java
new file mode 100644
index 0000000..88e001e
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java
@@ -0,0 +1,201 @@
+/*
+ * 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.fsprovider.internal.mapper;
+
+import static org.apache.jackrabbit.vault.util.Constants.DOT_CONTENT_XML;
+import static org.apache.jackrabbit.vault.util.Constants.FILTER_XML;
+import static org.apache.jackrabbit.vault.util.Constants.META_DIR;
+import static org.apache.jackrabbit.vault.util.Constants.ROOT_DIR;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.collections.Transformer;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
+import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
+import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
+import org.apache.jackrabbit.vault.util.PlatformNameFormat;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.fsprovider.internal.FsResourceMapper;
+import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class FileVaultResourceMapper implements FsResourceMapper {
+    
+    private static final String DOT_CONTENT_XML_SUFFIX = "/" + DOT_CONTENT_XML;
+    private static final String DOT_DIR_SUFFIX = "/.dir";
+
+    private final File providerFile;
+    private final ContentFileCache contentFileCache;
+    private final WorkspaceFilter workspaceFilter;
+    
+    private static final Logger log = LoggerFactory.getLogger(FileVaultResourceMapper.class);
+    
+    public FileVaultResourceMapper(File providerFile, ContentFileCache contentFileCache) {
+        this.providerFile = providerFile;
+        this.contentFileCache = contentFileCache;
+        this.workspaceFilter = getWorkspaceFilter();
+    }
+    
+    @Override
+    public Resource getResource(final ResourceResolver resolver, final String resourcePath) {
+        
+        // direct file
+        File file = getFile(resourcePath);
+        if (file != null && file.isFile()) {
+            return new FileResource(resolver, resourcePath, file);
+        }
+        
+        // content file
+        ContentFile contentFile = getContentFile(resourcePath, null);
+        if (contentFile != null) {
+            return new ContentFileResource(resolver, contentFile);
+        }
+        
+        // fallback to directory resource if folder was found but nothing else
+        if (file != null && file.isDirectory()) {
+            return new FileResource(resolver, resourcePath, file);
+        }
+        
+        return null;
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public Iterator<Resource> getChildren(final ResourceResolver resolver, final Resource parent) {
+        String parentPath = parent.getPath();
+        
+        Set<String> childPaths = new LinkedHashSet<>();
+        
+        // get children from content resource of parent
+        ContentFile parentContentFile = getContentFile(parentPath, null);
+        if (parentContentFile != null) {
+            Iterator<Map.Entry<String,Map<String,Object>>> childMaps = parentContentFile.getChildren();
+            while (childMaps.hasNext()) {
+                Map.Entry<String,Map<String,Object>> entry = childMaps.next();
+                String childPath = parentPath + "/" + entry.getKey();
+                if (pathMatches(childPath)) {
+                    childPaths.add(childPath);
+                }
+            }
+        }
+        
+        // additional check for children in filesystem
+        File parentFile = getFile(parentPath);
+        if (parentFile != null && parentFile.isDirectory()) {
+            for (File childFile : parentFile.listFiles()) {
+                String childPath = parentPath + "/" + PlatformNameFormat.getRepositoryName(childFile.getName());
+                if (pathMatches(childPath) && !childPaths.contains(childPath)) {
+                    childPaths.add(childPath);
+                }
+            }
+        }
+        
+        if (childPaths.isEmpty()) {
+            return null;
+        }
+        else {
+            return IteratorUtils.transformedIterator(childPaths.iterator(), new Transformer() {
+                @Override
+                public Object transform(Object input) {
+                    String path = (String)input;
+                    return getResource(resolver, path);
+                }
+            });
+        }
+    }
+
+    /**
+     * @return Workspace filter or null if none found.
+     */
+    private WorkspaceFilter getWorkspaceFilter() {
+        File filter = new File(providerFile, META_DIR + "/" + FILTER_XML);
+        if (filter.exists()) {
+            try {
+                DefaultWorkspaceFilter workspaceFilter = new DefaultWorkspaceFilter();
+                workspaceFilter.load(filter);
+                return workspaceFilter;
+            } catch (IOException | ConfigurationException ex) {
+                log.error("Unable to parse workspace filter: " + filter.getPath(), ex);
+            }
+        }
+        else {
+            log.warn("Workspace filter not found: " + filter.getPath());
+        }
+        return null;
+    }
+    
+    /**
+     * Checks if the given path matches the workspace filter.
+     * @param path Path
+     * @return true if path matches
+     */
+    public boolean pathMatches(String path) {
+        // ignore .dir folder
+        if (StringUtils.endsWith(path, DOT_DIR_SUFFIX) || StringUtils.endsWith(path, DOT_CONTENT_XML_SUFFIX)) {
+            return false;
+        }
+        if (workspaceFilter == null) {
+            return false;
+        }
+        else {
+            return workspaceFilter.contains(path);
+        }
+    }
+    
+    private File getFile(String path) {
+        if (StringUtils.endsWith(path, DOT_CONTENT_XML_SUFFIX)) {
+            return null;
+        }
+        File file = new File(providerFile, ROOT_DIR + PlatformNameFormat.getPlatformPath(path));
+        if (file.exists()) {
+            return file;
+        }
+        return null;
+    }
+    
+    private ContentFile getContentFile(String path, String subPath) {
+        File file = new File(providerFile, ROOT_DIR + PlatformNameFormat.getPlatformPath(path) + DOT_CONTENT_XML_SUFFIX);
+        if (file.exists()) {
+            ContentFile contentFile = new ContentFile(file, path, subPath, contentFileCache);
+            if (contentFile.hasContent()) {
+                return contentFile;
+            }
+        }
+        
+        // try to find in parent path which contains content fragment
+        String parentPath = ResourceUtil.getParent(path);
+        if (parentPath == null) {
+            return null;
+        }
+        String nextSubPath = path.substring(parentPath.length() + 1)
+                + (subPath != null ? "/" + subPath : "");
+        return getContentFile(parentPath, nextSubPath);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
index 6f52691..23abe66 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
@@ -110,22 +110,22 @@ public final class FsNode extends FsItem implements Node {
             else {
                 subPath = path.substring(contentFile.getPath().length() + 1);
             }
-            ContentFile referencedFile = contentFile.navigateTo(subPath);
+            ContentFile referencedFile = contentFile.navigateToAbsolute(subPath);
             if (referencedFile.hasContent()) {
                 return new FsNode(referencedFile, resolver);
             }
         }
-        else {
-            // node is outside content file
-            Node refNode = null;
-            Resource resource = resolver.getResource(path);
-            if (resource != null) {
-                refNode = resource.adaptTo(Node.class);
-                if (refNode != null) {
-                    return refNode;
-                }
+        
+        // check if node is outside content file
+        Node refNode = null;
+        Resource resource = resolver.getResource(path);
+        if (resource != null) {
+            refNode = resource.adaptTo(Node.class);
+            if (refNode != null) {
+                return refNode;
             }
         }
+
         throw new PathNotFoundException(relPath);
     }
 
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeIterator.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeIterator.java
index f03b0a7..09db7d4 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeIterator.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeIterator.java
@@ -63,14 +63,7 @@ class FsNodeIterator implements NodeIterator {
     @Override
     public Node nextNode() {
         Map.Entry<String,Map<String,Object>> nextEntry = children.next();
-        String subPath;
-        if (contentFile.getSubPath() == null) {
-            subPath = nextEntry.getKey();
-        }
-        else {
-            subPath = contentFile.getSubPath() + "/" + nextEntry.getKey();
-        }
-        return new FsNode(contentFile.navigateTo(subPath), resolver);
+        return new FsNode(contentFile.navigateToRelative(nextEntry.getKey()), resolver);
     }
 
     
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserUtil.java b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserUtil.java
index 4b46157..6ac72c5 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserUtil.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserUtil.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.fsprovider.internal.parser;
 
+import static org.apache.jackrabbit.vault.util.Constants.DOT_CONTENT_XML;
 import static org.apache.sling.fsprovider.internal.parser.ContentFileTypes.JCR_XML_SUFFIX;
 import static org.apache.sling.fsprovider.internal.parser.ContentFileTypes.JSON_SUFFIX;
 
@@ -62,11 +63,14 @@ class ContentFileParserUtil {
      * @return Content or null if content could not be parsed.
      */
     public static Map<String,Object> parse(File file) {
+        if (!file.exists()) {
+            return null;
+        }
         try {
             if (StringUtils.endsWith(file.getName(), JSON_SUFFIX)) {
                 return JSON_PARSER.parse(file);
             }
-            else if (StringUtils.endsWith(file.getName(), JCR_XML_SUFFIX)) {
+            else if (StringUtils.equals(file.getName(), DOT_CONTENT_XML) || StringUtils.endsWith(file.getName(), JCR_XML_SUFFIX)) {
                 return JCR_XML_PARSER.parse(file);
             }
         }
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java b/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
index 020405d..bc4892f 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
@@ -18,19 +18,19 @@
  */
 package org.apache.sling.fsprovider.internal;
 
+import static org.apache.sling.fsprovider.internal.TestUtils.assertChange;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.nio.file.Files;
-import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.resource.observation.ResourceChange;
 import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
 import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.fsprovider.internal.TestUtils.ResourceListener;
 import org.apache.sling.testing.mock.sling.ResourceResolverType;
 import org.apache.sling.testing.mock.sling.junit.SlingContext;
 import org.apache.sling.testing.mock.sling.junit.SlingContextBuilder;
@@ -39,10 +39,13 @@ import org.junit.Rule;
 import org.junit.Test;
 
 /**
- * Test events when changing filesystem content.
+ * Test events when changing filesystem content (Sling-Initial-Content).
  */
 public class FileMonitorTest {
 
+    private static final int CHECK_INTERVAL = 120;
+    private static final int WAIT_INTERVAL = 250;
+    
     private final File tempDir;
     private final ResourceListener resourceListener = new ResourceListener();
     
@@ -64,8 +67,9 @@ public class FileMonitorTest {
                 context.registerInjectActivateService(new FsResourceProvider(),
                         "provider.file", tempDir.getPath(),
                         "provider.root", "/fs-test",
-                        "provider.checkinterval", 120,
-                        "provider.json.content", true);
+                        "provider.checkinterval", CHECK_INTERVAL,
+                        "provider.fs.mode", FsMode.INITIAL_CONTENT.name(),
+                        "provider.initial.content.import.options", "overwrite:=true;ignoreImportProviders:=jcr.xml");
                 
                 // register resource change listener
                 context.registerService(ResourceChangeListener.class, resourceListener,
@@ -89,7 +93,7 @@ public class FileMonitorTest {
         File file1a = new File(tempDir, "folder1/file1a.txt");
         FileUtils.touch(file1a);
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(1, changes.size());
         assertChange(changes, "/fs-test/folder1/file1a.txt", ChangeType.CHANGED);
@@ -103,7 +107,7 @@ public class FileMonitorTest {
         File file1c = new File(tempDir, "folder1/file1c.txt");
         FileUtils.write(file1c, "newcontent");
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(2, changes.size());
         assertChange(changes, "/fs-test/folder1", ChangeType.CHANGED);
@@ -118,7 +122,7 @@ public class FileMonitorTest {
         File file1a = new File(tempDir, "folder1/file1a.txt");
         file1a.delete();
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(2, changes.size());
         assertChange(changes, "/fs-test/folder1", ChangeType.CHANGED);
@@ -133,7 +137,7 @@ public class FileMonitorTest {
         File folder99 = new File(tempDir, "folder99");
         folder99.mkdir();
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(2, changes.size());
         assertChange(changes, "/fs-test", ChangeType.CHANGED);
@@ -148,7 +152,7 @@ public class FileMonitorTest {
         File folder1 = new File(tempDir, "folder1");
         FileUtils.deleteDirectory(folder1);
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(2, changes.size());
         assertChange(changes, "/fs-test", ChangeType.CHANGED);
@@ -156,30 +160,29 @@ public class FileMonitorTest {
     }
 
     @Test
-    public void testUpdateJsonContent() throws Exception {
+    public void testUpdateContent() throws Exception {
         List<ResourceChange> changes = resourceListener.getChanges();
         assertTrue(changes.isEmpty());
         
         File file1a = new File(tempDir, "folder2/content.json");
         FileUtils.touch(file1a);
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
-        assertTrue(changes.size() > 1);
         assertChange(changes, "/fs-test/folder2/content", ChangeType.REMOVED);
         assertChange(changes, "/fs-test/folder2/content", ChangeType.ADDED);
         assertChange(changes, "/fs-test/folder2/content/jcr:content", ChangeType.ADDED);
     }
     
     @Test
-    public void testAddJsonContent() throws Exception {
+    public void testAddContent() throws Exception {
         List<ResourceChange> changes = resourceListener.getChanges();
         assertTrue(changes.isEmpty());
         
         File file1c = new File(tempDir, "folder1/file1c.json");
         FileUtils.write(file1c, "{\"prop1\":\"value1\",\"child1\":{\"prop2\":\"value1\"}}");
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(3, changes.size());
         assertChange(changes, "/fs-test/folder1", ChangeType.CHANGED);
@@ -188,41 +191,18 @@ public class FileMonitorTest {
     }
     
     @Test
-    public void testRemoveJsonContent() throws Exception {
+    public void testRemoveContent() throws Exception {
         List<ResourceChange> changes = resourceListener.getChanges();
         assertTrue(changes.isEmpty());
         
         File file1a = new File(tempDir, "folder2/content.json");
         file1a.delete();
         
-        Thread.sleep(250);
+        Thread.sleep(WAIT_INTERVAL);
 
         assertEquals(2, changes.size());
         assertChange(changes, "/fs-test/folder2", ChangeType.CHANGED);
         assertChange(changes, "/fs-test/folder2/content", ChangeType.REMOVED);
     }
     
-    
-    private void assertChange(List<ResourceChange> changes, String path, ChangeType changeType) {
-        boolean found = false;
-        for (ResourceChange change : changes) {
-            if (StringUtils.equals(change.getPath(), path) && change.getType() == changeType) {
-                found = true;
-                break;
-            }
-        }
-        assertTrue("Change with path=" + path + ", changeType=" + changeType, found);
-    }
-    
-    static class ResourceListener implements ResourceChangeListener {
-        private final List<ResourceChange> allChanges = new ArrayList<>();
-        @Override
-        public void onChange(List<ResourceChange> changes) {
-            allChanges.addAll(changes);
-        }
-        public List<ResourceChange> getChanges() {
-            return allChanges;
-        }
-    }
-
 }
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/FileVaultContentTest.java b/src/test/java/org/apache/sling/fsprovider/internal/FileVaultContentTest.java
new file mode 100644
index 0000000..a6f6744
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FileVaultContentTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.fsprovider.internal;
+
+import static org.apache.sling.fsprovider.internal.TestUtils.assertFile;
+import static org.apache.sling.fsprovider.internal.TestUtils.assertFolder;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.fsprovider.internal.TestUtils.RegisterFsResourcePlugin;
+import org.apache.sling.hamcrest.ResourceMatchers;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.junit.SlingContextBuilder;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Test access FileFault XML files, folders and content.
+ */
+public class FileVaultContentTest {
+
+    private Resource damAsset;
+    private Resource sampleContent;
+
+    @Rule
+    public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
+        .plugin(new RegisterFsResourcePlugin(
+                "provider.fs.mode", FsMode.FILEVAULT_XML.name(),
+                "provider.file", "src/test/resources/vaultfs-test",
+                "provider.root", "/content/dam/talk.png"
+                ))
+        .plugin(new RegisterFsResourcePlugin(
+                "provider.fs.mode", FsMode.FILEVAULT_XML.name(),
+                "provider.file", "src/test/resources/vaultfs-test",
+                "provider.root", "/content/samples"
+                ))
+        .build();
+
+    @Before
+    public void setUp() {
+        damAsset = context.resourceResolver().getResource("/content/dam/talk.png");
+        sampleContent = context.resourceResolver().getResource("/content/samples");
+    }
+
+    @Test
+    public void testDamAsset() {
+        assertNotNull(damAsset);
+        assertEquals("app:Asset", damAsset.getResourceType());
+        
+        Resource content = damAsset.getChild("jcr:content");
+        assertNotNull(content);
+        assertEquals("app:AssetContent", content.getResourceType());
+        
+        Resource metadata = content.getChild("metadata");
+        assertNotNull(metadata);
+        ValueMap props = metadata.getValueMap();
+        assertEquals((Integer)4, props.get("app:Bitsperpixel", Integer.class));
+        
+        assertFolder(content, "renditions");
+        assertFile(content, "renditions/original", null);
+        assertFile(content, "renditions/web.1280.1280.png", null);
+    }
+
+    @Test
+    public void testSampleContent() {
+        assertNotNull(sampleContent);
+        assertEquals("sling:OrderedFolder", sampleContent.getResourceType());
+
+        Resource enContent = sampleContent.getChild("en/jcr:content");
+        assertArrayEquals(new String[] { "/etc/mobile/groups/responsive" }, enContent.getValueMap().get("app:deviceGroups", String[].class));
+    }
+
+    @Test
+    public void testListChildren() {
+        Resource en = sampleContent.getChild("en");
+        List<Resource> children = ImmutableList.copyOf(en.listChildren());
+        assertEquals(2, children.size());
+        
+        Resource child1 = children.get(0);
+        assertEquals("jcr:content", child1.getName());
+        assertEquals("samples/sample-app/components/content/page/homepage", child1.getResourceType());
+ 
+        Resource child2 = children.get(1);
+        assertEquals("tools", child2.getName());
+        assertEquals("app:Page", child2.getResourceType());
+        
+        // child3 (conference) is hidden because of filter
+    }
+
+    @Test
+    public void testJcrMixedContent() throws RepositoryException {
+        // prepare mixed JCR content
+        Node root = context.resourceResolver().adaptTo(Session.class).getNode("/");
+        Node content = root.addNode("content", "nt:folder");
+        Node samples = content.addNode("samples", "nt:folder");
+        Node en = samples.addNode("en", "nt:folder");
+        Node conference = en.addNode("conference", "nt:folder");
+        conference.addNode("page2", "nt:folder");
+        samples.addNode("it", "nt:folder");
+        
+        // pass-through because of filter
+        assertNotNull(context.resourceResolver().getResource("/content/samples/en/conference"));
+        assertNotNull(sampleContent.getChild("en/conference"));
+        assertNotNull(context.resourceResolver().getResource("/content/samples/en/conference/page2"));
+        assertNotNull(sampleContent.getChild("en/conference/page2"));
+        
+        // hidden because overlayed by resource provider
+        assertNull(context.resourceResolver().getResource("/content/samples/it"));
+        assertNull(sampleContent.getChild("it"));
+
+        // list children with mixed content
+        Resource enResource = sampleContent.getChild("en");
+        assertThat(enResource, ResourceMatchers.containsChildren("jcr:content", "tools", "conference"));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/FileVaultFileMonitorTest.java b/src/test/java/org/apache/sling/fsprovider/internal/FileVaultFileMonitorTest.java
new file mode 100644
index 0000000..8fe5f3b
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FileVaultFileMonitorTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.fsprovider.internal;
+
+import static org.apache.sling.fsprovider.internal.TestUtils.assertChange;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.fsprovider.internal.TestUtils.ResourceListener;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.junit.SlingContextBuilder;
+import org.apache.sling.testing.mock.sling.junit.SlingContextCallback;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Test events when changing filesystem content (FileVault XML).
+ */
+public class FileVaultFileMonitorTest {
+    
+    private static final int CHECK_INTERVAL = 120;
+    private static final int WAIT_INTERVAL = 250;
+
+    private final File tempDir;
+    private final ResourceListener resourceListener = new ResourceListener();
+    
+    public FileVaultFileMonitorTest() throws Exception {
+        tempDir = Files.createTempDirectory(getClass().getName()).toFile();
+    }
+
+    @Rule
+    public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
+        .beforeSetUp(new SlingContextCallback() {
+            @Override
+            public void execute(SlingContext context) throws Exception {
+                // copy test content to temp. directory
+                tempDir.mkdirs();
+                File sourceDir = new File("src/test/resources/vaultfs-test");
+                FileUtils.copyDirectory(sourceDir, tempDir);
+                
+                // mount temp. directory
+                context.registerInjectActivateService(new FsResourceProvider(),
+                        "provider.file", tempDir.getPath(),
+                        "provider.root", "/content/dam/talk.png",
+                        "provider.checkinterval", CHECK_INTERVAL,
+                        "provider.fs.mode", FsMode.FILEVAULT_XML.name());
+                context.registerInjectActivateService(new FsResourceProvider(),
+                        "provider.file", tempDir.getPath(),
+                        "provider.root", "/content/samples",
+                        "provider.checkinterval", CHECK_INTERVAL,
+                        "provider.fs.mode", FsMode.FILEVAULT_XML.name());
+                
+                // register resource change listener
+                context.registerService(ResourceChangeListener.class, resourceListener,
+                        ResourceChangeListener.PATHS, new String[] { "/content/dam/talk.png", "/content/samples" });
+            }
+        })
+        .afterTearDown(new SlingContextCallback() {
+            @Override
+            public void execute(SlingContext context) throws Exception {
+                // remove temp directory
+                tempDir.delete();
+            }
+        })
+        .build();
+
+    @Test
+    public void testUpdateFile() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/dam/talk.png/_jcr_content/renditions/web.1280.1280.png");
+        FileUtils.touch(file);
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(1, changes.size());
+        assertChange(changes, "/content/dam/talk.png/jcr:content/renditions/web.1280.1280.png", ChangeType.CHANGED);
+    }
+    
+    @Test
+    public void testAddFile() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/dam/talk.png/_jcr_content/renditions/text.txt");
+        FileUtils.write(file, "newcontent");
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, "/content/dam/talk.png/jcr:content/renditions", ChangeType.CHANGED);
+        assertChange(changes, "/content/dam/talk.png/jcr:content/renditions/text.txt", ChangeType.ADDED);
+    }
+    
+    @Test
+    public void testRemoveFile() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/dam/talk.png/_jcr_content/renditions/web.1280.1280.png");
+        file.delete();
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, "/content/dam/talk.png/jcr:content/renditions", ChangeType.CHANGED);
+        assertChange(changes, "/content/dam/talk.png/jcr:content/renditions/web.1280.1280.png", ChangeType.REMOVED);
+    }
+    
+    @Test
+    public void testAddFolder() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File folder = new File(tempDir, "jcr_root/content/dam/talk.png/_jcr_content/newfolder");
+        folder.mkdir();
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, "/content/dam/talk.png/jcr:content", ChangeType.CHANGED);
+        assertChange(changes, "/content/dam/talk.png/jcr:content/newfolder", ChangeType.ADDED);
+    }
+    
+    @Test
+    public void testRemoveFolder() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File folder = new File(tempDir, "jcr_root/content/dam/talk.png/_jcr_content/renditions");
+        FileUtils.deleteDirectory(folder);
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, "/content/dam/talk.png/jcr:content", ChangeType.CHANGED);
+        assertChange(changes, "/content/dam/talk.png/jcr:content/renditions", ChangeType.REMOVED);
+    }
+
+    @Test
+    public void testUpdateContent() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/samples/en/.content.xml");
+        FileUtils.touch(file);
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertChange(changes, "/content/samples/en", ChangeType.REMOVED);
+        assertChange(changes, "/content/samples/en", ChangeType.ADDED);
+        assertChange(changes, "/content/samples/en/jcr:content", ChangeType.ADDED);
+    }
+    
+    @Test
+    public void testAddContent() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/samples/fr/.content.xml");
+        file.getParentFile().mkdir();
+        FileUtils.write(file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<jcr:root xmlns:jcr=\"http://www.jcp.org/jcr/1.0\" xmlns:app=\"http://sample.com/jcr/app/1.0\" "
+                + "xmlns:sling=\"http://sling.apache.org/jcr/sling/1.0\" jcr:primaryType=\"app:Page\">\n"
+                + "<jcr:content jcr:primaryType=\"app:PageContent\"/>\n"
+                + "</jcr:root>");
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertChange(changes, "/content/samples", ChangeType.CHANGED);
+        assertChange(changes, "/content/samples/fr", ChangeType.ADDED);
+        assertChange(changes, "/content/samples/fr/jcr:content", ChangeType.ADDED);
+    }
+    
+    @Test
+    public void testRemoveContent() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/samples/en");
+        FileUtils.deleteDirectory(file);
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, "/content/samples", ChangeType.CHANGED);
+        assertChange(changes, "/content/samples/en", ChangeType.REMOVED);
+    }
+    
+    @Test
+    public void testRemoveContentDotXmlOnly() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file = new File(tempDir, "jcr_root/content/samples/en/.content.xml");
+        file.delete();
+        
+        Thread.sleep(WAIT_INTERVAL);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, "/content/samples/en", ChangeType.CHANGED);
+        // this second event is not fully correct, but this is a quite special case, accept it for now 
+        assertChange(changes, "/content/samples/en", ChangeType.REMOVED);
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/InitialContentImportOptionsTest.java b/src/test/java/org/apache/sling/fsprovider/internal/InitialContentImportOptionsTest.java
new file mode 100644
index 0000000..513c51f
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/InitialContentImportOptionsTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.fsprovider.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+public class InitialContentImportOptionsTest {
+
+    @Test
+    public void testNull() {
+        InitialContentImportOptions underTest = new InitialContentImportOptions(null);
+        assertFalse(underTest.isOverwrite());
+        assertTrue(underTest.getIgnoreImportProviders().isEmpty());
+    }
+
+    @Test
+    public void testBlank() {
+        InitialContentImportOptions underTest = new InitialContentImportOptions(" ");
+        assertFalse(underTest.isOverwrite());
+        assertTrue(underTest.getIgnoreImportProviders().isEmpty());
+    }
+
+    @Test
+    public void testOptions1() {
+        InitialContentImportOptions underTest = new InitialContentImportOptions("overwrite:=true;ignoreImportProviders:=xml,json");
+        assertTrue(underTest.isOverwrite());
+        assertEquals(ImmutableSet.of("xml,json"), underTest.getIgnoreImportProviders());
+    }
+
+    @Test
+    public void testOptions2() {
+        InitialContentImportOptions underTest = new InitialContentImportOptions(" overwrite := false ; ignoreImportProviders := xml ");
+        assertFalse(underTest.isOverwrite());
+        assertEquals(ImmutableSet.of("xml"), underTest.getIgnoreImportProviders());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/JcrXmlContentTest.java b/src/test/java/org/apache/sling/fsprovider/internal/JcrXmlContentTest.java
index ded7f29..c1dc1c2 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/JcrXmlContentTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/JcrXmlContentTest.java
@@ -55,7 +55,10 @@ public class JcrXmlContentTest {
 
     @Rule
     public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
-        .plugin(new RegisterFsResourcePlugin("provider.jcrxml.content", true))
+            .plugin(new RegisterFsResourcePlugin(
+                    "provider.fs.mode", FsMode.INITIAL_CONTENT.name(),
+                    "provider.initial.content.import.options", "overwrite:=true;ignoreImportProviders:=json"
+                    ))
         .build();
 
     @Before
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
index 031812f..74c7d81 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
@@ -56,7 +56,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 /**
- * Test access to files and folders from filesystem.
+ * Test access to files and folders and JSON content from filesystem.
  */
 public class JsonContentTest {
 
@@ -65,7 +65,10 @@ public class JsonContentTest {
 
     @Rule
     public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
-        .plugin(new RegisterFsResourcePlugin("provider.json.content", true))
+        .plugin(new RegisterFsResourcePlugin(
+                "provider.fs.mode", FsMode.INITIAL_CONTENT.name(),
+                "provider.initial.content.import.options", "overwrite:=true;ignoreImportProviders:=jcr.xml"
+                ))
         .build();
 
     @Before
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/TestUtils.java b/src/test/java/org/apache/sling/fsprovider/internal/TestUtils.java
index 9ee6162..bbaf34b 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/TestUtils.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/TestUtils.java
@@ -28,13 +28,18 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.CharEncoding;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
 import org.apache.sling.hamcrest.ResourceMatchers;
 import org.apache.sling.testing.mock.osgi.MapUtil;
 import org.apache.sling.testing.mock.osgi.context.AbstractContextPlugin;
@@ -53,6 +58,7 @@ class TestUtils {
             config.put("provider.file", "src/test/resources/fs-test");
             config.put("provider.root", "/fs-test");
             config.put("provider.checkinterval", 0);
+            config.put("provider.fs.mode", FsMode.INITIAL_CONTENT_FILES_FOLDERS.name());
             config.putAll(props);
             context.registerInjectActivateService(new FsResourceProvider(), config);
         }
@@ -94,4 +100,26 @@ class TestUtils {
         }
     }    
 
+    public static void assertChange(List<ResourceChange> changes, String path, ChangeType changeType) {
+        boolean found = false;
+        for (ResourceChange change : changes) {
+            if (StringUtils.equals(change.getPath(), path) && change.getType() == changeType) {
+                found = true;
+                break;
+            }
+        }
+        assertTrue("Change with path=" + path + ", changeType=" + changeType + " expected", found);
+    }
+    
+    public static class ResourceListener implements ResourceChangeListener {
+        private final List<ResourceChange> allChanges = new ArrayList<>();
+        @Override
+        public void onChange(List<ResourceChange> changes) {
+            allChanges.addAll(changes);
+        }
+        public List<ResourceChange> getChanges() {
+            return allChanges;
+        }
+    }
+
 }
diff --git a/src/test/resources/vaultfs-test/META-INF/vault/filter.xml b/src/test/resources/vaultfs-test/META-INF/vault/filter.xml
index 20be2d8..a56ee24 100644
--- a/src/test/resources/vaultfs-test/META-INF/vault/filter.xml
+++ b/src/test/resources/vaultfs-test/META-INF/vault/filter.xml
@@ -19,5 +19,7 @@
 -->
 <workspaceFilter version="1.0">
     <filter root="/content/dam/talk.png" />
-    <filter root="/content/samples" />
+    <filter root="/content/samples">
+        <exclude pattern="/content/samples/en/conference(/.*)?"/>
+    </filter>
 </workspaceFilter>
diff --git a/src/test/resources/vaultfs-test/jcr_root/content/dam/talk.png/.content.xml b/src/test/resources/vaultfs-test/jcr_root/content/dam/talk.png/.content.xml
index 4f8312a..e207a1e 100644
--- a/src/test/resources/vaultfs-test/jcr_root/content/dam/talk.png/.content.xml
+++ b/src/test/resources/vaultfs-test/jcr_root/content/dam/talk.png/.content.xml
@@ -39,8 +39,6 @@
         dc:format="image/png"
         dc:modified="{Date}2014-09-19T21:20:26.812+02:00"
         jcr:primaryType="nt:unstructured"
-        tiff:ImageLength="{Long}270"
-        tiff:ImageWidth="{Long}480"
         writebackEnable="{Boolean}true" />
   </jcr:content>
 </jcr:root>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.