sling-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From romb...@apache.org
Subject [sling-org-apache-sling-fsresource] 03/29: SLING-6440 Filesystem Resource Provider: Support "mounting" content resources from JSON files
Date Tue, 07 Nov 2017 09:38:36 GMT
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 1309a7011211495891482746f57a4de78237e79e
Author: Stefan Seifert <sseifert@apache.org>
AuthorDate: Tue Feb 21 13:54:37 2017 +0000

    SLING-6440 Filesystem Resource Provider: Support "mounting" content resources from JSON files
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/fsresource@1783888 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  41 +-
 .../fsprovider/internal/ContentFileExtensions.java |  68 +++
 .../sling/fsprovider/internal/FileMonitor.java     |  43 +-
 .../sling/fsprovider/internal/FsResource.java      |  74 ++--
 .../fsprovider/internal/FsResourceMapper.java      |  47 ++
 .../fsprovider/internal/FsResourceProvider.java    | 254 +++++------
 .../fsprovider/internal/mapper/ContentFile.java    | 143 ++++++
 .../internal/mapper/ContentFileResource.java       | 125 ++++++
 .../internal/mapper/ContentFileResourceMapper.java | 146 +++++++
 .../internal/mapper/FileResourceMapper.java        | 145 +++++++
 .../fsprovider/internal/mapper/ValueMapUtil.java   |  57 +++
 .../fsprovider/internal/mapper/jcr/FsItem.java     | 165 +++++++
 .../fsprovider/internal/mapper/jcr/FsNode.java     | 481 +++++++++++++++++++++
 .../internal/mapper/jcr/FsNodeIterator.java        |  74 ++++
 .../fsprovider/internal/mapper/jcr/FsProperty.java | 241 +++++++++++
 .../internal/mapper/jcr/FsPropertyIterator.java    |  79 ++++
 .../fsprovider/internal/mapper/jcr/FsValue.java    | 162 +++++++
 .../internal/parser/ContentFileParser.java         |  52 +++
 .../fsprovider/internal/parser/JsonFileParser.java | 126 ++++++
 .../sling/fsprovider/internal/FileMonitorTest.java |  49 ++-
 .../sling/fsprovider/internal/FilesFolderTest.java |  11 +-
 ...sFolderTest.java => InvalidRootFolderTest.java} |  29 +-
 .../sling/fsprovider/internal/JcrMixedTest.java    |   6 +-
 .../sling/fsprovider/internal/JsonContentTest.java | 234 ++++++++++
 .../sling/fsprovider/internal/TestUtils.java       |  42 +-
 .../internal/mapper/ContentFileTest.java           | 112 +++++
 .../internal/mapper/ValueMapUtilTest.java          |  55 +++
 .../internal/parser/ContentFileParserTest.java     |  49 +++
 src/test/resources/fs-test/folder2/content.json    | 262 +++++++++++
 .../fs-test/folder2/content/content2.json          |   4 +
 src/test/resources/fs-test/folder2/file2a.txt      |   1 -
 .../resources/fs-test/folder2/folder21/file21a.txt |   1 +
 32 files changed, 3115 insertions(+), 263 deletions(-)

diff --git a/pom.xml b/pom.xml
index 1353c6c..fa7233a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,12 @@
                 <configuration>
                     <!-- Export SCR metadata to classpath to have them available in unit tests -->
                     <exportScr>true</exportScr>
+                    <instructions>
+                        <Embed-Dependency>
+                            johnzon-core;scope=compile;inline=false,
+                            geronimo-json_1.0_spec;scope=compile;inline=false
+                        </Embed-Dependency>
+                    </instructions>
                 </configuration>
             </plugin>
             <plugin>
@@ -113,8 +119,34 @@
             <artifactId>slf4j-api</artifactId>
         </dependency>
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.3.2</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>3.2.1</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.johnzon</groupId>
+          <artifactId>johnzon-core</artifactId>
+          <version>1.0.0</version>
+          <scope>compile</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.geronimo.specs</groupId>
+          <artifactId>geronimo-json_1.0_spec</artifactId>
+          <version>1.0-alpha-1</version>
+          <scope>compile</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
@@ -123,6 +155,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.testing.sling-mock</artifactId>
             <version>2.2.4</version>
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java b/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java
new file mode 100644
index 0000000..3711ac9
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/ContentFileExtensions.java
@@ -0,0 +1,68 @@
+/*
+ * 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.io.File;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Matches file names for content file extensions.
+ */
+public final class ContentFileExtensions {
+    
+    private final List<String> contentFileSuffixes;
+
+    public ContentFileExtensions(List<String> contentFileSuffixes) {
+        this.contentFileSuffixes = contentFileSuffixes;
+    }
+    
+    /**
+     * Get suffix from file name.
+     * @param file File
+     * @return Content file name suffix or null if not a context file.
+     */
+    public String getSuffix(File file) {
+        for (String suffix : contentFileSuffixes) {
+            if (StringUtils.endsWith(file.getName(), suffix)) {
+                return suffix;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Checks suffix from file name.
+     * @param file File
+     * @return true if content file
+     */
+    public boolean matchesSuffix(File file) {
+        return getSuffix(file) != null;
+    }
+    
+    /**
+     * @return Content file suffixes.
+     */
+    public Collection<String> getSuffixes() {
+        return contentFileSuffixes;
+    }
+    
+}
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 c964359..7249912 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.Timer;
 import java.util.TimerTask;
 
+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.spi.resource.provider.ObservationReporter;
@@ -34,7 +35,7 @@ import org.slf4j.LoggerFactory;
  * This class is a monitor for the file system
  * that periodically checks for changes.
  */
-public class FileMonitor extends TimerTask {
+public final class FileMonitor extends TimerTask {
 
     /** The logger. */
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
@@ -46,16 +47,19 @@ public class FileMonitor extends TimerTask {
     private final Monitorable root;
 
     private final FsResourceProvider provider;
+    
+    private final ContentFileExtensions contentFileExtensions;
 
     /**
      * Creates a new instance of this class.
      * @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, final ContentFileExtensions contentFileExtensions) {
         this.provider = provider;
-        this.root = new Monitorable(this.provider.getProviderRoot(), this.provider.getRootFile());
-        createStatus(this.root);
+        this.contentFileExtensions = contentFileExtensions;
+        this.root = new Monitorable(this.provider.getProviderRoot(), this.provider.getRootFile(), null);
+        createStatus(this.root, contentFileExtensions);
         logger.debug("Starting file monitor for {} with an interval of {}ms", this.root.file, interval);
         timer.schedule(this, 0, interval);
     }
@@ -130,7 +134,7 @@ public class FileMonitor extends TimerTask {
         if ( monitorable.status instanceof NonExistingStatus ) {
             if ( monitorable.file.exists() ) {
                 // new file and reset status
-                createStatus(monitorable);
+                createStatus(monitorable, contentFileExtensions);
                 sendEvents(monitorable,
                            ChangeType.ADDED,
                            reporter);
@@ -176,9 +180,8 @@ public class FileMonitor extends TimerTask {
                                     }
                                 }
                                 if (children[i] == null) {
-                                    children[i] = new Monitorable(
-                                        monitorable.path + '/'
-                                            + files[i].getName(), files[i]);
+                                    children[i] = new Monitorable(monitorable.path + '/' + files[i].getName(), files[i],
+                                            contentFileExtensions.getSuffix(files[i]));
                                     children[i].status = NonExistingStatus.SINGLETON;
                                     check(children[i], reporter);
                                 }
@@ -212,25 +215,29 @@ public class FileMonitor extends TimerTask {
     /**
      * Create a status object for the monitorable
      */
-    private static void createStatus(final Monitorable monitorable) {
+    private static void createStatus(final Monitorable monitorable, ContentFileExtensions contentFileExtensions) {
         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, monitorable.path);
+            monitorable.status = new DirStatus(monitorable.file, monitorable.path, contentFileExtensions);
         }
     }
 
     /** The monitorable to hold the resource path, the file and the status. */
     private static final class Monitorable {
         public final String path;
-        public final File   file;
+        public final File file;
         public Object status;
-
-        public Monitorable(final String path, final File file) {
-            this.path = path;
+        public Monitorable(final String path, final File file, String contentFileSuffix) {
             this.file = file;
+            if (contentFileSuffix != null) {
+                this.path = StringUtils.substringBeforeLast(path, contentFileSuffix);
+            }
+            else {
+                this.path = path;
+            }
         }
     }
 
@@ -246,15 +253,15 @@ public class FileMonitor extends TimerTask {
     private static final class DirStatus extends FileStatus {
         public Monitorable[] children;
 
-        public DirStatus(final File dir, final String path) {
+        public DirStatus(final File dir, final String path, final ContentFileExtensions contentFileExtensions) {
             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(path + '/'
-                        + files[i].getName(), files[i]);
-                    FileMonitor.createStatus(this.children[i]);
+                    this.children[i] = new Monitorable(path + '/' + files[i].getName(), files[i],
+                            contentFileExtensions.getSuffix(files[i]));
+                    FileMonitor.createStatus(this.children[i], contentFileExtensions);
                 }
             } else {
                 this.children = new Monitorable[0];
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/FsResource.java b/src/main/java/org/apache/sling/fsprovider/internal/FsResource.java
index 502d0ca..7472dc6 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FsResource.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsResource.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.sling.fsprovider.internal;
+package org.apache.sling.fsprovider.internal.mapper;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -28,6 +28,8 @@ import java.util.Calendar;
 import java.util.HashMap;
 import java.util.Map;
 
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
 import org.apache.sling.adapter.annotations.Adaptable;
 import org.apache.sling.adapter.annotations.Adapter;
 import org.apache.sling.api.resource.AbstractResource;
@@ -36,6 +38,7 @@ import org.apache.sling.api.resource.ResourceMetadata;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.fsprovider.internal.FsResourceProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -47,7 +50,7 @@ import org.slf4j.LoggerFactory;
     @Adapter({File.class, URL.class}),
     @Adapter(condition="If the resource is an FsResource and is a readable file.", value=InputStream.class)
 })
-public class FsResource extends AbstractResource {
+public final class FileResource extends AbstractResource {
 
     /**
      * The resource type for file system files mapped into the resource tree by
@@ -61,9 +64,6 @@ public class FsResource extends AbstractResource {
      */
     static final String RESOURCE_TYPE_FOLDER = "nt:folder";
 
-    // default log, assigned on demand
-    private Logger log;
-
     // the owning resource resolver
     private final ResourceResolver resolver;
 
@@ -79,6 +79,8 @@ public class FsResource extends AbstractResource {
     // the resource metadata, assigned on demand
     private ResourceMetadata metaData;
 
+    private static final Logger log = LoggerFactory.getLogger(FileResource.class);
+    
     /**
      * Creates an instance of this Filesystem resource.
      *
@@ -86,7 +88,7 @@ public class FsResource extends AbstractResource {
      * @param resourcePath The resource path in the resource tree
      * @param file The wrapped file
      */
-    FsResource(ResourceResolver resolver, String resourcePath, File file) {
+    FileResource(ResourceResolver resolver, String resourcePath, File file) {
         this.resolver = resolver;
         this.resourcePath = resourcePath;
         this.file = file;
@@ -139,11 +141,8 @@ public class FsResource extends AbstractResource {
      */
     public String getResourceType() {
         if (resourceType == null) {
-            resourceType = file.isFile()
-                    ? RESOURCE_TYPE_FILE
-                            : RESOURCE_TYPE_FOLDER;
+            resourceType = file.isFile() ? RESOURCE_TYPE_FILE : RESOURCE_TYPE_FOLDER;
         }
-
         return resourceType;
     }
 
@@ -156,39 +155,31 @@ public class FsResource extends AbstractResource {
     @SuppressWarnings("unchecked")
     public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
         if (type == File.class) {
-
             return (AdapterType) file;
-
-        } else if (type == InputStream.class) {
-
+        }
+        else if (type == InputStream.class) {
             if (!file.isDirectory() && file.canRead()) {
-
                 try {
                     return (AdapterType) new FileInputStream(file);
-                } catch (IOException ioe) {
-                    getLog().info(
-                            "adaptTo: Cannot open a stream on the file " + file,
-                            ioe);
                 }
-
-            } else {
-
-                getLog().debug("adaptTo: File {} is not a readable file", file);
-
+                catch (IOException ioe) {
+                    log.info("adaptTo: Cannot open a stream on the file " + file, ioe);
+                }
             }
-
-        } else if (type == URL.class) {
-
+            else {
+                log.debug("adaptTo: File {} is not a readable file", file);
+            }
+        }
+        else if (type == URL.class) {
             try {
                 return (AdapterType) file.toURI().toURL();
-            } catch (MalformedURLException mue) {
-                getLog().info(
-                        "adaptTo: Cannot convert the file path " + file
-                        + " to an URL", mue);
+            }
+            catch (MalformedURLException mue) {
+                log.info("adaptTo: Cannot convert the file path " + file + " to an URL", mue);
             }
 
-        } else if (type == ValueMap.class) {
-
+        }
+        else if (type == ValueMap.class) {
             // this resource simulates nt:file/nt:folder behavior by returning it as resource type
             // we should simulate the corresponding JCR properties in a value map as well
             if (file.exists() && file.canRead()) {
@@ -200,18 +191,17 @@ public class FsResource extends AbstractResource {
                 props.put("jcr:created", lastModifed);
                 return (AdapterType) new ValueMapDecorator(props);
             }
-
         }
-
         return super.adaptTo(type);
     }
 
-    // ---------- internal
-
-    private Logger getLog() {
-        if (log == null) {
-            log = LoggerFactory.getLogger(getClass());
-        }
-        return log;
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+                .append("path", resourcePath)
+                .append("file", file.getPath())
+                .append("resourceType", getResourceType())
+                .build();
     }
+
 }
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/FsResourceMapper.java b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceMapper.java
new file mode 100644
index 0000000..4cb2c17
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.Iterator;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+
+/**
+ * Maps files to resources.
+ */
+public interface FsResourceMapper {
+
+    /**
+     * Get single resource.
+     * @param resolver Resource resolver
+     * @param resourcePath Resource path
+     * @return Resource or null if not exists
+     */
+    Resource getResource(ResourceResolver resolver, String resourcePath);
+    
+    /**
+     * Get children of resource.
+     * @param resolver Resource resolver.
+     * @param parent Parent resource.
+     * @return Child resources or null if no children exist
+     */
+    Iterator<Resource> getChildren(ResourceResolver resolver, Resource parent);
+    
+}
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 f2ed39a..f379bad 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
@@ -19,14 +19,19 @@
 package org.apache.sling.fsprovider.internal;
 
 import java.io.File;
-import java.util.Collections;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.NoSuchElementException;
+import java.util.List;
 import java.util.Set;
 
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.collections.Predicate;
 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.parser.ContentFileParser;
 import org.apache.sling.spi.resource.provider.ObservationReporter;
 import org.apache.sling.spi.resource.provider.ProviderContext;
 import org.apache.sling.spi.resource.provider.ResolveContext;
@@ -61,12 +66,12 @@ import org.osgi.service.metatype.annotations.ObjectClassDefinition;
                    Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
            })
 @Designate(ocd=FsResourceProvider.Config.class, factory=true)
-public class FsResourceProvider extends ResourceProvider<Object> {
+public final class FsResourceProvider extends ResourceProvider<Object> {
     
     /**
      * Resource metadata property set by {@link FsResource} if the underlying file reference is a directory.
      */
-    static final String RESOURCE_METADATA_FILE_DIRECTORY = ":org.apache.sling.fsprovider.file.directory";
+    public static final String RESOURCE_METADATA_FILE_DIRECTORY = ":org.apache.sling.fsprovider.file.directory";
     
     @ObjectClassDefinition(name = "Apache Sling Filesystem Resource Provider",
             description = "Configure an instance of the filesystem " +
@@ -100,6 +105,10 @@ public 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();
+        
         /**
          * Internal Name hint for web console.
          */
@@ -109,14 +118,18 @@ public class FsResourceProvider extends ResourceProvider<Object> {
     // The location in the resource tree where the resources are mapped
     private String providerRoot;
 
-    // providerRoot + "/" to be used for prefix matching of paths
-    private String providerRootPrefix;
-
     // The "root" file or folder in the file system
     private File providerFile;
 
-    /** The monitor to detect file changes. */
+    // The monitor to detect file changes.
     private FileMonitor monitor;
+    
+    // maps filesystem to resources
+    private FsResourceMapper fileMapper;
+    private FsResourceMapper contentFileMapper;
+    
+    // if true resources from filesystem are only "overlayed" to JCR resources, serving JCR as fallback within the same path
+    private boolean overlayParentResourceProvider;
 
     /**
      * Returns a resource wrapping a file system file or folder for the given
@@ -133,21 +146,30 @@ public class FsResourceProvider extends ResourceProvider<Object> {
             final String path,
             final ResourceContext resourceContext,
             final Resource parent) {
-        Resource rsrc = getResource(ctx.getResourceResolver(), path, getFile(path));
-        // 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) ) {
-        	// get resource from shadowed provider
-        	final ResourceProvider rp = ctx.getParentResourceProvider();
-        	if ( rp != null ) {
-        	    Resource resourceFromParentResourceProvider = rp.getResource((ResolveContext)ctx.getParentResolveContext(), 
-	            		path, 
-	            		resourceContext, parent);
-        	    if (resourceFromParentResourceProvider != null) {
-        	        rsrc = resourceFromParentResourceProvider;
-        	    }
-        	}        	
+        
+        ResourceResolver resolver = ctx.getResourceResolver();
+        Resource rsrc = contentFileMapper.getResource(resolver, path);
+        if (rsrc == null) {
+            rsrc = fileMapper.getResource(resolver, path);
+        }
+        
+        if (this.overlayParentResourceProvider) {
+            // 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) ) {
+            	// get resource from shadowed provider
+            	final ResourceProvider rp = ctx.getParentResourceProvider();
+            	if ( rp != null ) {
+            	    Resource resourceFromParentResourceProvider = rp.getResource((ResolveContext)ctx.getParentResolveContext(), 
+    	            		path, 
+    	            		resourceContext, parent);
+            	    if (resourceFromParentResourceProvider != null) {
+            	        rsrc = resourceFromParentResourceProvider;
+            	    }
+            	}        	
+            }
         }
+
         return rsrc;
     }
 
@@ -157,102 +179,49 @@ public class FsResourceProvider extends ResourceProvider<Object> {
     @SuppressWarnings("unchecked")
     @Override
     public Iterator<Resource> listChildren(final ResolveContext<Object> ctx, final Resource parent) {
-        File parentFile = parent.adaptTo(File.class);
-
-        // not a FsResource, try to create one from the resource
-        if (parentFile == null) {
-            // if the parent path is at or below the provider root, get
-            // the respective file
-            parentFile = getFile(parent.getPath());
-
-            // if the parent path is actually the parent of the provider
-            // root, return a single element iterator just containing the
-            // provider file, unless the provider file is a directory and
-            // a repository item with the same path actually exists
-            if (parentFile == null) {
-
-                String parentPath = parent.getPath().concat("/");
-                if (providerRoot.startsWith(parentPath)) {
-                    String relPath = providerRoot.substring(parentPath.length());
-                    if (relPath.indexOf('/') < 0) {
-                        Resource res = getResource(
-                                parent.getResourceResolver(), providerRoot,
-                                providerFile);
-                        if (res != null) {
-                            return Collections.singletonList(res).iterator();
-                        }
-                    }
-                }
-
-                // no children here
-                return null;
-            }
+        ResourceResolver resolver = ctx.getResourceResolver();
+        
+        List<Iterator<Resource>> allChildren = new ArrayList<>();
+        Iterator<Resource> children;
+        
+        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
-    	final ResourceProvider rp = ctx.getParentResourceProvider();
-    	final Iterator<Resource> parentChildrenIterator;
-    	if ( rp != null ) {
-    		parentChildrenIterator = rp.listChildren(ctx.getParentResolveContext(), parent);
-    	} else {
-    		parentChildrenIterator = null;
-    	}
-        final File[] children = parentFile.listFiles();
-
-        final ResourceResolver resolver = ctx.getResourceResolver();
-        final String parentPath = parent.getPath();
-        return new Iterator<Resource>() {
-
-            final Set<String> names = new HashSet<>();
-
-            int index = 0;
-
-            Resource next = seek();
-
-            @Override
-            public boolean hasNext() {
-                return next != null;
-            }
-
-            @Override
-            public Resource next() {
-                if (!hasNext()) {
-                    throw new NoSuchElementException();
+        if (this.overlayParentResourceProvider) {
+        	final ResourceProvider parentResourceProvider = ctx.getParentResourceProvider();
+        	if (parentResourceProvider != null) {
+        		children = parentResourceProvider.listChildren(ctx.getParentResolveContext(), parent);
+                if (children != null) {
+                    allChildren.add(children);
                 }
+        	}
+        }
 
-                Resource result = next;
-                next = seek();
-                return result;
-            }
-
-            @Override
-            public void remove() {
-                throw new UnsupportedOperationException("remove");
-            }
-
-            private Resource seek() {
-                while (children != null && index < children.length) {
-                    File file = children[index++];
-                    String path = parentPath + "/" + file.getName();
-                    Resource result = getResource(resolver, path, file);
-                    if (result != null) {
-                    	names.add(file.getName());
-                        return result;
-                    }
-                }
-                if ( parentChildrenIterator != null ) {
-                	while ( parentChildrenIterator.hasNext() ) {
-                		final Resource result = parentChildrenIterator.next();
-                		if ( !names.contains(result.getName()) ) {
-                			names.add(result.getName());
-                			return result;
-                		}
-                	}
+    	if (allChildren.isEmpty()) {
+    	    return null;
+    	}
+    	else if (allChildren.size() == 1) {
+    	    return allChildren.get(0);
+    	}
+    	else {
+    	    // merge all children from the different iterators, but filter out potential duplicates with same resource name
+    	    return IteratorUtils.filteredIterator(IteratorUtils.chainedIterator(allChildren), new Predicate() {
+    	        private Set<String> names = new HashSet<>();
+                @Override
+                public boolean evaluate(Object object) {
+                    Resource resource = (Resource)object;
+                    return names.add(resource.getName());
                 }
-                // nothing found any more
-                return null;
-            }
-        };
+            });
+    	}
     }
 
     // ---------- SCR Integration
@@ -269,11 +238,22 @@ public class FsResourceProvider extends ResourceProvider<Object> {
         }
 
         this.providerRoot = providerRoot;
-        this.providerRootPrefix = providerRoot.concat("/");
         this.providerFile = getProviderFile(providerFileName, bundleContext);
+        this.overlayParentResourceProvider = true;
+        
+        List<String> contentFileSuffixes = new ArrayList<>();
+        if (config.provider_json_content()) {
+            contentFileSuffixes.add(ContentFileParser.JSON_SUFFIX);
+            this.overlayParentResourceProvider = false;
+        }
+        ContentFileExtensions contentFileExtensions = new ContentFileExtensions(contentFileSuffixes);
+        
+        this.fileMapper = new FileResourceMapper(this.providerRoot, this.providerFile, contentFileExtensions);
+        this.contentFileMapper = new ContentFileResourceMapper(this.providerRoot, this.providerFile, contentFileExtensions);
+        
         // start background monitor if check interval is higher than 100
         if ( config.provider_checkinterval() > 100 ) {
-            this.monitor = new FileMonitor(this, config.provider_checkinterval());
+            this.monitor = new FileMonitor(this, config.provider_checkinterval(), contentFileExtensions);
         }
     }
 
@@ -284,8 +264,10 @@ public class FsResourceProvider extends ResourceProvider<Object> {
             this.monitor = null;
         }
         this.providerRoot = null;
-        this.providerRootPrefix = null;
         this.providerFile = null;
+        this.overlayParentResourceProvider = false;
+        this.fileMapper = null;
+        this.contentFileMapper = null;
     }
 
     File getRootFile() {
@@ -325,45 +307,6 @@ public class FsResourceProvider extends ResourceProvider<Object> {
         return providerFile;
     }
 
-    /**
-     * Returns a file corresponding to the given absolute resource tree path. If
-     * the path equals the configured provider root, the provider root file is
-     * returned. If the path starts with the configured provider root, a file is
-     * returned relative to the provider root file whose relative path is the
-     * remains of the resource tree path without the provider root path.
-     * Otherwise <code>null</code> is returned.
-     */
-    private File getFile(String path) {
-        if (path.equals(providerRoot)) {
-            return providerFile;
-        }
-
-        if (path.startsWith(providerRootPrefix)) {
-            String relPath = path.substring(providerRootPrefix.length());
-            return new File(providerFile, relPath);
-        }
-
-        return null;
-    }
-
-    private Resource getResource(final ResourceResolver resolver,
-            final String resourcePath, 
-            final File file) {
-
-        if (file != null) {
-
-            // if the file exists, but is not a directory or no repository entry
-            // exists, return it as a resource
-            if (file.exists()) {
-                return new FsResource(resolver, resourcePath, file);
-            }
-
-        }
-
-        // not applicable or not an existing file path
-        return null;
-    }
-
     public ObservationReporter getObservationReporter() {
         final ProviderContext ctx = this.getProviderContext();
         if ( ctx != null ) {
@@ -371,4 +314,5 @@ public class FsResourceProvider extends ResourceProvider<Object> {
         }
         return null;
     }
+
 }
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
new file mode 100644
index 0000000..319a3de
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
@@ -0,0 +1,143 @@
+/*
+ * 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 java.io.File;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.fsprovider.internal.parser.ContentFileParser;
+
+/**
+ * Reference to a file that contains a content fragment (e.g. JSON, JCR XML).
+ */
+public final class ContentFile {
+    
+    private final File file;
+    private final String subPath;
+    private boolean contentInitialized;
+    private Object content;
+    private ValueMap valueMap;
+    
+    /**
+     * @param file File with content fragment
+     * @param subPath Relative path addressing content fragment inside file
+     */
+    public ContentFile(File file, String subPath) {
+        this.file = file;
+        this.subPath = subPath;
+    }
+
+    /**
+     * @param file File with content fragment
+     * @param subPath Relative path addressing content fragment inside file
+     * @param content Content
+     */
+    public ContentFile(File file, String subPath, Object content) {
+        this.file = file;
+        this.subPath = subPath;
+        this.contentInitialized = true;
+        this.content = content;
+    }
+
+    /**
+     * @return File with content fragment
+     */
+    public File getFile() {
+        return file;
+    }
+
+    /**
+     * @return Relative path addressing content fragment inside file
+     */
+    public String getSubPath() {
+        return subPath;
+    }
+    
+    /**
+     * Content object referenced by sub path.
+     * @return Map if resource, property value if property.
+     */
+    public Object getContent() {
+        if (!contentInitialized) {
+            Map<String,Object> rootContent = ContentFileParser.parse(file);
+            content = getDeepContent(rootContent, subPath);
+            contentInitialized = true;
+        }
+        return content;
+    }
+    
+    /**
+     * @return true if any content was found.
+     */
+    public boolean hasContent() {
+        return getContent() != null;
+    }
+    
+    /**
+     * @return true if content references resource map.
+     */
+    public boolean isResource() {
+        return (getContent() instanceof Map);
+    }
+    
+    /**
+     * @return ValueMap for resource. Never null.
+     */
+    @SuppressWarnings("unchecked")
+    public ValueMap getValueMap() {
+        if (valueMap == null) {
+            Object currentContent = getContent();
+            if (currentContent instanceof Map) {
+                valueMap = ValueMapUtil.toValueMap((Map<String,Object>)currentContent);
+            }
+            else {
+                valueMap = ValueMap.EMPTY;
+            }
+        }
+        return valueMap;
+    }
+    
+    @SuppressWarnings("unchecked")
+    private static Object getDeepContent(Object object, String subPath) {
+        if (object == null) {
+            return null;
+        }
+        if (subPath == null) {
+            return object;
+        }
+        if (!(object instanceof Map)) {
+            return null;
+        }
+        String name;
+        String remainingSubPath;
+        int slashIndex = subPath.indexOf('/');
+        if (slashIndex >= 0) {
+            name = subPath.substring(0, slashIndex);
+            remainingSubPath = subPath.substring(slashIndex + 1);
+        }
+        else {
+            name = subPath;
+            remainingSubPath = null;
+        }
+        Object subObject = ((Map<String,Object>)object).get(name);
+        return getDeepContent(subObject, remainingSubPath);
+    }
+    
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResource.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResource.java
new file mode 100644
index 0000000..11bfcbb
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResource.java
@@ -0,0 +1,125 @@
+/*
+ * 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 javax.jcr.Node;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.sling.api.resource.AbstractResource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.fsprovider.internal.mapper.jcr.FsNode;
+
+/**
+ * Represents a JSON File with resource content.
+ */
+public final class ContentFileResource extends AbstractResource {
+
+    // the owning resource resolver
+    private final ResourceResolver resolver;
+
+    // the path of this resource in the resource tree
+    private final String resourcePath;
+
+    // the file wrapped by this instance
+    private final ContentFile contentFile;
+
+    // the resource type, assigned on demand
+    private String resourceType;
+    private String resourceSuperType;
+
+    // the resource metadata, assigned on demand
+    private ResourceMetadata metaData;
+
+    /**
+     * @param resolver The owning resource resolver
+     * @param resourcePath The resource path in the resource tree
+     * @param contentFile Content file with sub path
+     */
+    ContentFileResource(ResourceResolver resolver, String resourcePath, ContentFile contentFile) {
+        this.resolver = resolver;
+        this.resourcePath = resourcePath;
+        this.contentFile = contentFile;
+    }
+
+    public String getPath() {
+        return resourcePath;
+    }
+
+    public ResourceMetadata getResourceMetadata() {
+        if (metaData == null) {
+            metaData = new ResourceMetadata();
+            metaData.setModificationTime(contentFile.getFile().lastModified());
+            metaData.setResolutionPath(resourcePath);
+        }
+        return metaData;
+    }
+
+    public ResourceResolver getResourceResolver() {
+        return resolver;
+    }
+
+    public String getResourceSuperType() {
+        if (resourceSuperType == null) {
+            resourceSuperType = contentFile.getValueMap().get("sling:resourceSuperType", String.class);
+        }
+        return resourceSuperType;
+    }
+
+    public String getResourceType() {
+        if (resourceType == null) {
+            ValueMap props = getValueMap();
+            resourceType = props.get("sling:resourceType", String.class);
+            if (resourceType == null) {
+                // fallback to jcr:primaryType when resource type not set
+                resourceType = props.get("jcr:primaryType", String.class);
+            }
+        }
+        return resourceType;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        if (type == ContentFile.class) {
+            return (AdapterType)this.contentFile;
+        }
+        else if (type == ValueMap.class) {
+            return (AdapterType)contentFile.getValueMap();
+        }
+        else if (type == Node.class && contentFile.isResource()) {
+            // support a subset of JCR API for content file resources
+            return (AdapterType)new FsNode(this);
+        }
+        return super.adaptTo(type);
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+                .append("path", resourcePath)
+                .append("file", contentFile.getFile().getPath())
+                .append("subPath", contentFile.getSubPath())
+                .append("resourceType", getResourceType())
+                .build();
+    }
+
+}
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
new file mode 100644
index 0000000..1fe257c
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
@@ -0,0 +1,146 @@
+/*
+ * 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 java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.collections.Transformer;
+import org.apache.commons.lang3.StringUtils;
+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.ContentFileExtensions;
+import org.apache.sling.fsprovider.internal.FsResourceMapper;
+
+public final class ContentFileResourceMapper implements FsResourceMapper {
+    
+    // providerRoot + "/" to be used for prefix matching of paths
+    private final String providerRootPrefix;
+
+    // The "root" file or folder in the file system
+    private final File providerFile;
+    
+    private final ContentFileExtensions contentFileExtensions;
+    
+    public ContentFileResourceMapper(String providerRoot, File providerFile, ContentFileExtensions contentFileExtensions) {
+        this.providerRootPrefix = providerRoot.concat("/");
+        this.providerFile = providerFile;
+        this.contentFileExtensions = contentFileExtensions;
+    }
+    
+    @Override
+    public Resource getResource(final ResourceResolver resolver, final String resourcePath) {
+        ContentFile contentFile = getFile(resourcePath, null);
+        if (contentFile != null && contentFile.hasContent()) {
+            return new ContentFileResource(resolver, resourcePath, contentFile);
+        }
+        else {
+            return null;
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public Iterator<Resource> getChildren(final ResourceResolver resolver, final Resource parent) {
+        final String parentPath = parent.getPath();
+        ContentFile parentContentFile = parent.adaptTo(ContentFile.class);
+
+        // not a FsResource, try to create one from the resource
+        if (parentContentFile == null) {
+            parentContentFile = getFile(parentPath, null);
+            if (parentContentFile == null) {
+                
+                // check if parent is a file resource that contains a file content resource
+                File parentFile = parent.adaptTo(File.class);
+                if (parentFile != null && parentFile.isDirectory()) {
+                    List<Resource> childResources = new ArrayList<>();
+                    for (File file : parentFile.listFiles()) {
+                        String filenameSuffix = contentFileExtensions.getSuffix(file);
+                        if (filenameSuffix != null) {
+                            ContentFile contentFile = new ContentFile(file, null);
+                            String path = parentPath + "/" + StringUtils.substringBeforeLast(file.getName(), filenameSuffix);
+                            childResources.add(new ContentFileResource(resolver, path, contentFile));
+                        }
+                    }
+                    if (!childResources.isEmpty()) {
+                        return childResources.iterator();
+                    }
+                }
+                
+                // no children here
+                return null;
+            }
+        }
+
+        // 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(), subPath, entry.getValue()));
+                }
+            }
+        }
+        if (children.isEmpty()) {
+            return null;
+        }
+        else {
+            return IteratorUtils.transformedIterator(children.iterator(), new Transformer() {
+                @Override
+                public Object transform(Object input) {
+                    ContentFile contentFile = (ContentFile)input;
+                    String path = parentPath + "/" + ResourceUtil.getName(contentFile.getSubPath());
+                    return new ContentFileResource(resolver, path, contentFile);
+                }
+            });
+        }
+    }
+    
+    private ContentFile getFile(String path, String subPath) {
+        if (!StringUtils.startsWith(path, providerRootPrefix)) {
+            return null;
+        }
+        String relPath = path.substring(providerRootPrefix.length());
+        for (String filenameSuffix : contentFileExtensions.getSuffixes()) {
+            File file = new File(providerFile, relPath + filenameSuffix);
+            if (file.exists()) {
+                return new ContentFile(file, subPath);
+            }
+        }
+        // try to find in parent path which contains content fragment
+        String parentPath = ResourceUtil.getParent(path);
+        String nextSubPath = path.substring(parentPath.length() + 1)
+                + (subPath != null ? "/" + subPath : "");
+        return getFile(parentPath, nextSubPath);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResourceMapper.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResourceMapper.java
new file mode 100644
index 0000000..12b0f7a
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResourceMapper.java
@@ -0,0 +1,145 @@
+/*
+ * 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 java.io.File;
+import java.util.Iterator;
+
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.collections.Predicate;
+import org.apache.commons.collections.Transformer;
+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.ContentFileExtensions;
+import org.apache.sling.fsprovider.internal.FsResourceMapper;
+
+public final class FileResourceMapper implements FsResourceMapper {
+
+    // The location in the resource tree where the resources are mapped
+    private final String providerRoot;
+
+    // providerRoot + "/" to be used for prefix matching of paths
+    private final String providerRootPrefix;
+
+    // The "root" file or folder in the file system
+    private final File providerFile;
+    
+    private final ContentFileExtensions contentFileExtensions;
+    
+    public FileResourceMapper(String providerRoot, File providerFile, ContentFileExtensions contentFileExtensions) {
+        this.providerRoot = providerRoot;
+        this.providerRootPrefix = providerRoot.concat("/");
+        this.providerFile = providerFile;
+        this.contentFileExtensions = contentFileExtensions;
+    }
+    
+    @Override
+    public Resource getResource(final ResourceResolver resolver, final String resourcePath) {
+        File file = getFile(resourcePath);
+        if (file != null) {
+            return new FileResource(resolver, resourcePath, file);
+        }
+        else {
+            return null;
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public Iterator<Resource> getChildren(final ResourceResolver resolver, final Resource parent) {
+        final String parentPath = parent.getPath();
+        File parentFile = parent.adaptTo(File.class);
+
+        // not a FsResource, try to create one from the resource
+        if (parentFile == null) {
+            // if the parent path is at or below the provider root, get
+            // the respective file
+            parentFile = getFile(parentPath);
+
+            // if the parent path is actually the parent of the provider
+            // root, return a single element iterator just containing the
+            // provider file, unless the provider file is a directory and
+            // a repository item with the same path actually exists
+            if (parentFile == null) {
+
+                if (providerFile.exists() && !StringUtils.startsWith(parentPath, providerRoot)) {
+                    String parentPathPrefix = parentPath.concat("/");
+                    if (providerRoot.startsWith(parentPathPrefix)) {
+                        String relPath = providerRoot.substring(parentPathPrefix.length());
+                        if (relPath.indexOf('/') < 0) {
+                            Resource res = new FileResource(resolver, providerRoot, providerFile);
+                            return IteratorUtils.singletonIterator(res);
+                        }
+                    }
+                }
+
+                // no children here
+                return null;
+            }
+        }
+        
+        // ensure parent is a directory
+        if (!parentFile.isDirectory()) {
+            return null;
+        }
+
+        Iterator<File> children = IteratorUtils.filteredIterator(IteratorUtils.arrayIterator(parentFile.listFiles()), new Predicate() {
+            @Override
+            public boolean evaluate(Object object) {
+                File file = (File)object;
+                return !contentFileExtensions.matchesSuffix(file);
+            }
+        });
+        if (!children.hasNext()) {
+            return null;
+        }
+        return IteratorUtils.transformedIterator(children, new Transformer() {
+            @Override
+            public Object transform(Object input) {
+                File file = (File)input;
+                String path = parentPath + "/" + file.getName();
+                return new FileResource(resolver, path, file);
+            }
+        });
+    }
+
+    /**
+     * Returns a file corresponding to the given absolute resource tree path. If
+     * the path equals the configured provider root, the provider root file is
+     * returned. If the path starts with the configured provider root, a file is
+     * returned relative to the provider root file whose relative path is the
+     * remains of the resource tree path without the provider root path.
+     * Otherwise <code>null</code> is returned.
+     */
+    private File getFile(String path) {
+        if (path.equals(providerRoot)) {
+            return providerFile;
+        }
+        if (path.startsWith(providerRootPrefix)) {
+            String relPath = path.substring(providerRootPrefix.length());
+            File file = new File(providerFile, relPath);
+            if (file.exists() && !contentFileExtensions.matchesSuffix(file)) {
+                return file;
+            }
+        }
+        return null;
+    }
+    
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtil.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtil.java
new file mode 100644
index 0000000..67122b1
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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 java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+
+final class ValueMapUtil {
+    
+    private ValueMapUtil() {
+        // static methods only
+    }
+    
+    /**
+     * Convert map to value map.
+     * @param content Content map.
+     * @return Value map.
+     */
+    public static ValueMap toValueMap(Map<String,Object> content) {
+        Map<String,Object> props = new HashMap<>();
+        for (Map.Entry<String, Object> entry : ((Map<String,Object>)content).entrySet()) {
+            if (entry.getValue() instanceof Map) {
+                // skip child resources
+                continue;
+            }
+            else if (entry.getValue() instanceof Collection) {
+                // convert lists to arrays
+                props.put(entry.getKey(), ((Collection)entry.getValue()).toArray());
+            }
+            else {
+                props.put(entry.getKey(), entry.getValue());
+            }
+        }
+        return new ValueMapDecorator(props);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsItem.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsItem.java
new file mode 100644
index 0000000..11e1836
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsItem.java
@@ -0,0 +1,165 @@
+/*
+ * 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.jcr;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.Item;
+import javax.jcr.ItemExistsException;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.ItemVisitor;
+import javax.jcr.Node;
+import javax.jcr.ReferentialIntegrityException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.version.VersionException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+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.api.resource.ValueMap;
+
+/**
+ * Simplified implementation of read-only content access via the JCR API.
+ */
+abstract class FsItem implements Item {
+    
+    protected final Resource resource;
+    protected final ValueMap props;
+    protected final ResourceResolver resolver;
+    
+    public FsItem(Resource resource) {
+        this.resource = resource;
+        this.props = resource.getValueMap();
+        this.resolver = resource.getResourceResolver();
+    }
+
+    @Override
+    public String getPath() throws RepositoryException {
+        return resource.getPath();
+    }
+
+    @Override
+    public String getName() throws RepositoryException {
+        return resource.getName();
+    }
+
+    @Override
+    public FsItem getAncestor(int depth) throws ItemNotFoundException, AccessDeniedException, RepositoryException {
+        String path = ResourceUtil.getParent(resource.getPath(), getDepth() - depth - 1);
+        if (path != null) {
+            Resource ancestor = resolver.getResource(path);
+            if (ancestor != null) {
+                return new FsNode(ancestor);
+            }
+        }
+        throw new ItemNotFoundException(path);
+    }
+
+    @Override
+    public Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException {
+        Resource parent = resource.getParent();
+        if (parent != null) {
+            Node parentNode = parent.adaptTo(Node.class);
+            if (parentNode != null) {
+                return parentNode;
+            }
+        }
+        throw new ItemNotFoundException();
+    }
+
+    @Override
+    public int getDepth() throws RepositoryException {
+        if (StringUtils.equals("/", getPath())) {
+            return 0;
+        } else {
+            return StringUtils.countMatches(getPath(), "/");
+        }
+    }
+
+    @Override
+    public Session getSession() throws RepositoryException {
+        return resolver.adaptTo(Session.class);
+    }
+
+    @Override
+    public boolean isNode() {
+        return (this instanceof Node);
+    }
+
+    @Override
+    public boolean isNew() {
+        return false;
+    }
+
+    @Override
+    public boolean isModified() {
+        return false;
+    }
+
+    @Override
+    public boolean isSame(Item otherItem) throws RepositoryException {
+        return StringUtils.equals(getPath(), otherItem.getPath());
+    }
+
+    @Override
+    public void accept(ItemVisitor visitor) throws RepositoryException {
+        // do nothing
+    }
+    
+    @Override
+    public String toString() {
+        try {
+            return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+                    .append("path", getPath())
+                    .build();
+        }
+        catch (RepositoryException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+    
+    
+    // --- unsupported methods ---
+
+    @Override
+    public void save() throws AccessDeniedException, ItemExistsException, ConstraintViolationException,
+            InvalidItemStateException, ReferentialIntegrityException, VersionException, LockException,
+            NoSuchNodeTypeException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void refresh(boolean keepChanges) throws InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void remove() throws VersionException, LockException, ConstraintViolationException, AccessDeniedException,
+            RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+}
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
new file mode 100644
index 0000000..308701c
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
@@ -0,0 +1,481 @@
+/*
+ * 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.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Binary;
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.InvalidLifecycleTransitionException;
+import javax.jcr.Item;
+import javax.jcr.ItemExistsException;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.MergeException;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import javax.jcr.lock.Lock;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.ActivityViolationException;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionException;
+import javax.jcr.version.VersionHistory;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Simplified implementation of read-only content access via the JCR API.
+ */
+public final class FsNode extends FsItem implements Node {
+    
+    public FsNode(Resource resource) {
+        super(resource);
+    }
+    
+    @Override
+    public Node getNode(String relPath) throws PathNotFoundException, RepositoryException {
+        Resource child = resource.getChild(relPath);
+        if (child != null) {
+            return new FsNode(child);
+        }
+        throw new PathNotFoundException(relPath);
+    }
+
+    @Override
+    public NodeIterator getNodes() throws RepositoryException {
+        return new FsNodeIterator(resource.listChildren());
+    }
+
+    @Override
+    public Property getProperty(String relPath) throws PathNotFoundException, RepositoryException {
+        if (props.containsKey(relPath)) {
+            return new FsProperty(resource, relPath, this);
+        }
+        throw new PathNotFoundException(relPath);
+    }
+
+    @Override
+    public PropertyIterator getProperties() throws RepositoryException {
+        return new FsPropertyIterator(props.keySet().iterator(), resource, this);
+    }
+
+    @Override
+    public String getUUID() throws UnsupportedRepositoryOperationException, RepositoryException {
+        String uuid = props.get("jcr:uuid", String.class);
+        if (uuid != null) {
+            return uuid;
+        }
+        else {
+            throw new UnsupportedRepositoryOperationException();
+        }
+    }
+
+    @Override
+    public boolean hasNode(String relPath) throws RepositoryException {
+        return resource.getChild(relPath) != null;
+    }
+
+    @Override
+    public boolean hasProperty(String relPath) throws RepositoryException {
+        return props.containsKey(relPath);
+    }
+
+    @Override
+    public boolean hasNodes() throws RepositoryException {
+        return resource.listChildren().hasNext();
+    }
+
+    @Override
+    public boolean hasProperties() throws RepositoryException {
+        return !props.isEmpty();
+    }
+
+    @Override
+    public boolean isNodeType(String nodeTypeName) throws RepositoryException {
+        return StringUtils.equals(nodeTypeName, props.get("jcr:primaryType", String.class));
+    }
+
+    @Override
+    public boolean canAddMixin(String mixinName) throws NoSuchNodeTypeException, RepositoryException {
+        return false;
+    }
+
+    @Override
+    public boolean isCheckedOut() throws RepositoryException {
+        return false;
+    }
+
+    @Override
+    public boolean holdsLock() throws RepositoryException {
+        return false;
+    }
+
+    @Override
+    public boolean isLocked() throws RepositoryException {
+        return false;
+    }
+
+
+    // --- unsupported methods ---
+    
+    @Override
+    public Node addNode(String relPath) throws ItemExistsException, PathNotFoundException, VersionException,
+            ConstraintViolationException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Node addNode(String relPath, String primaryNodeTypeName)
+            throws ItemExistsException, PathNotFoundException, NoSuchNodeTypeException, LockException, VersionException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void orderBefore(String srcChildRelPath, String destChildRelPath)
+            throws UnsupportedRepositoryOperationException, VersionException, ConstraintViolationException,
+            ItemNotFoundException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Value value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Value value, int type) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Value[] values) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Value[] values, int type) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, String[] values) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, String[] values, int type) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, String value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, String value, int type) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, InputStream value) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Binary value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, boolean value) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, double value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, BigDecimal value) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, long value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Calendar value) throws ValueFormatException, VersionException,
+            LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(String name, Node value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getReferences() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getReferences(String name) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getWeakReferences() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getWeakReferences(String name) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setPrimaryType(String nodeTypeName) throws NoSuchNodeTypeException, VersionException,
+            ConstraintViolationException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addMixin(String mixinName) throws NoSuchNodeTypeException, VersionException,
+            ConstraintViolationException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeMixin(String mixinName) throws NoSuchNodeTypeException, VersionException,
+            ConstraintViolationException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinition getDefinition() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Version checkin() throws VersionException, UnsupportedRepositoryOperationException,
+            InvalidItemStateException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void checkout() throws UnsupportedRepositoryOperationException, LockException, ActivityViolationException,
+            RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void doneMerge(Version version) throws VersionException, InvalidItemStateException,
+            UnsupportedRepositoryOperationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void cancelMerge(Version version) throws VersionException, InvalidItemStateException,
+            UnsupportedRepositoryOperationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void update(String srcWorkspace) throws NoSuchWorkspaceException, AccessDeniedException, LockException,
+            InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator merge(String srcWorkspace, boolean bestEffort) throws NoSuchWorkspaceException,
+            AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getCorrespondingNodePath(String workspaceName)
+            throws ItemNotFoundException, NoSuchWorkspaceException, AccessDeniedException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator getSharedSet() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeSharedSet()
+            throws VersionException, LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeShare()
+            throws VersionException, LockException, ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(String versionName, boolean removeExisting) throws VersionException, ItemExistsException,
+            UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(Version version, boolean removeExisting) throws VersionException, ItemExistsException,
+            InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(Version version, String relPath, boolean removeExisting)
+            throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException,
+            UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restoreByLabel(String versionLabel, boolean removeExisting)
+            throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException,
+            InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public VersionHistory getVersionHistory() throws UnsupportedRepositoryOperationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Version getBaseVersion() throws UnsupportedRepositoryOperationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Lock lock(boolean isDeep, boolean isSessionScoped) throws UnsupportedRepositoryOperationException,
+            LockException, AccessDeniedException, InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Lock getLock()
+            throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void unlock() throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException,
+            InvalidItemStateException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void followLifecycleTransition(String transition)
+            throws UnsupportedRepositoryOperationException, InvalidLifecycleTransitionException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getAllowedLifecycleTransistions()
+            throws UnsupportedRepositoryOperationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator getNodes(String namePattern) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator getNodes(String[] nameGlobs) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType getPrimaryNodeType() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getIdentifier() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getIndex() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType[] getMixinNodeTypes() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Item getPrimaryItem() throws ItemNotFoundException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getProperties(String namePattern) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getProperties(String[] nameGlobs) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+}
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
new file mode 100644
index 0000000..a2ef2fe
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeIterator.java
@@ -0,0 +1,74 @@
+/*
+ * 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.jcr;
+
+import java.util.Iterator;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Simplified implementation of read-only content access via the JCR API.
+ */
+class FsNodeIterator implements NodeIterator {
+    
+    private final Iterator<Resource> resources;
+
+    public FsNodeIterator(Iterator<Resource> resources) {
+        this.resources = resources;
+    }
+
+    public boolean hasNext() {
+        return resources.hasNext();
+    }
+
+    public Object next() {
+        return nextNode();
+    }
+
+    @Override
+    public Node nextNode() {
+        return resources.next().adaptTo(Node.class);
+    }
+
+    
+    // --- unsupported methods ---
+        
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void skip(long skipNum) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getSize() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getPosition() {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsProperty.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsProperty.java
new file mode 100644
index 0000000..4d1d138
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsProperty.java
@@ -0,0 +1,241 @@
+/*
+ * 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.jcr;
+
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Binary;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.PropertyDefinition;
+import javax.jcr.version.VersionException;
+
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Simplified implementation of read-only content access via the JCR API.
+ */
+class FsProperty extends FsItem implements Property {
+    
+    private final String propertyName;
+    private final Node node;
+    
+    public FsProperty(Resource resource, String propertyName, Node node) {
+        super(resource);
+        this.propertyName = propertyName;
+        this.node = node;
+    }
+    
+    @Override
+    public String getName() throws RepositoryException {
+        return propertyName;
+    }
+
+    @Override
+    public Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException {
+        return getNode();
+    }
+
+    @Override
+    public Node getNode() throws ItemNotFoundException, ValueFormatException, RepositoryException {
+        return node;
+    }
+    
+    @Override
+    public String getPath() throws RepositoryException {
+        return resource.getPath() + "/" + propertyName;
+    }
+
+    @Override
+    public Value getValue() throws ValueFormatException, RepositoryException {
+        return new FsValue(props, propertyName);
+    }
+
+    @Override
+    public String getString() throws ValueFormatException, RepositoryException {
+        return getValue().getString();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public InputStream getStream() throws ValueFormatException, RepositoryException {
+        return getValue().getStream();
+    }
+
+    @Override
+    public Binary getBinary() throws ValueFormatException, RepositoryException {
+        return getValue().getBinary();
+    }
+
+    @Override
+    public long getLong() throws ValueFormatException, RepositoryException {
+        return getValue().getLong();
+    }
+
+    @Override
+    public double getDouble() throws ValueFormatException, RepositoryException {
+        return getValue().getDouble();
+    }
+
+    @Override
+    public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
+        return getValue().getDecimal();
+    }
+
+    @Override
+    public Calendar getDate() throws ValueFormatException, RepositoryException {
+        return getValue().getDate();
+    }
+
+    @Override
+    public boolean getBoolean() throws ValueFormatException, RepositoryException {
+        return getValue().getBoolean();
+    }
+
+    @Override
+    public Value[] getValues() throws ValueFormatException, RepositoryException {
+        if (!isMultiple()) {
+            throw new ValueFormatException();
+        }
+        Object value = props.get(propertyName);
+        int size = Array.getLength(value);
+        Value[] result = new Value[size];
+        for (int i=0; i<size; i++) {
+            result[i] = new FsValue(props, propertyName, i);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean isMultiple() throws RepositoryException {
+        Object value = props.get(propertyName);
+        return value != null && value.getClass().isArray();
+    }
+
+    @Override
+    public int getType() throws RepositoryException {
+        return getValue().getType();
+    }
+    
+
+    // --- unsupported methods ---
+    
+    @Override
+    public void setValue(Value value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(String value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(String[] values) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Binary value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(long value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(double value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(boolean value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Node value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyDefinition getDefinition() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property getProperty() throws ItemNotFoundException, ValueFormatException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLength() throws ValueFormatException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long[] getLengths() throws ValueFormatException, RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsPropertyIterator.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsPropertyIterator.java
new file mode 100644
index 0000000..de2d572
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsPropertyIterator.java
@@ -0,0 +1,79 @@
+/*
+ * 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.jcr;
+
+import java.util.Iterator;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Simplified implementation of read-only content access via the JCR API.
+ */
+class FsPropertyIterator implements PropertyIterator {
+    
+    private final Iterator<String> propertyNames;
+    private final Resource resource;
+    private final Node node;
+    
+    public FsPropertyIterator(Iterator<String> propertyNames, Resource resource, Node node) {
+        this.propertyNames = propertyNames;
+        this.resource = resource;
+        this.node = node;
+    }
+
+    public boolean hasNext() {
+        return propertyNames.hasNext();
+    }
+
+    public Object next() {
+        return nextProperty();
+    }
+
+    @Override
+    public Property nextProperty() {
+        return new FsProperty(resource, propertyNames.next(), node);
+    }
+
+    
+    // --- unsupported methods ---
+        
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void skip(long skipNum) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getSize() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getPosition() {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsValue.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsValue.java
new file mode 100644
index 0000000..4ca2873
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsValue.java
@@ -0,0 +1,162 @@
+/*
+ * 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.jcr;
+
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.Binary;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+import org.apache.sling.api.resource.ValueMap;
+
+/**
+ * Simplified implementation of read-only content access via the JCR API.
+ */
+class FsValue implements Value {
+    
+    private final ValueMap props;
+    private final String propertyName;
+    private final int arrayIndex;
+    
+    public FsValue(ValueMap props, String propertyName) {
+        this.props = props;
+        this.propertyName = propertyName;
+        this.arrayIndex = -1;
+    }
+
+    public FsValue(ValueMap props, String propertyName, int arrayIndex) {
+        this.props = props;
+        this.propertyName = propertyName;
+        this.arrayIndex = arrayIndex;
+    }
+
+    @Override
+    public String getString() throws ValueFormatException, IllegalStateException, RepositoryException {
+        if (arrayIndex >= 0) {
+            return props.get(propertyName, String[].class)[arrayIndex];
+        }
+        else {
+            return props.get(propertyName, String.class);
+        }
+    }
+
+    @Override
+    public long getLong() throws ValueFormatException, RepositoryException {
+        if (arrayIndex >= 0) {
+            return props.get(propertyName, Long[].class)[arrayIndex];
+        }
+        else {
+            return props.get(propertyName, 0L);
+        }
+    }
+
+    @Override
+    public double getDouble() throws ValueFormatException, RepositoryException {
+        if (arrayIndex >= 0) {
+            return props.get(propertyName, Double[].class)[arrayIndex];
+        }
+        else {
+            return props.get(propertyName, 0d);
+        }
+    }
+
+    @Override
+    public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
+        if (arrayIndex >= 0) {
+            return props.get(propertyName, BigDecimal[].class)[arrayIndex];
+        }
+        else {
+            return props.get(propertyName, BigDecimal.ZERO);
+        }
+    }
+
+    @Override
+    public Calendar getDate() throws ValueFormatException, RepositoryException {
+        if (arrayIndex >= 0) {
+            return props.get(propertyName, Calendar[].class)[arrayIndex];
+        }
+        else {
+            return props.get(propertyName, Calendar.class);
+        }
+    }
+
+    @Override
+    public boolean getBoolean() throws ValueFormatException, RepositoryException {
+        if (arrayIndex >= 0) {
+            return props.get(propertyName, Boolean[].class)[arrayIndex];
+        }
+        else {
+            return props.get(propertyName, false);
+        }
+    }
+
+    @Override
+    public int getType() {
+        Object value = props.get(propertyName);
+        if (value == null) {
+            return PropertyType.UNDEFINED;
+        }
+        Class type = value.getClass();
+        if (type.isArray() && Array.getLength(value) > 0) {
+            Object firstItem = Array.get(value, 0);
+            if (firstItem != null) {
+                type = firstItem.getClass();
+            }
+        }
+        if (type == String.class) {
+            return PropertyType.STRING;
+        }
+        if (type == Boolean.class || type == boolean.class) {
+            return PropertyType.BOOLEAN;
+        }
+        if (type == BigDecimal.class) {
+            return PropertyType.DECIMAL;
+        }
+        if (type == Double.class || type == double.class || type == Float.class || type == float.class) {
+            return PropertyType.DOUBLE;
+        }
+        if (Number.class.isAssignableFrom(type)) {
+            return PropertyType.LONG;
+        }
+        if (type == Calendar.class) {
+            return PropertyType.DATE;
+        }
+        return PropertyType.UNDEFINED;
+    }
+
+
+    // --- unsupported methods ---
+    
+    @Override
+    public InputStream getStream() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Binary getBinary() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
new file mode 100644
index 0000000..46d5b85
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
@@ -0,0 +1,52 @@
+/*
+ * 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.parser;
+
+import java.io.File;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Parses file that contains content fragments (e.g. JSON, JCR XML).
+ */
+public final class ContentFileParser {
+    
+    /**
+     * JSON content files.
+     */
+    public static final String JSON_SUFFIX = ".json";
+    
+    private ContentFileParser() {
+        // static methods only
+    }
+    
+    /**
+     * Parse content from file.
+     * @param file File. Type is detected automatically.
+     * @return Content or null if content could not be parsed.
+     */
+    public static Map<String,Object> parse(File file) {
+        if (StringUtils.endsWith(file.getName(), JSON_SUFFIX)) {
+            return JsonFileParser.parse(file);
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/parser/JsonFileParser.java b/src/main/java/org/apache/sling/fsprovider/internal/parser/JsonFileParser.java
new file mode 100644
index 0000000..c9eddc5
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/parser/JsonFileParser.java
@@ -0,0 +1,126 @@
+/*
+ * 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.parser;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonReaderFactory;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parses JSON file that contains content fragments.
+ */
+class JsonFileParser {
+    
+    private static final Logger log = LoggerFactory.getLogger(JsonFileParser.class);
+    
+    private static final JsonReaderFactory JSON_READER_FACTORY;
+    static {
+        // allow comments in JSON files
+        Map<String,Object> jsonReaderFactoryConfig = new HashMap<>();
+        jsonReaderFactoryConfig.put("org.apache.johnzon.supports-comments", true);
+        // workaround for JsonProvider classloader issue until https://issues.apache.org/jira/browse/GERONIMO-6560 is fixed
+        ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
+        try {
+            Thread.currentThread().setContextClassLoader(JsonFileParser.class.getClassLoader());
+            JSON_READER_FACTORY = Json.createReaderFactory(jsonReaderFactoryConfig);
+        }
+        finally {
+            Thread.currentThread().setContextClassLoader(oldClassLoader);
+        }
+    }
+    
+    private JsonFileParser() {
+        // static methods only
+    }
+    
+    /**
+     * Parse JSON file.
+     * @param file File
+     * @return Content
+     */
+    public static Map<String,Object> parse(File file) {
+        log.debug("Parse JSON content from {}", file.getPath());
+        try {
+            try (FileInputStream fis = new FileInputStream(file);
+                    JsonReader reader = JSON_READER_FACTORY.createReader(fis)) {
+                return toMap(reader.readObject());
+            }
+        }
+        catch (IOException ex) {
+            log.warn("Error parsing JSON content from " + file.getPath(), ex);
+            return null;
+        }
+    }
+    
+    private static Map<String,Object> toMap(JsonObject object) {
+        Map<String,Object> map = new LinkedHashMap<>();
+        for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
+            map.put(entry.getKey(), convertValue(entry.getValue()));
+        }
+        return map;
+    }
+    
+    private static Object convertValue(JsonValue value) {
+        switch (value.getValueType()) {
+            case STRING:
+                return ((JsonString)value).getString();
+            case NUMBER:
+                JsonNumber numberValue = (JsonNumber)value;
+                if (numberValue.isIntegral()) {
+                    return numberValue.longValue();
+                }
+                else {
+                    return numberValue.doubleValue();
+                }
+            case TRUE:
+                return true;
+            case FALSE:
+                return false;
+            case NULL:
+                return null;
+            case ARRAY:
+                JsonArray arrayValue = (JsonArray)value;
+                Object[] values = new Object[arrayValue.size()];
+                for (int i=0; i<values.length; i++) {
+                    values[i] = convertValue(arrayValue.get(i));
+                }
+                return values;
+            case OBJECT:
+                return toMap((JsonObject)value);
+            default:
+                throw new IllegalArgumentException("Unexpected JSON value type: " + value.getValueType());
+        }
+    }
+    
+}
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 3201c3f..a6f7568 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
@@ -63,7 +63,8 @@ public class FileMonitorTest {
                 context.registerInjectActivateService(new FsResourceProvider(),
                         "provider.file", tempDir.getPath(),
                         "provider.root", "/fs-test",
-                        "provider.checkinterval", 120);
+                        "provider.checkinterval", 120,
+                        "provider.json.content", true);
                 
                 // register resource change listener
                 context.registerService(ResourceChangeListener.class, resourceListener,
@@ -85,7 +86,7 @@ public class FileMonitorTest {
         assertTrue(changes.isEmpty());
         
         File file1a = new File(tempDir, "folder1/file1a.txt");
-        FileUtils.write(file1a, "newcontent");
+        FileUtils.touch(file1a);
         
         Thread.sleep(250);
 
@@ -153,6 +154,50 @@ public class FileMonitorTest {
         assertChange(changes, 1, "/fs-test/folder1", ChangeType.REMOVED);
     }
 
+    @Test
+    public void testUpdateJsonContent() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file1a = new File(tempDir, "folder2/content.json");
+        FileUtils.touch(file1a);
+        
+        Thread.sleep(250);
+
+        assertEquals(1, changes.size());
+        assertChange(changes, 0, "/fs-test/folder2/content", ChangeType.CHANGED);
+    }
+    
+    @Test
+    public void testAddJsonContent() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file1c = new File(tempDir, "folder1/file1c.json");
+        FileUtils.write(file1c, "{'prop1':'value1'}");
+        
+        Thread.sleep(250);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, 0, "/fs-test/folder1", ChangeType.CHANGED);
+        assertChange(changes, 1, "/fs-test/folder1/file1c", ChangeType.ADDED);
+    }
+    
+    @Test
+    public void testRemoveJsonContent() throws Exception {
+        List<ResourceChange> changes = resourceListener.getChanges();
+        assertTrue(changes.isEmpty());
+        
+        File file1a = new File(tempDir, "folder2/content.json");
+        file1a.delete();
+        
+        Thread.sleep(250);
+
+        assertEquals(2, changes.size());
+        assertChange(changes, 0, "/fs-test/folder2", ChangeType.CHANGED);
+        assertChange(changes, 1, "/fs-test/folder2/content", ChangeType.REMOVED);
+    }
+    
     
     private void assertChange(List<ResourceChange> changes, int index, String path, ChangeType changeType) {
         ResourceChange change = changes.get(index);
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java b/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
index d837d6e..578befd 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
@@ -18,12 +18,13 @@
  */
 package org.apache.sling.fsprovider.internal;
 
-import static org.apache.sling.fsprovider.internal.TestUtils.REGISTER_FSRESOURCE_PLUGIN;
 import static org.apache.sling.fsprovider.internal.TestUtils.assertFile;
 import static org.apache.sling.fsprovider.internal.TestUtils.assertFolder;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThat;
 
 import org.apache.sling.api.resource.Resource;
+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;
@@ -42,7 +43,7 @@ public class FilesFolderTest {
 
     @Rule
     public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
-        .plugin(REGISTER_FSRESOURCE_PLUGIN)
+        .plugin(new RegisterFsResourcePlugin())
         .build();
 
     @Before
@@ -63,14 +64,16 @@ public class FilesFolderTest {
         assertFile(fsroot, "folder1/file1a.txt", "file1a");
         assertFile(fsroot, "folder1/file1b.txt", "file1b");
         assertFile(fsroot, "folder1/folder11/file11a.txt", "file11a");
-        assertFile(fsroot, "folder2/file2a.txt", "file2a");
+        assertFile(fsroot, "folder2/content.json", null);
     }
 
     @Test
     public void testListChildren() {
         assertThat(root, ResourceMatchers.containsChildren("fs-test"));
         assertThat(fsroot, ResourceMatchers.hasChildren("folder1", "folder2"));
-        assertThat(fsroot.getChild("folder1"), ResourceMatchers.hasChildren("file1a.txt", "file1b.txt"));
+        assertThat(fsroot.getChild("folder1"), ResourceMatchers.hasChildren("folder11", "file1a.txt", "file1b.txt"));
+        assertThat(fsroot.getChild("folder2"), ResourceMatchers.hasChildren("folder21", "content.json"));
+        assertFalse(fsroot.getChild("folder1/file1a.txt").listChildren().hasNext());
     }
 
 }
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java b/src/test/java/org/apache/sling/fsprovider/internal/InvalidRootFolderTest.java
similarity index 57%
copy from src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
copy to src/test/java/org/apache/sling/fsprovider/internal/InvalidRootFolderTest.java
index d837d6e..9f38c1a 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/InvalidRootFolderTest.java
@@ -18,13 +18,11 @@
  */
 package org.apache.sling.fsprovider.internal;
 
-import static org.apache.sling.fsprovider.internal.TestUtils.REGISTER_FSRESOURCE_PLUGIN;
-import static org.apache.sling.fsprovider.internal.TestUtils.assertFile;
-import static org.apache.sling.fsprovider.internal.TestUtils.assertFolder;
-import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 
 import org.apache.sling.api.resource.Resource;
-import org.apache.sling.hamcrest.ResourceMatchers;
+import org.apache.sling.fsprovider.internal.TestUtils.RegisterFsResourcePlugin;
 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;
@@ -33,44 +31,35 @@ import org.junit.Rule;
 import org.junit.Test;
 
 /**
- * Test access to files and folders from filesystem.
+ * Test with invalid fs folder.
  */
-public class FilesFolderTest {
+public class InvalidRootFolderTest {
 
-    private Resource root;
     private Resource fsroot;
 
     @Rule
     public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
-        .plugin(REGISTER_FSRESOURCE_PLUGIN)
+        .plugin(new RegisterFsResourcePlugin("provider.file", "/invalid-folder"))
         .build();
 
     @Before
     public void setUp() {
-        root = context.resourceResolver().getResource("/");
         fsroot = context.resourceResolver().getResource("/fs-test");
     }
 
     @Test
     public void testFolders() {
-        assertFolder(fsroot, "folder1");
-        assertFolder(fsroot, "folder1/folder11");
-        assertFolder(fsroot, "folder2");
+        assertNull(fsroot.getChild("folder1"));
     }
 
     @Test
     public void testFiles() {
-        assertFile(fsroot, "folder1/file1a.txt", "file1a");
-        assertFile(fsroot, "folder1/file1b.txt", "file1b");
-        assertFile(fsroot, "folder1/folder11/file11a.txt", "file11a");
-        assertFile(fsroot, "folder2/file2a.txt", "file2a");
+        assertNull(fsroot.getChild("folder1/file1a.txt"));
     }
 
     @Test
     public void testListChildren() {
-        assertThat(root, ResourceMatchers.containsChildren("fs-test"));
-        assertThat(fsroot, ResourceMatchers.hasChildren("folder1", "folder2"));
-        assertThat(fsroot.getChild("folder1"), ResourceMatchers.hasChildren("file1a.txt", "file1b.txt"));
+        assertFalse(fsroot.listChildren().hasNext());
     }
 
 }
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/JcrMixedTest.java b/src/test/java/org/apache/sling/fsprovider/internal/JcrMixedTest.java
index 41bdbae..1231090 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/JcrMixedTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/JcrMixedTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.sling.fsprovider.internal;
 
-import static org.apache.sling.fsprovider.internal.TestUtils.REGISTER_FSRESOURCE_PLUGIN;
 import static org.apache.sling.fsprovider.internal.TestUtils.assertFile;
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
@@ -27,6 +26,7 @@ import javax.jcr.Node;
 import javax.jcr.RepositoryException;
 
 import org.apache.sling.api.resource.Resource;
+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;
@@ -45,7 +45,7 @@ public class JcrMixedTest {
 
     @Rule
     public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
-        .plugin(REGISTER_FSRESOURCE_PLUGIN)
+        .plugin(new RegisterFsResourcePlugin())
         .build();
 
     @Before
@@ -84,7 +84,7 @@ public class JcrMixedTest {
         assertFile(fsroot, "folder1/file1a.txt", "file1a");
         assertFile(fsroot, "folder1/file1b.txt", "file1b");
         assertFile(fsroot, "folder1/folder11/file11a.txt", "file11a");
-        assertFile(fsroot, "folder2/file2a.txt", "file2a");
+        assertFile(fsroot, "folder2/content.json", null);
 
         // do not expected properties from JCR for files
         Resource file1a = fsroot.getChild("folder1/file1a.txt");
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
new file mode 100644
index 0000000..1dcf7a4
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.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.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.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+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;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Test access to files and folders from filesystem.
+ */
+public class JsonContentTest {
+
+    private Resource root;
+    private Resource fsroot;
+
+    @Rule
+    public SlingContext context = new SlingContextBuilder(ResourceResolverType.JCR_MOCK)
+        .plugin(new RegisterFsResourcePlugin("provider.json.content", true))
+        .build();
+
+    @Before
+    public void setUp() {
+        root = context.resourceResolver().getResource("/");
+        fsroot = context.resourceResolver().getResource("/fs-test");
+    }
+
+    @Test
+    public void testFolders() {
+        assertFolder(fsroot, "folder1");
+        assertFolder(fsroot, "folder1/folder11");
+        assertFolder(fsroot, "folder2");
+    }
+
+    @Test
+    public void testFiles() {
+        assertFile(fsroot, "folder1/file1a.txt", "file1a");
+        assertFile(fsroot, "folder1/file1b.txt", "file1b");
+        assertFile(fsroot, "folder1/folder11/file11a.txt", "file11a");
+        assertNull(fsroot.getChild("folder2/content.json"));
+    }
+
+    @Test
+    public void testListChildren() {
+        assertThat(root, ResourceMatchers.containsChildren("fs-test"));
+        assertThat(fsroot, ResourceMatchers.hasChildren("folder1", "folder2"));
+        assertThat(fsroot.getChild("folder1"), ResourceMatchers.hasChildren("folder11", "file1a.txt", "file1b.txt"));
+        assertThat(fsroot.getChild("folder2"), ResourceMatchers.hasChildren("folder21", "content"));
+    }
+
+    @Test
+    public void testJsonContent_Root() {
+        Resource underTest = fsroot.getChild("folder2/content");
+        assertNotNull(underTest);
+        assertEquals("app:Page", underTest.getValueMap().get("jcr:primaryType", String.class));
+        assertEquals("app:Page", underTest.getResourceType());
+        assertThat(underTest, ResourceMatchers.hasChildren("jcr:content"));
+    }
+
+    @Test
+    public void testJsonContent_Level1() {
+        Resource underTest = fsroot.getChild("folder2/content/jcr:content");
+        assertNotNull(underTest);
+        assertEquals("app:PageContent", underTest.getValueMap().get("jcr:primaryType", String.class));
+        assertEquals("sample/components/homepage", underTest.getResourceType());
+        assertEquals("sample/components/supertype", underTest.getResourceSuperType());
+        assertThat(underTest, ResourceMatchers.hasChildren("par", "header", "newslist", "lead", "image", "carousel", "rightpar"));
+    }
+
+    @Test
+    public void testJsonContent_Level5() {
+        Resource underTest = fsroot.getChild("folder2/content/jcr:content/par/image/file/jcr:content");
+        assertNotNull(underTest);
+        assertEquals("nt:resource", underTest.getValueMap().get("jcr:primaryType", String.class));
+        assertFalse(underTest.listChildren().hasNext());
+    }
+
+    @Test
+    public void testJsonContent_Datatypes() {
+        Resource underTest = fsroot.getChild("folder2/content/toolbar/profiles/jcr:content");
+        ValueMap props = underTest.getValueMap();
+        
+        assertEquals("Profiles", props.get("jcr:title", String.class));
+        assertEquals(true, props.get("booleanProp", false));
+        assertEquals((Long)1234567890123L, props.get("longProp", Long.class));
+        assertEquals((Double)1.2345d, props.get("decimalProp", Double.class), 0.00001d);
+        
+        assertArrayEquals(new String[] { "aa", "bb", "cc" }, props.get("stringPropMulti", String[].class));
+        assertArrayEquals(new Long[] { 1234567890123L, 55L }, props.get("longPropMulti", Long[].class));
+    }
+
+    @Test
+    public void testJsonContent_Datatypes_JCR() throws RepositoryException {
+        Resource underTest = fsroot.getChild("folder2/content/toolbar/profiles/jcr:content");
+        ValueMap props = underTest.getValueMap();
+        Node node = underTest.adaptTo(Node.class);
+        
+        assertEquals("/fs-test/folder2/content/toolbar/profiles/jcr:content", node.getPath());
+        assertEquals(6, node.getDepth());
+        assertTrue(node.isNodeType("app:PageContent"));
+        
+        assertTrue(node.hasProperty("jcr:title"));
+        assertEquals(PropertyType.STRING, node.getProperty("jcr:title").getType());
+        assertFalse(node.getProperty("jcr:title").isMultiple());
+        assertEquals("/fs-test/folder2/content/toolbar/profiles/jcr:content/jcr:title", node.getProperty("jcr:title").getPath());
+        assertEquals("Profiles", node.getProperty("jcr:title").getString());
+        assertEquals(PropertyType.BOOLEAN, node.getProperty("booleanProp").getType());
+        assertEquals(true, node.getProperty("booleanProp").getBoolean());
+        assertEquals(PropertyType.LONG, node.getProperty("longProp").getType());
+        assertEquals(1234567890123L, node.getProperty("longProp").getLong());
+        assertEquals(PropertyType.DOUBLE, node.getProperty("decimalProp").getType());
+        assertEquals(1.2345d, node.getProperty("decimalProp").getDouble(), 0.00001d);
+        
+        assertEquals(PropertyType.STRING, node.getProperty("stringPropMulti").getType());
+        assertTrue(node.getProperty("stringPropMulti").isMultiple());
+        Value[] stringPropMultiValues = node.getProperty("stringPropMulti").getValues();
+        assertEquals(3, stringPropMultiValues.length);
+        assertEquals("aa", stringPropMultiValues[0].getString());
+        assertEquals("bb", stringPropMultiValues[1].getString());
+        assertEquals("cc", stringPropMultiValues[2].getString());
+
+        assertEquals(PropertyType.LONG, node.getProperty("longPropMulti").getType());
+        assertTrue(node.getProperty("longPropMulti").isMultiple());
+        Value[] longPropMultiValues = node.getProperty("longPropMulti").getValues();
+        assertEquals(2, longPropMultiValues.length);
+        assertEquals(1234567890123L, longPropMultiValues[0].getLong());
+        assertEquals(55L, longPropMultiValues[1].getLong());
+        
+        // assert property iterator
+        Set<String> propertyNames = new HashSet<>();
+        PropertyIterator propertyIterator = node.getProperties();
+        while (propertyIterator.hasNext()) {
+            propertyNames.add(propertyIterator.nextProperty().getName());
+        }
+        assertTrue(props.keySet().containsAll(propertyNames));
+
+        // assert node iterator
+        Set<String> nodeNames = new HashSet<>();
+        NodeIterator nodeIterator = node.getNodes();
+        while (nodeIterator.hasNext()) {
+            nodeNames.add(nodeIterator.nextNode().getName());
+        }
+        assertEquals(ImmutableSet.of("par", "rightpar"), nodeNames);
+        
+        // node hierarchy
+        assertTrue(node.hasNode("rightpar"));
+        Node rightpar = node.getNode("rightpar");
+        assertEquals(7, rightpar.getDepth());
+        Node parent = rightpar.getParent();
+        assertTrue(node.isSame(parent));
+        Node ancestor = (Node)rightpar.getAncestor(4);
+        assertEquals(underTest.getParent().getPath(), ancestor.getPath());        
+    }
+
+    @Test
+    public void testJsonContent_InvalidPath() {
+        Resource underTest = fsroot.getChild("folder2/content/jcr:content/xyz");
+        assertNull(underTest);
+    }
+
+    @Test
+    public void testJcrMixedContent() throws RepositoryException {
+        // prepare mixed JCR content
+        Node node = root.adaptTo(Node.class);
+        Node fstest = node.addNode("fs-test", "nt:folder");
+        fstest.addNode("folder3", "nt:folder");
+
+        assertNull(fsroot.getChild("folder3"));
+    }
+
+    @Test
+    public void testFolder2ChildNodes() throws RepositoryException {
+        Resource folder2 = fsroot.getChild("folder2");
+        List<Resource> children = ImmutableList.copyOf(folder2.listChildren());
+        
+        assertEquals(2, children.size());
+        Resource child1 = children.get(0);
+        assertEquals("content", child1.getName());
+        assertEquals("app:Page", child1.getResourceType());
+        assertEquals("app:Page", child1.getValueMap().get("jcr:primaryType", String.class));
+
+        Resource child2 = children.get(1);
+        assertEquals("folder21", child2.getName());
+        assertEquals("nt:folder", child2.getValueMap().get("jcr:primaryType", String.class));
+    }
+
+}
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 8d9785e..9ee6162 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/TestUtils.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/TestUtils.java
@@ -28,25 +28,33 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.HashMap;
+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.hamcrest.ResourceMatchers;
+import org.apache.sling.testing.mock.osgi.MapUtil;
 import org.apache.sling.testing.mock.osgi.context.AbstractContextPlugin;
-import org.apache.sling.testing.mock.osgi.context.ContextPlugin;
 import org.apache.sling.testing.mock.sling.context.SlingContextImpl;
 
 class TestUtils {
 
-    public static ContextPlugin<SlingContextImpl> REGISTER_FSRESOURCE_PLUGIN = new AbstractContextPlugin<SlingContextImpl>() {
+    public static class RegisterFsResourcePlugin extends AbstractContextPlugin<SlingContextImpl> {
+        private final Map<String,Object> props;
+        public RegisterFsResourcePlugin(Object... props) {
+            this.props = MapUtil.toMap(props); 
+        }
         @Override
         public void beforeSetUp(SlingContextImpl context) throws Exception {
-            context.registerInjectActivateService(new FsResourceProvider(),
-                    "provider.file", "src/test/resources/fs-test",
-                    "provider.root", "/fs-test",
-                    "provider.checkinterval", 0);
+            Map<String,Object> config = new HashMap<>();
+            config.put("provider.file", "src/test/resources/fs-test");
+            config.put("provider.root", "/fs-test");
+            config.put("provider.checkinterval", 0);
+            config.putAll(props);
+            context.registerInjectActivateService(new FsResourceProvider(), config);
         }
     };
 
@@ -69,19 +77,21 @@ class TestUtils {
         assertThat(file, ResourceMatchers.props("jcr:primaryType", "nt:file"));
         assertEquals("nt:file", file.getResourceType());
         
-        try {
-            try (InputStream is = file.adaptTo(InputStream.class)) {
-                String data = IOUtils.toString(is, CharEncoding.UTF_8);
-                assertEquals(content, data);
-            }
-        }
-        catch (IOException ex) {
-            throw new RuntimeException(ex);
-        }
-
         assertNull(file.getResourceSuperType());
         assertEquals(file.getName(), file.adaptTo(File.class).getName());
         assertTrue(StringUtils.contains(file.adaptTo(URL.class).toString(), file.getName()));
+        
+        if (content != null) {
+            try {
+                try (InputStream is = file.adaptTo(InputStream.class)) {
+                    String data = IOUtils.toString(is, CharEncoding.UTF_8);
+                    assertEquals(content, data);
+                }
+            }
+            catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
     }    
 
 }
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/mapper/ContentFileTest.java b/src/test/java/org/apache/sling/fsprovider/internal/mapper/ContentFileTest.java
new file mode 100644
index 0000000..b3c18ee
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/mapper/ContentFileTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.junit.Test;
+
+public class ContentFileTest {
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testRootContent() {
+        File file = new File("src/test/resources/fs-test/folder2/content.json");
+        
+        ContentFile underTest = new ContentFile(file, null);
+        assertEquals(file, underTest.getFile());
+        assertNull(underTest.getSubPath());
+        
+        assertTrue(underTest.hasContent());
+
+        Map<String,Object> content = (Map<String,Object>)underTest.getContent();
+        assertEquals("app:Page", content.get("jcr:primaryType"));
+        assertEquals("app:PageContent", ((Map<String,Object>)content.get("jcr:content")).get("jcr:primaryType"));
+
+        ValueMap props = underTest.getValueMap();
+        assertEquals("app:Page", props.get("jcr:primaryType"));
+        assertNull(props.get("jcr:content"));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testContentLevel1() {
+        File file = new File("src/test/resources/fs-test/folder2/content.json");
+        
+        ContentFile underTest = new ContentFile(file, "jcr:content");
+        assertEquals(file, underTest.getFile());
+        assertEquals("jcr:content", underTest.getSubPath());
+        
+        assertTrue(underTest.hasContent());
+
+        Map<String,Object> content = (Map<String,Object>)underTest.getContent();
+        assertEquals("app:PageContent", content.get("jcr:primaryType"));
+
+        ValueMap props = underTest.getValueMap();
+        assertEquals("app:PageContent", props.get("jcr:primaryType"));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testContentLevel5() {
+        File file = new File("src/test/resources/fs-test/folder2/content.json");
+        
+        ContentFile underTest = new ContentFile(file, "jcr:content/par/image/file/jcr:content");
+        assertEquals(file, underTest.getFile());
+        assertEquals("jcr:content/par/image/file/jcr:content", underTest.getSubPath());
+        
+        assertTrue(underTest.hasContent());
+
+        Map<String,Object> content = (Map<String,Object>)underTest.getContent();
+        assertEquals("nt:resource", content.get("jcr:primaryType"));
+
+        ValueMap props = underTest.getValueMap();
+        assertEquals("nt:resource", props.get("jcr:primaryType"));
+    }
+
+    @Test
+    public void testContentProperty() {
+        File file = new File("src/test/resources/fs-test/folder2/content.json");
+        
+        ContentFile underTest = new ContentFile(file, "jcr:content/jcr:title");
+        assertEquals(file, underTest.getFile());
+        assertEquals("jcr:content/jcr:title", underTest.getSubPath());
+        
+        assertTrue(underTest.hasContent());
+
+        assertEquals("English", underTest.getContent());
+
+        assertTrue(underTest.getValueMap().isEmpty());
+    }
+
+    @Test
+    public void testInvalidFile() {
+        File file = new File("src/test/resources/fs-test/folder1/file1a.txt");
+        ContentFile underTest = new ContentFile(file, null);
+        assertFalse(underTest.hasContent());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtilTest.java b/src/test/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtilTest.java
new file mode 100644
index 0000000..71d6a38
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtilTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class ValueMapUtilTest {
+
+    @Test
+    public void testToValueMap() {
+        Map<String,Object> content = new HashMap<>();
+        content.put("stringProp", "abc");
+        content.put("intProp", 123);
+        content.put("childNode", ImmutableMap.<String,Object>of());
+        content.put("stringArray", new String[] { "a", "b", "c" });
+        content.put("stringList", ImmutableList.of("ab", "cd"));
+        content.put("intList", ImmutableList.of(12, 34));
+        
+        ValueMap props = ValueMapUtil.toValueMap(content);
+        assertEquals("abc", props.get("stringProp", String.class));
+        assertEquals((Integer)123, props.get("intProp", 0));
+        assertNull(props.get("childNode"));
+        assertArrayEquals(new String[] { "a", "b", "c" }, props.get("stringArray", String[].class));
+        assertArrayEquals(new String[] { "ab", "cd" }, props.get("stringList", String[].class));
+        assertArrayEquals(new Integer[] { 12, 34 }, props.get("intList", Integer[].class));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserTest.java b/src/test/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserTest.java
new file mode 100644
index 0000000..0bc14a4
--- /dev/null
+++ b/src/test/java/org/apache/sling/fsprovider/internal/parser/ContentFileParserTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.File;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class ContentFileParserTest {
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testParseJson() {
+        File file = new File("src/test/resources/fs-test/folder2/content.json");
+        Map<String,Object> content = ContentFileParser.parse(file);
+        assertNotNull(content);
+        assertEquals("app:Page", content.get("jcr:primaryType"));
+        assertEquals("app:PageContent", ((Map<String,Object>)content.get("jcr:content")).get("jcr:primaryType"));
+    }
+
+    @Test
+    public void testParseInvalidJson() {
+        File file = new File("src/test/resources/fs-test/folder1/file1a.txt");
+        Map<String,Object> content = ContentFileParser.parse(file);
+        assertNull(content);
+    }
+
+}
diff --git a/src/test/resources/fs-test/folder2/content.json b/src/test/resources/fs-test/folder2/content.json
new file mode 100644
index 0000000..e808ef8
--- /dev/null
+++ b/src/test/resources/fs-test/folder2/content.json
@@ -0,0 +1,262 @@
+/* Comment example */
+{
+  "jcr:primaryType": "app:Page",
+  "jcr:createdBy": "admin",
+  "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+  /* Comment example */
+  "jcr:content": {
+    "jcr:primaryType": "app:PageContent",  /* Comment example */
+    "jcr:createdBy": "admin",
+    "jcr:title": "English",
+    "app:template": "/apps/sample/templates/homepage",
+    "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+    "app:lastModified": "Tue Apr 22 2014 15:11:24 GMT+0200",
+    "dateISO8601String": "2014-04-22T15:11:24.000+02:00",
+    "pageTitle": "Sample Homepage",
+    "sling:resourceType": "sample/components/homepage",
+    "sling:resourceSuperType": "sample/components/supertype",
+    "app:designPath": "/etc/designs/sample",
+    "app:lastModifiedBy": "admin",
+    "utf8Property": "äöü߀",
+    "par": {
+      "jcr:primaryType": "nt:unstructured",
+      "sling:resourceType": "foundation/components/parsys",
+      "colctrl": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "layout": "2;colctrl-lt0",
+        "jcr:created": "Mon Aug 23 2010 22:02:24 GMT+0200",
+        "jcr:lastModified": "Mon Aug 23 2010 22:02:35 GMT+0200",
+        "sling:resourceType": "foundation/components/parsys/colctrl"
+      },
+      "image": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "fileReference": "/content/dam/sample/portraits/jane_doe.jpg",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:03:39 GMT+0200",
+        "width": "340",
+        "jcr:lastModified": "Sun Oct 31 2010 21:39:50 GMT+0100",
+        "sling:resourceType": "foundation/components/image",
+        "file": {
+          "jcr:primaryType": "nt:file",
+          "jcr:createdBy": "admin",
+          "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+          "jcr:content": {
+            "jcr:primaryType": "nt:resource",
+            "jcr:lastModifiedBy": "anonymous",
+            "jcr:mimeType": "image/jpeg",
+            "jcr:lastModified": "Thu Aug 07 2014 16:32:59 GMT+0200",
+            ":jcr:data": 24377,
+            "jcr:uuid": "eda76d00-b2cd-4b59-878f-c33f71ceaddc"
+          }
+        }
+      },
+      "title_1": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:title": "Strategic Consulting",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:12:08 GMT+0200",
+        "jcr:lastModified": "Wed Oct 27 2010 21:33:24 GMT+0200",
+        "sling:resourceType": "sample/components/title"
+      },
+      "text_1": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Sun Oct 31 2010 21:48:04 GMT+0100",
+        "text": "<p><span class=\"Apple-style-span\" style=\"font-size: 12px;\">In&nbsp;today's competitive market, organizations can face several key geometric challenges:<\/span><\/p>\n<ul>\n<li><span class=\"Apple-style-span\" style=\"font-size: 12px;\">Polyhedral Sectioning<\/span><\/li>\n<li><span class=\"Apple-style-span\" style=\"font-size: 12px;\">Triangulation&nbsp;<\/span><\/li>\n<li><span class=\"Apple-style-span\" style=\"font-size: 12px;\">Trigonometric Calculation<\/span><\ [...]
+        "jcr:lastModified": "Sun Oct 31 2010 21:49:06 GMT+0100",
+        "sling:resourceType": "foundation/components/text",
+        "textIsRich": "true"
+      },
+      "col_break12825937554040": {
+        "jcr:primaryType": "nt:unstructured",
+        "controlType": "break",
+        "sling:resourceType": "foundation/components/parsys/colctrl"
+      },
+      "image_0": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "fileReference": "/content/dam/sample/offices/clean_room.jpg",
+        "height": "226",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:04:46 GMT+0200",
+        "jcr:lastModified": "Fri Nov 05 2010 10:38:15 GMT+0100",
+        "sling:resourceType": "foundation/components/image",
+        "imageRotate": "0",
+        "file": {
+          "jcr:primaryType": "nt:file",
+          "jcr:createdBy": "admin",
+          "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+          "jcr:content": {
+            "jcr:primaryType": "nt:resource",
+            "jcr:lastModifiedBy": "anonymous",
+            "jcr:mimeType": "image/jpeg",
+            "jcr:lastModified": "Thu Aug 07 2014 16:32:59 GMT+0200",
+            ":jcr:data": 21142,
+            "jcr:uuid": "6139077f-191f-4337-aaef-55456ebe6784"
+          }
+        }
+      },
+      "title_2": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:title": "Shape Technology",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:12:13 GMT+0200",
+        "jcr:lastModified": "Tue Oct 26 2010 21:16:29 GMT+0200",
+        "sling:resourceType": "sample/components/title"
+      },
+      "text_0": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:16:30 GMT+0200",
+        "text": "<p>The Sample investment in R&amp;D has done more than solidify our industry leadership role, we have now outpaced our competitors to such an extent that we are in an altogether new space.<\/p>\n<p>This is why our high quality polygons and polyhedra provide the only turnkey solutions across the whole range of euclidean geometry. And our mathematicians are working on the next generation of fractal curves to bring you shapes that are unthinkable today.<\/p>\n<p><\/p>\n<p>< [...]
+        "jcr:lastModified": "Mon Nov 08 2010 20:39:00 GMT+0100",
+        "sling:resourceType": "foundation/components/text",
+        "textIsRich": "true"
+      },
+      "col_end12825937444810": {
+        "jcr:primaryType": "nt:unstructured",
+        "controlType": "end",
+        "sling:resourceType": "foundation/components/parsys/colctrl"
+      }
+    },
+    "header": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:title": "trust our experience\r\nto manage your business",
+      "imageReference": "/content/dam/sample/header.png",
+      "text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc eget neque. Nunc condimentum ipsum et orci. Aenean est. Cras eget diam. read more",
+      "sling:resourceType": "sample/components/header"
+    },
+    "newslist": {
+      "jcr:primaryType": "nt:unstructured",
+      "headline": "trust our experience\nto manage your business",
+      "text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc eget neque. Nunc condimentum ipsum et orci. Aenean est. Cras eget diam. read more",
+      "sling:resourceType": "sample/components/listchildren",
+      "listroot": "/content/sample/en/about/news"
+    },
+    "lead": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:title": "World Leader in Applied Geometry ",
+      "jcr:lastModifiedBy": "admin",
+      "text": "Lead Text",
+      "title": "Lead Title",
+      "jcr:description": "Sample has been selling and servicing shapes for over 2000 years. From our beginnings as a small vendor of squares and rectangles we have grown our business into a leading global provider of platonic solids and fractals. Join us as we lead geometry into the future.",
+      "jcr:lastModified": "Wed Jan 19 2011 14:35:29 GMT+0100",
+      "sling:resourceType": "sample/components/lead",
+      "app:annotations": {"jcr:primaryType": "nt:unstructured"}
+    },
+    "image": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:lastModifiedBy": "admin",
+      "jcr:lastModified": "Wed Oct 27 2010 21:30:59 GMT+0200",
+      "imageRotate": "0"
+    },
+    "carousel": {
+      "jcr:primaryType": "nt:unstructured",
+      "playSpeed": "6000",
+      "jcr:lastModifiedBy": "admin",
+      "pages": [
+        "/content/sample/en/events/techsummit",
+        "/content/sample/en/events/userconf",
+        "/content/sample/en/events/shapecon",
+        "/content/sample/en/events/dsc"
+      ],
+      "jcr:lastModified": "Tue Oct 05 2010 14:14:27 GMT+0200",
+      "transTime": "1000",
+      "sling:resourceType": "foundation/components/carousel",
+      "listFrom": "static"
+    },
+    "rightpar": {
+      "jcr:primaryType": "nt:unstructured",
+      "sling:resourceType": "foundation/components/parsys",
+      "teaser": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Tue Jan 25 2011 11:30:09 GMT+0100",
+        "campaignpath": "/content/campaigns/sample",
+        "jcr:lastModified": "Wed Feb 02 2011 08:40:30 GMT+0100",
+        "sling:resourceType": "personalization/components/teaser"
+      }
+    }
+  },
+  "toolbar": {
+    "jcr:primaryType": "app:Page",
+    "jcr:createdBy": "admin",
+    "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+    "jcr:content": {
+      "jcr:primaryType": "app:PageContent",
+      "subtitle": "Contains the toolbar",
+      "jcr:createdBy": "admin",
+      "jcr:title": "Toolbar",
+      "app:template": "/apps/sample/templates/contentpage",
+      "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+      "app:lastModified": "Wed Aug 25 2010 22:51:02 GMT+0200",
+      "hideInNav": "true",
+      "sling:resourceType": "sample/components/contentpage",
+      "app:lastModifiedBy": "admin",
+      "par": {
+        "jcr:primaryType": "nt:unstructured",
+        "sling:resourceType": "foundation/components/parsys"
+      },
+      "rightpar": {
+        "jcr:primaryType": "nt:unstructured",
+        "sling:resourceType": "foundation/components/iparsys",
+        "iparsys_fake_par": {
+          "jcr:primaryType": "nt:unstructured",
+          "sling:resourceType": "foundation/components/iparsys/par"
+        }
+      }
+    },
+    "profiles": {
+      "jcr:primaryType": "app:Page",
+      "jcr:createdBy": "admin",
+      "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+      "jcr:content": {
+        "jcr:primaryType": "app:PageContent",
+        "jcr:createdBy": "admin",
+        "jcr:title": "Profiles",
+        "app:template": "/apps/sample/templates/contentpage",
+        "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+        "app:lastModified": "Thu Nov 05 2009 20:27:13 GMT+0100",
+        "hideInNav": true,
+        "sling:resourceType": "sample/components/contentpage",
+        "app:lastModifiedBy": "admin",
+        "longProp": 1234567890123,
+        "decimalProp": 1.2345,
+        "booleanProp": true,
+        "longPropMulti": [1234567890123,55],
+        "decimalPropMulti": [1.2345,1.1],
+        "booleanPropMulti": [true,false],
+        "stringPropMulti": ["aa","bb","cc"],
+        "par": {
+          "jcr:primaryType": "nt:unstructured",
+          "sling:resourceType": "foundation/components/parsys",
+          "textimage": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "foundation/components/textimage"
+          },
+          "mygadgets": {
+            "jcr:primaryType": "nt:unstructured",
+            "gadgets": "http://customer.meteogroup.de/meteogroup/gadgets/wetter24.xml\nhttp://germanweatherradar.googlecode.com/svn/trunk/german-weather-radar.xml\nhttp://www.digitalpowered.info/gadget/ski.pictures.xml\nhttp://www.canbuffi.de/gadgets/clock/clock.xml",
+            "sling:resourceType": "personalization/components/mygadgets"
+          }
+        },
+        "rightpar": {
+          "jcr:primaryType": "nt:unstructured",
+          "sling:resourceType": "foundation/components/iparsys",
+          "iparsys_fake_par": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "foundation/components/iparsys/par"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/resources/fs-test/folder2/content/content2.json b/src/test/resources/fs-test/folder2/content/content2.json
new file mode 100644
index 0000000..261509a
--- /dev/null
+++ b/src/test/resources/fs-test/folder2/content/content2.json
@@ -0,0 +1,4 @@
+{
+  "jcr:primaryType": "app:Page",
+  "jcr:createdBy": "admin"
+}
diff --git a/src/test/resources/fs-test/folder2/file2a.txt b/src/test/resources/fs-test/folder2/file2a.txt
deleted file mode 100644
index e2baf26..0000000
--- a/src/test/resources/fs-test/folder2/file2a.txt
+++ /dev/null
@@ -1 +0,0 @@
-file2a
\ No newline at end of file
diff --git a/src/test/resources/fs-test/folder2/folder21/file21a.txt b/src/test/resources/fs-test/folder2/folder21/file21a.txt
new file mode 100644
index 0000000..3d5becc
--- /dev/null
+++ b/src/test/resources/fs-test/folder2/folder21/file21a.txt
@@ -0,0 +1 @@
+file21a
\ No newline at end of file

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

Mime
View raw message