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 eadcbb2fe9cd27feb4e1e6049ef78e2eef4ff3da
Author: Stefan Seifert <[email protected]>
AuthorDate: Fri Feb 24 20:45:26 2017 +0000

    SLING-6440 further improvements for content file/json support:
    - enhance jcr api layer
    - make sure jcr primary type is always defined
    - send events for all nodes in content files
    - introduce simple LRU memory cache for parsed content files
    
    git-svn-id: 
https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/fsresource@1784324
 13f79535-47bb-0310-9956-ffa450edef68
---
 .../sling/fsprovider/internal/FileMonitor.java     | 130 +++++++++++++----
 .../fsprovider/internal/FsResourceProvider.java    |  25 +++-
 .../fsprovider/internal/mapper/ContentFile.java    |  26 +++-
 .../internal/mapper/ContentFileResource.java       |   7 +-
 .../internal/mapper/ContentFileResourceMapper.java |  19 +--
 .../fsprovider/internal/mapper/ValueMapUtil.java   |   9 ++
 .../fsprovider/internal/mapper/jcr/FsNode.java     |  35 +++--
 .../fsprovider/internal/mapper/jcr/FsNodeType.java | 157 +++++++++++++++++++++
 .../fsprovider/internal/mapper/jcr/FsProperty.java |  10 +-
 .../internal/mapper/jcr/FsPropertyDefinition.java  | 100 +++++++++++++
 .../internal/parser/ContentFileCache.java          | 107 ++++++++++++++
 .../internal/parser/ContentFileParser.java         |  20 ++-
 ...ontentFileParser.java => ContentFileTypes.java} |  25 +---
 .../sling/fsprovider/internal/FileMonitorTest.java |  20 ++-
 .../sling/fsprovider/internal/FilesFolderTest.java |   1 +
 .../sling/fsprovider/internal/JsonContentTest.java |  23 ++-
 .../internal/mapper/ContentFileTest.java           |  13 +-
 .../internal/parser/ContentFileCacheTest.java      |  92 ++++++++++++
 src/test/resources/fs-test/folder2/content.json    |   2 +-
 .../fs-test/folder2/content/file2content.txt       |   1 +
 20 files changed, 713 insertions(+), 109 deletions(-)

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 7249912..05d01b7 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
@@ -19,13 +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.List;
+import java.util.Map;
+import java.util.Set;
 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.fsprovider.internal.mapper.ContentFile;
+import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 import org.apache.sling.spi.resource.provider.ObservationReporter;
 import org.apache.sling.spi.resource.provider.ObserverConfiguration;
 import org.slf4j.Logger;
@@ -37,8 +43,7 @@ import org.slf4j.LoggerFactory;
  */
 public final class FileMonitor extends TimerTask {
 
-    /** The logger. */
-    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
 
     private final Timer timer = new Timer();
     private boolean stop = false;
@@ -49,18 +54,21 @@ public final class FileMonitor extends TimerTask {
     private final FsResourceProvider provider;
     
     private final ContentFileExtensions contentFileExtensions;
+    private final ContentFileCache contentFileCache;
 
     /**
      * 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, 
final ContentFileExtensions contentFileExtensions) {
+    public FileMonitor(final FsResourceProvider provider, final long interval,
+            final ContentFileExtensions contentFileExtensions, final 
ContentFileCache contentFileCache) {
         this.provider = provider;
         this.contentFileExtensions = contentFileExtensions;
+        this.contentFileCache = contentFileCache;
         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);
+        createStatus(this.root, contentFileExtensions, contentFileCache);
+        log.debug("Starting file monitor for {} with an interval of {}ms", 
this.root.file, interval);
         timer.schedule(this, 0, interval);
     }
 
@@ -90,7 +98,7 @@ public final class FileMonitor extends TimerTask {
                 Thread.currentThread().interrupt();
             }
         }
-        logger.debug("Stopped file monitor for {}", this.root.file);
+        log.debug("Stopped file monitor for {}", this.root.file);
     }
 
     /**
@@ -129,24 +137,21 @@ public final class FileMonitor extends TimerTask {
      * @param reporter The ObservationReporter
      */
     private void check(final Monitorable monitorable, final 
ObservationReporter reporter) {
-        logger.debug("Checking {}", monitorable.file);
+        log.trace("Checking {}", monitorable.file);
         // if the file is non existing, check if it has been readded
         if ( monitorable.status instanceof NonExistingStatus ) {
             if ( monitorable.file.exists() ) {
                 // new file and reset status
-                createStatus(monitorable, contentFileExtensions);
-                sendEvents(monitorable,
-                           ChangeType.ADDED,
-                           reporter);
+                createStatus(monitorable, contentFileExtensions, 
contentFileCache);
+                sendEvents(monitorable, ChangeType.ADDED, reporter);
             }
         } else {
             // check if the file has been removed
             if ( !monitorable.file.exists() ) {
                 // removed file and update status
-                sendEvents(monitorable,
-                           ChangeType.REMOVED,
-                           reporter);
+                sendEvents(monitorable, ChangeType.REMOVED, reporter);
                 monitorable.status = NonExistingStatus.SINGLETON;
+                contentFileCache.remove(monitorable.path);
             } else {
                 // check for changes
                 final FileStatus fs = (FileStatus)monitorable.status;
@@ -154,10 +159,9 @@ public final class FileMonitor extends TimerTask {
                 if ( fs.lastModified < monitorable.file.lastModified() ) {
                     fs.lastModified = monitorable.file.lastModified();
                     // changed
-                    sendEvents(monitorable,
-                               ChangeType.CHANGED,
-                               reporter);
+                    sendEvents(monitorable, ChangeType.CHANGED, reporter);
                     changed = true;
+                    contentFileCache.remove(monitorable.path);
                 }
                 if ( fs instanceof DirStatus ) {
                     // directory
@@ -200,28 +204,86 @@ public final class FileMonitor extends TimerTask {
      * Send the event async via the event admin.
      */
     private void sendEvents(final Monitorable monitorable, final ChangeType 
changeType, final ObservationReporter reporter) {
-        if ( logger.isDebugEnabled() ) {
-            logger.debug("Detected change for resource {} : {}", 
monitorable.path, changeType);
+        if (log.isDebugEnabled()) {
+            log.debug("Detected change for resource {} : {}", 
monitorable.path, changeType);
         }
 
-        for(final ObserverConfiguration config : 
reporter.getObserverConfigurations()) {
+        for (final ObserverConfiguration config : 
reporter.getObserverConfigurations()) {
             if ( config.matches(monitorable.path) ) {
-                final ResourceChange change = new ResourceChange(changeType, 
monitorable.path, false, null, null, null);
-                reporter.reportChanges(Collections.singleton(change), false);
+                List<ResourceChange> changes = 
collectResourceChanges(monitorable, changeType);
+                if (log.isTraceEnabled()) {
+                    for (ResourceChange change : changes) {
+                        log.debug("Send change for resource {}: {} to {}", 
change.getPath(), change.getType(), config);
+                    }
+                }
+                reporter.reportChanges(changes, false);
+            }
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    private List<ResourceChange> collectResourceChanges(final Monitorable 
monitorable, final ChangeType changeType) {
+        List<ResourceChange> changes = new ArrayList<>();
+        if (monitorable.status instanceof ContentFileStatus) {
+            ContentFile contentFile = 
((ContentFileStatus)monitorable.status).contentFile;
+            if (changeType == ChangeType.CHANGED) {
+                Map<String,Object> content = 
(Map<String,Object>)contentFile.getContent();
+                // we cannot easily report the diff of resource changes 
between two content files
+                // so we simulate a removal of the toplevel node and then add 
all nodes contained in the current content file again.
+                changes.add(buildContentResourceChange(ChangeType.REMOVED, 
content, monitorable.path));
+                addContentResourceChanges(changes, ChangeType.ADDED, content, 
monitorable.path);
+            }
+            else {
+                addContentResourceChanges(changes, changeType, 
(Map<String,Object>)contentFile.getContent(), monitorable.path);
+            }
+        }
+        else {
+            changes.add(new ResourceChange(changeType, monitorable.path, 
false, null, null, null));
+        }
+        return changes;
+    }
+    @SuppressWarnings("unchecked")
+    private void addContentResourceChanges(final List<ResourceChange> changes, 
final ChangeType changeType,
+            final Map<String,Object> content, final String path) {
+        changes.add(buildContentResourceChange(changeType, content, path));
+        if (content != null) {
+            for (Map.Entry<String,Object> entry : content.entrySet()) {
+                if (entry.getValue() instanceof Map) {
+                    String childPath = path + "/" + entry.getKey();
+                    addContentResourceChanges(changes, changeType, 
(Map<String,Object>)entry.getValue(), childPath);
+                }
             }
         }
     }
+    private ResourceChange buildContentResourceChange(final ChangeType 
changeType, final Map<String,Object> content, final String path) {
+        Set<String> addedPropertyNames = null;
+        if (content != null && changeType == ChangeType.ADDED) {
+            addedPropertyNames = new HashSet<>();
+            for (Map.Entry<String,Object> entry : content.entrySet()) {
+                if (!(entry.getValue() instanceof Map)) {
+                    addedPropertyNames.add(entry.getKey());
+                }
+            }
+        }
+        return new ResourceChange(changeType, path, false, addedPropertyNames, 
null, null);
+    }
 
     /**
      * Create a status object for the monitorable
      */
-    private static void createStatus(final Monitorable monitorable, 
ContentFileExtensions contentFileExtensions) {
+    private static void createStatus(final Monitorable monitorable, 
ContentFileExtensions contentFileExtensions, ContentFileCache contentFileCache) 
{
         if ( !monitorable.file.exists() ) {
             monitorable.status = NonExistingStatus.SINGLETON;
         } else if ( monitorable.file.isFile() ) {
-            monitorable.status = new FileStatus(monitorable.file);
+            if (contentFileExtensions.matchesSuffix(monitorable.file)) {
+                monitorable.status = new ContentFileStatus(monitorable.file,
+                        new ContentFile(monitorable.file, monitorable.path, 
null, contentFileCache));
+            }
+            else {
+                monitorable.status = new FileStatus(monitorable.file);
+            }
         } else {
-            monitorable.status = new DirStatus(monitorable.file, 
monitorable.path, contentFileExtensions);
+            monitorable.status = new DirStatus(monitorable.file, 
monitorable.path, contentFileExtensions, contentFileCache);
         }
     }
 
@@ -248,12 +310,22 @@ public final class FileMonitor extends TimerTask {
             this.lastModified = file.lastModified();
         }
     }
-
+    
+    /** Status for content files */
+    private static class ContentFileStatus extends FileStatus {
+        public final ContentFile contentFile;
+        public ContentFileStatus(final File file, final ContentFile 
contentFile) {
+            super(file);
+            this.contentFile = contentFile;
+        }
+    }
+    
     /** Status for directories. */
     private static final class DirStatus extends FileStatus {
         public Monitorable[] children;
 
-        public DirStatus(final File dir, final String path, final 
ContentFileExtensions contentFileExtensions) {
+        public DirStatus(final File dir, final String path,
+                final ContentFileExtensions contentFileExtensions, final 
ContentFileCache contentFileCache) {
             super(dir);
             final File[] files = dir.listFiles();
             if (files != null) {
@@ -261,7 +333,7 @@ public final class FileMonitor extends TimerTask {
                 for (int i = 0; i < files.length; i++) {
                     this.children[i] = new Monitorable(path + '/' + 
files[i].getName(), files[i],
                             contentFileExtensions.getSuffix(files[i]));
-                    FileMonitor.createStatus(this.children[i], 
contentFileExtensions);
+                    FileMonitor.createStatus(this.children[i], 
contentFileExtensions, contentFileCache);
                 }
             } else {
                 this.children = new Monitorable[0];
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 f379bad..5d1ca9b 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
@@ -31,7 +31,8 @@ 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.fsprovider.internal.parser.ContentFileCache;
+import org.apache.sling.fsprovider.internal.parser.ContentFileTypes;
 import org.apache.sling.spi.resource.provider.ObservationReporter;
 import org.apache.sling.spi.resource.provider.ProviderContext;
 import org.apache.sling.spi.resource.provider.ResolveContext;
@@ -108,7 +109,11 @@ public final class FsResourceProvider extends 
ResourceProvider<Object> {
         @AttributeDefinition(name = "Mount JSON",
                 description = "Mount .json files as content in the resource 
hierarchy.")
         boolean provider_json_content();
-        
+
+        @AttributeDefinition(name = "Cache Size",
+                description = "Max. number of content files cached in memory.")
+        int provider_cache_size() default 1000;
+
         /**
          * Internal Name hint for web console.
          */
@@ -130,6 +135,9 @@ public final class FsResourceProvider extends 
ResourceProvider<Object> {
     
     // if true resources from filesystem are only "overlayed" to JCR 
resources, serving JCR as fallback within the same path
     private boolean overlayParentResourceProvider;
+    
+    // cache for parsed content files
+    private ContentFileCache contentFileCache;
 
     /**
      * Returns a resource wrapping a file system file or folder for the given
@@ -243,17 +251,20 @@ public final class FsResourceProvider extends 
ResourceProvider<Object> {
         
         List<String> contentFileSuffixes = new ArrayList<>();
         if (config.provider_json_content()) {
-            contentFileSuffixes.add(ContentFileParser.JSON_SUFFIX);
+            contentFileSuffixes.add(ContentFileTypes.JSON_SUFFIX);
             this.overlayParentResourceProvider = false;
         }
         ContentFileExtensions contentFileExtensions = new 
ContentFileExtensions(contentFileSuffixes);
         
+        this.contentFileCache = new 
ContentFileCache(config.provider_cache_size());
         this.fileMapper = new FileResourceMapper(this.providerRoot, 
this.providerFile, contentFileExtensions);
-        this.contentFileMapper = new 
ContentFileResourceMapper(this.providerRoot, this.providerFile, 
contentFileExtensions);
+        this.contentFileMapper = new 
ContentFileResourceMapper(this.providerRoot, this.providerFile,
+                contentFileExtensions, this.contentFileCache);
         
         // start background monitor if check interval is higher than 100
         if ( config.provider_checkinterval() > 100 ) {
-            this.monitor = new FileMonitor(this, 
config.provider_checkinterval(), contentFileExtensions);
+            this.monitor = new FileMonitor(this, 
config.provider_checkinterval(),
+                    contentFileExtensions, this.contentFileCache);
         }
     }
 
@@ -268,6 +279,10 @@ public final class FsResourceProvider extends 
ResourceProvider<Object> {
         this.overlayParentResourceProvider = false;
         this.fileMapper = null;
         this.contentFileMapper = null;
+        if (this.contentFileCache != null) {
+            this.contentFileCache.clear();
+            this.contentFileCache = null;
+        }
     }
 
     File getRootFile() {
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
index 319a3de..2e60b1f 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFile.java
@@ -22,7 +22,7 @@ import java.io.File;
 import java.util.Map;
 
 import org.apache.sling.api.resource.ValueMap;
-import org.apache.sling.fsprovider.internal.parser.ContentFileParser;
+import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 
 /**
  * Reference to a file that contains a content fragment (e.g. JSON, JCR XML).
@@ -30,28 +30,35 @@ import 
org.apache.sling.fsprovider.internal.parser.ContentFileParser;
 public final class ContentFile {
     
     private final File file;
+    private final String path;
     private final String subPath;
+    private final ContentFileCache contentFileCache;
     private boolean contentInitialized;
     private Object content;
     private ValueMap valueMap;
     
     /**
      * @param file File with content fragment
+     * @param path Root path of the content file
      * @param subPath Relative path addressing content fragment inside file
+     * @param contentFileCache Content file cache
      */
-    public ContentFile(File file, String subPath) {
+    public ContentFile(File file, String path, String subPath, 
ContentFileCache contentFileCache) {
         this.file = file;
+        this.path = path;
         this.subPath = subPath;
+        this.contentFileCache = contentFileCache;
     }
 
     /**
      * @param file File with content fragment
+     * @param path Root path of the content file
      * @param subPath Relative path addressing content fragment inside file
+     * @param contentFileCache Content file cache
      * @param content Content
      */
-    public ContentFile(File file, String subPath, Object content) {
-        this.file = file;
-        this.subPath = subPath;
+    public ContentFile(File file, String path, String subPath, 
ContentFileCache contentFileCache, Object content) {
+        this(file, path, subPath, contentFileCache);
         this.contentInitialized = true;
         this.content = content;
     }
@@ -62,6 +69,13 @@ public final class ContentFile {
     public File getFile() {
         return file;
     }
+    
+    /**
+     * @return Root path of content file
+     */
+    public String getPath() {
+        return path;
+    }
 
     /**
      * @return Relative path addressing content fragment inside file
@@ -76,7 +90,7 @@ public final class ContentFile {
      */
     public Object getContent() {
         if (!contentInitialized) {
-            Map<String,Object> rootContent = ContentFileParser.parse(file);
+            Map<String,Object> rootContent = contentFileCache.get(path, file);
             content = getDeepContent(rootContent, subPath);
             contentInitialized = true;
         }
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
index 11bfcbb..410419e 100644
--- 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResource.java
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResource.java
@@ -54,10 +54,11 @@ public final class ContentFileResource extends 
AbstractResource {
      * @param resourcePath The resource path in the resource tree
      * @param contentFile Content file with sub path
      */
-    ContentFileResource(ResourceResolver resolver, String resourcePath, 
ContentFile contentFile) {
+    ContentFileResource(ResourceResolver resolver, ContentFile contentFile) {
         this.resolver = resolver;
-        this.resourcePath = resourcePath;
         this.contentFile = contentFile;
+        this.resourcePath = contentFile.getPath()
+                + (contentFile.getSubPath() != null ? "/" + 
contentFile.getSubPath() : "");
     }
 
     public String getPath() {
@@ -79,7 +80,7 @@ public final class ContentFileResource extends 
AbstractResource {
 
     public String getResourceSuperType() {
         if (resourceSuperType == null) {
-            resourceSuperType = 
contentFile.getValueMap().get("sling:resourceSuperType", String.class);
+            resourceSuperType = getValueMap().get("sling:resourceSuperType", 
String.class);
         }
         return resourceSuperType;
     }
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
index 1fe257c..b5306b9 100644
--- 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ContentFileResourceMapper.java
@@ -32,6 +32,7 @@ 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;
+import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 
 public final class ContentFileResourceMapper implements FsResourceMapper {
     
@@ -42,18 +43,21 @@ public final class ContentFileResourceMapper implements 
FsResourceMapper {
     private final File providerFile;
     
     private final ContentFileExtensions contentFileExtensions;
+    private final ContentFileCache contentFileCache;
     
-    public ContentFileResourceMapper(String providerRoot, File providerFile, 
ContentFileExtensions contentFileExtensions) {
+    public ContentFileResourceMapper(String providerRoot, File providerFile,
+            ContentFileExtensions contentFileExtensions, ContentFileCache 
contentFileCache) {
         this.providerRootPrefix = providerRoot.concat("/");
         this.providerFile = providerFile;
         this.contentFileExtensions = contentFileExtensions;
+        this.contentFileCache = contentFileCache;
     }
     
     @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);
+            return new ContentFileResource(resolver, contentFile);
         }
         else {
             return null;
@@ -78,9 +82,9 @@ public final class ContentFileResourceMapper implements 
FsResourceMapper {
                     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));
+                            ContentFile contentFile = new ContentFile(file, 
path, null, contentFileCache);
+                            childResources.add(new 
ContentFileResource(resolver, contentFile));
                         }
                     }
                     if (!childResources.isEmpty()) {
@@ -106,7 +110,7 @@ public final class ContentFileResourceMapper implements 
FsResourceMapper {
                     else {
                         subPath = parentContentFile.getSubPath() + "/" + 
entry.getKey();
                     }
-                    children.add(new ContentFile(parentContentFile.getFile(), 
subPath, entry.getValue()));
+                    children.add(new ContentFile(parentContentFile.getFile(), 
parentContentFile.getPath(), subPath, contentFileCache, entry.getValue()));
                 }
             }
         }
@@ -118,8 +122,7 @@ public final class ContentFileResourceMapper implements 
FsResourceMapper {
                 @Override
                 public Object transform(Object input) {
                     ContentFile contentFile = (ContentFile)input;
-                    String path = parentPath + "/" + 
ResourceUtil.getName(contentFile.getSubPath());
-                    return new ContentFileResource(resolver, path, 
contentFile);
+                    return new ContentFileResource(resolver, contentFile);
                 }
             });
         }
@@ -133,7 +136,7 @@ public final class ContentFileResourceMapper implements 
FsResourceMapper {
         for (String filenameSuffix : contentFileExtensions.getSuffixes()) {
             File file = new File(providerFile, relPath + filenameSuffix);
             if (file.exists()) {
-                return new ContentFile(file, subPath);
+                return new ContentFile(file, path, subPath, contentFileCache);
             }
         }
         // try to find in parent path which contains content fragment
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
index 67122b1..8ddbda7 100644
--- 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtil.java
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/ValueMapUtil.java
@@ -22,6 +22,8 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
+import javax.jcr.nodetype.NodeType;
+
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.api.wrappers.ValueMapDecorator;
 
@@ -38,6 +40,7 @@ final class ValueMapUtil {
      */
     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
@@ -51,6 +54,12 @@ final class ValueMapUtil {
                 props.put(entry.getKey(), entry.getValue());
             }
         }
+        
+        // fallback to default jcr:primaryType is none is set
+        if (!props.containsKey("jcr:primaryType")) {
+            props.put("jcr:primaryType", NodeType.NT_UNSTRUCTURED);
+        }
+        
         return new ValueMapDecorator(props);
     }
 
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
index 308701c..1d4b842 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNode.java
@@ -63,6 +63,14 @@ public final class FsNode extends FsItem implements Node {
         super(resource);
     }
     
+    private String getPrimaryTypeName() {
+        return  props.get("jcr:primaryType", String.class);
+    }
+    
+    private String[] getMixinTypeNames() {
+        return props.get("jcr:mixinTypes", new String[0]);
+    }
+    
     @Override
     public Node getNode(String relPath) throws PathNotFoundException, 
RepositoryException {
         Resource child = resource.getChild(relPath);
@@ -123,7 +131,7 @@ public final class FsNode extends FsItem implements Node {
 
     @Override
     public boolean isNodeType(String nodeTypeName) throws RepositoryException {
-        return StringUtils.equals(nodeTypeName, props.get("jcr:primaryType", 
String.class));
+        return StringUtils.equals(nodeTypeName, getPrimaryTypeName());
     }
 
     @Override
@@ -146,6 +154,21 @@ public final class FsNode extends FsItem implements Node {
         return false;
     }
 
+    @Override
+    public NodeType getPrimaryNodeType() throws RepositoryException {
+        return new FsNodeType(getPrimaryTypeName(), false);
+    }
+
+    @Override
+    public NodeType[] getMixinNodeTypes() throws RepositoryException {
+        String[] mixinTypeNames = getMixinTypeNames();
+        NodeType[] mixinTypes = new NodeType[mixinTypeNames.length];
+        for (int i=0; i<mixinTypeNames.length; i++) {
+            mixinTypes[i] = new FsNodeType(mixinTypeNames[i], true);
+        }
+        return mixinTypes;
+    }
+    
 
     // --- unsupported methods ---
     
@@ -444,11 +467,6 @@ public final class FsNode extends FsItem implements Node {
     }
 
     @Override
-    public NodeType getPrimaryNodeType() throws RepositoryException {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
     public String getIdentifier() throws RepositoryException {
         throw new UnsupportedOperationException();
     }
@@ -459,11 +477,6 @@ public final class FsNode extends FsItem implements Node {
     }
 
     @Override
-    public NodeType[] getMixinNodeTypes() throws RepositoryException {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
     public Item getPrimaryItem() throws ItemNotFoundException, 
RepositoryException {
         throw new UnsupportedOperationException();
     }
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeType.java 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeType.java
new file mode 100644
index 0000000..8d60812
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsNodeType.java
@@ -0,0 +1,157 @@
+/*
+ * 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.Value;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeIterator;
+import javax.jcr.nodetype.PropertyDefinition;
+
+import org.apache.commons.lang3.StringUtils;
+
+class FsNodeType implements NodeType {
+    
+    private final String name;
+    private final boolean mixin;
+    
+    public FsNodeType(String name, boolean mixin) {
+        this.name = name;
+        this.mixin = mixin;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String[] getDeclaredSupertypeNames() {
+        return new String[0];
+    }
+
+    @Override
+    public boolean isAbstract() {
+        return false;
+    }
+
+    @Override
+    public boolean isMixin() {
+        return mixin;
+    }
+
+    @Override
+    public boolean hasOrderableChildNodes() {
+        return false;
+    }
+
+    @Override
+    public boolean isQueryable() {
+        return false;
+    }
+
+    @Override
+    public String getPrimaryItemName() {
+        return null;
+    }
+
+    @Override
+    public NodeType[] getSupertypes() {
+        return new NodeType[0];
+    }
+
+    @Override
+    public NodeType[] getDeclaredSupertypes() {
+        return new NodeType[0];
+    }
+
+    @Override
+    public boolean isNodeType(String nodeTypeName) {
+        return StringUtils.equals(name, nodeTypeName);
+    }
+
+
+    // --- unsupported methods ---    
+    
+    @Override
+    public PropertyDefinition[] getDeclaredPropertyDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinition[] getDeclaredChildNodeDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator getSubtypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator getDeclaredSubtypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyDefinition[] getPropertyDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinition[] getChildNodeDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canSetProperty(String propertyName, Value value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canSetProperty(String propertyName, Value[] values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canAddChildNode(String childNodeName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canAddChildNode(String childNodeName, String nodeTypeName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canRemoveItem(String itemName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canRemoveNode(String nodeName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canRemoveProperty(String propertyName) {
+        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
index 4d1d138..469bd11 100644
--- 
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
@@ -143,7 +143,12 @@ class FsProperty extends FsItem implements Property {
         return getValue().getType();
     }
     
+    @Override
+    public PropertyDefinition getDefinition() throws RepositoryException {
+        return new FsPropertyDefinition(propertyName);
+    }
 
+    
     // --- unsupported methods ---
     
     @Override
@@ -219,11 +224,6 @@ class FsProperty extends FsItem implements Property {
     }
 
     @Override
-    public PropertyDefinition getDefinition() throws RepositoryException {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
     public Property getProperty() throws ItemNotFoundException, 
ValueFormatException, RepositoryException {
         throw new UnsupportedOperationException();
     }
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsPropertyDefinition.java
 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsPropertyDefinition.java
new file mode 100644
index 0000000..85920cb
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/jcr/FsPropertyDefinition.java
@@ -0,0 +1,100 @@
+/*
+ * 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.PropertyType;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
+import javax.jcr.version.OnParentVersionAction;
+
+class FsPropertyDefinition implements PropertyDefinition {
+    
+    private final String name;
+    
+    public FsPropertyDefinition(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public NodeType getDeclaringNodeType() {
+        return null;
+    }
+
+    @Override
+    public boolean isAutoCreated() {
+        return false;
+    }
+
+    @Override
+    public boolean isMandatory() {
+        return false;
+    }
+
+    @Override
+    public int getOnParentVersion() {
+        return OnParentVersionAction.COPY;
+    }
+
+    @Override
+    public boolean isProtected() {
+        return false;
+    }
+
+    @Override
+    public int getRequiredType() {
+        return PropertyType.UNDEFINED;
+    }
+
+    @Override
+    public String[] getValueConstraints() {
+        return new String[0];
+    }
+
+    @Override
+    public Value[] getDefaultValues() {
+        return new Value[0];
+    }
+
+    @Override
+    public boolean isMultiple() {
+        return false;
+    }
+
+    @Override
+    public String[] getAvailableQueryOperators() {
+        return new String[0];
+    }
+
+    @Override
+    public boolean isFullTextSearchable() {
+        return false;
+    }
+
+    @Override
+    public boolean isQueryOrderable() {
+        return false;
+    }    
+
+}
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileCache.java
 
b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileCache.java
new file mode 100644
index 0000000..289fb67
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileCache.java
@@ -0,0 +1,107 @@
+/*
+ * 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.Collections;
+import java.util.Map;
+
+import org.apache.commons.collections.map.LRUMap;
+
+/**
+ * Cache for parsed content from content files (e.g. JSON, JCR XML).
+ */
+public final class ContentFileCache {
+
+    private final Map<String,Map<String,Object>> contentCache;
+    private final Map<String,Object> NULL_MAP = Collections.emptyMap();
+    
+    /**
+     * @param maxSize Cache size. 0 = caching disabled.
+     */
+    @SuppressWarnings("unchecked")
+    public ContentFileCache(int maxSize) {
+        if (maxSize > 0) {
+            this.contentCache = Collections.synchronizedMap(new 
LRUMap(maxSize));
+        }
+        else {
+            this.contentCache = null;
+        }
+    }
+    
+    /**
+     * Get content.
+     * @param path Path (used as cache key).
+     * @param file File
+     * @return Content or null
+     */
+    public Map<String,Object> get(String path, File file) {
+        Map<String,Object> content = null;
+        if (contentCache != null) {
+            content = contentCache.get(path);
+        }
+        if (content == null) {
+            content = ContentFileParser.parse(file);
+            if (content == null) {
+                content = NULL_MAP;
+            }
+            if (contentCache != null) {
+                contentCache.put(path, content);
+            }
+        }
+        if (content == NULL_MAP) {
+            return null;
+        }
+        else {
+            return content;
+        }
+    }
+    
+    /**
+     * Remove content from cache.
+     * @param path Path (used as cache key)
+     */
+    public void remove(String path) {
+        if (contentCache != null) {
+            contentCache.remove(path);
+        }
+    }
+
+    /**
+     * Clear whole cache
+     */
+    public void clear() {
+        if (contentCache != null) {
+            contentCache.clear();
+        }
+    }
+    
+    /**
+     * @return Current cache size
+     */
+    public int size() {
+        if (contentCache != null) {
+            return contentCache.size();
+        }
+        else {
+            return 0;
+        }
+    }
+
+}
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
index 46d5b85..8f7a11f 100644
--- 
a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
@@ -18,20 +18,21 @@
  */
 package org.apache.sling.fsprovider.internal.parser;
 
+import static 
org.apache.sling.fsprovider.internal.parser.ContentFileTypes.JSON_SUFFIX;
+
 import java.io.File;
 import java.util.Map;
 
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Parses file that contains content fragments (e.g. JSON, JCR XML).
  */
-public final class ContentFileParser {
+class ContentFileParser {
     
-    /**
-     * JSON content files.
-     */
-    public static final String JSON_SUFFIX = ".json";
+    private static final Logger log = 
LoggerFactory.getLogger(ContentFileParser.class);
     
     private ContentFileParser() {
         // static methods only
@@ -43,8 +44,13 @@ public final class ContentFileParser {
      * @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);
+        try {
+            if (StringUtils.endsWith(file.getName(), JSON_SUFFIX)) {
+                return JsonFileParser.parse(file);
+            }
+        }
+        catch (Throwable ex) {
+            log.warn("Error parsing content from " + file.getPath(), ex);
         }
         return null;
     }
diff --git 
a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
 
b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileTypes.java
similarity index 62%
copy from 
src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
copy to 
src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileTypes.java
index 46d5b85..9d995e6 100644
--- 
a/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileParser.java
+++ 
b/src/main/java/org/apache/sling/fsprovider/internal/parser/ContentFileTypes.java
@@ -18,35 +18,18 @@
  */
 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).
+ * Content file types.
  */
-public final class ContentFileParser {
+public final class ContentFileTypes {
     
     /**
      * JSON content files.
      */
     public static final String JSON_SUFFIX = ".json";
-    
-    private ContentFileParser() {
+
+    private ContentFileTypes() {
         // 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/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java 
b/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
index a6f7568..30012ef 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FileMonitorTest.java
@@ -37,6 +37,8 @@ import 
org.apache.sling.testing.mock.sling.junit.SlingContextCallback;
 import org.junit.Rule;
 import org.junit.Test;
 
+import com.google.common.collect.ImmutableSet;
+
 /**
  * Test events when changing filesystem content.
  */
@@ -164,8 +166,10 @@ public class FileMonitorTest {
         
         Thread.sleep(250);
 
-        assertEquals(1, changes.size());
-        assertChange(changes, 0, "/fs-test/folder2/content", 
ChangeType.CHANGED);
+        assertTrue(changes.size() > 1);
+        assertChange(changes, 0, "/fs-test/folder2/content", 
ChangeType.REMOVED);
+        assertChange(changes, 1, "/fs-test/folder2/content", ChangeType.ADDED);
+        assertChange(changes, 2, "/fs-test/folder2/content/jcr:content", 
ChangeType.ADDED);
     }
     
     @Test
@@ -174,13 +178,14 @@ public class FileMonitorTest {
         assertTrue(changes.isEmpty());
         
         File file1c = new File(tempDir, "folder1/file1c.json");
-        FileUtils.write(file1c, "{'prop1':'value1'}");
+        FileUtils.write(file1c, 
"{\"prop1\":\"value1\",\"child1\":{\"prop2\":\"value1\"}}");
         
         Thread.sleep(250);
 
-        assertEquals(2, changes.size());
+        assertEquals(3, changes.size());
         assertChange(changes, 0, "/fs-test/folder1", ChangeType.CHANGED);
-        assertChange(changes, 1, "/fs-test/folder1/file1c", ChangeType.ADDED);
+        assertChange(changes, 1, "/fs-test/folder1/file1c", ChangeType.ADDED, 
"prop1");
+        assertChange(changes, 2, "/fs-test/folder1/file1c/child1", 
ChangeType.ADDED, "prop2");
     }
     
     @Test
@@ -199,10 +204,13 @@ public class FileMonitorTest {
     }
     
     
-    private void assertChange(List<ResourceChange> changes, int index, String 
path, ChangeType changeType) {
+    private void assertChange(List<ResourceChange> changes, int index, String 
path, ChangeType changeType, String... addedPropertyNames) {
         ResourceChange change = changes.get(index);
         assertEquals(path, change.getPath());
         assertEquals(changeType, change.getType());
+        if (addedPropertyNames.length > 0) {
+            assertEquals(ImmutableSet.copyOf(addedPropertyNames), 
change.getAddedPropertyNames());
+        }
     }
     
     static class ResourceListener implements ResourceChangeListener {
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 578befd..403592a 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/FilesFolderTest.java
@@ -65,6 +65,7 @@ public class FilesFolderTest {
         assertFile(fsroot, "folder1/file1b.txt", "file1b");
         assertFile(fsroot, "folder1/folder11/file11a.txt", "file11a");
         assertFile(fsroot, "folder2/content.json", null);
+        assertFile(fsroot, "folder2/content/file2content.txt", "file2content");
     }
 
     @Test
diff --git 
a/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java 
b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
index 1dcf7a4..34240a8 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
@@ -38,6 +38,7 @@ import javax.jcr.PropertyIterator;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
 import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
 
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ValueMap;
@@ -85,6 +86,7 @@ public class JsonContentTest {
         assertFile(fsroot, "folder1/file1b.txt", "file1b");
         assertFile(fsroot, "folder1/folder11/file11a.txt", "file11a");
         assertNull(fsroot.getChild("folder2/content.json"));
+        assertFile(fsroot, "folder2/content/file2content.txt", "file2content");
     }
 
     @Test
@@ -144,11 +146,11 @@ public class JsonContentTest {
         
         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("jcr:title", 
node.getProperty("jcr:title").getDefinition().getName());
         
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());
@@ -196,10 +198,27 @@ public class JsonContentTest {
         Node parent = rightpar.getParent();
         assertTrue(node.isSame(parent));
         Node ancestor = (Node)rightpar.getAncestor(4);
-        assertEquals(underTest.getParent().getPath(), ancestor.getPath());     
   
+        assertEquals(underTest.getParent().getPath(), ancestor.getPath());
+        
+        // node types
+        assertTrue(node.isNodeType("app:PageContent"));
+        assertEquals("app:PageContent", node.getPrimaryNodeType().getName());
+        assertFalse(node.getPrimaryNodeType().isMixin());
+        NodeType[] mixinTypes = node.getMixinNodeTypes();
+        assertEquals(2, mixinTypes.length);
+        assertEquals("type1", mixinTypes[0].getName());
+        assertEquals("type2", mixinTypes[1].getName());
+        assertTrue(mixinTypes[0].isMixin());
+        assertTrue(mixinTypes[1].isMixin());
     }
 
     @Test
+    public void testFallbackNodeType() throws RepositoryException {
+        Resource underTest = 
fsroot.getChild("folder2/content/jcr:content/par/title_2");
+        assertEquals(NodeType.NT_UNSTRUCTURED, 
underTest.adaptTo(Node.class).getPrimaryNodeType().getName());
+    }
+    
+    @Test
     public void testJsonContent_InvalidPath() {
         Resource underTest = 
fsroot.getChild("folder2/content/jcr:content/xyz");
         assertNull(underTest);
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
index b3c18ee..4659991 100644
--- 
a/src/test/java/org/apache/sling/fsprovider/internal/mapper/ContentFileTest.java
+++ 
b/src/test/java/org/apache/sling/fsprovider/internal/mapper/ContentFileTest.java
@@ -27,16 +27,19 @@ import java.io.File;
 import java.util.Map;
 
 import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 import org.junit.Test;
 
 public class ContentFileTest {
+    
+    private ContentFileCache contentFileCache = new ContentFileCache(0);
 
     @SuppressWarnings("unchecked")
     @Test
     public void testRootContent() {
         File file = new 
File("src/test/resources/fs-test/folder2/content.json");
         
-        ContentFile underTest = new ContentFile(file, null);
+        ContentFile underTest = new ContentFile(file, 
"/fs-test/folder2/content", null, contentFileCache);
         assertEquals(file, underTest.getFile());
         assertNull(underTest.getSubPath());
         
@@ -56,7 +59,7 @@ public class ContentFileTest {
     public void testContentLevel1() {
         File file = new 
File("src/test/resources/fs-test/folder2/content.json");
         
-        ContentFile underTest = new ContentFile(file, "jcr:content");
+        ContentFile underTest = new ContentFile(file, 
"/fs-test/folder2/content", "jcr:content", contentFileCache);
         assertEquals(file, underTest.getFile());
         assertEquals("jcr:content", underTest.getSubPath());
         
@@ -74,7 +77,7 @@ public class ContentFileTest {
     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");
+        ContentFile underTest = new ContentFile(file, 
"/fs-test/folder2/content", "jcr:content/par/image/file/jcr:content", 
contentFileCache);
         assertEquals(file, underTest.getFile());
         assertEquals("jcr:content/par/image/file/jcr:content", 
underTest.getSubPath());
         
@@ -91,7 +94,7 @@ public class ContentFileTest {
     public void testContentProperty() {
         File file = new 
File("src/test/resources/fs-test/folder2/content.json");
         
-        ContentFile underTest = new ContentFile(file, "jcr:content/jcr:title");
+        ContentFile underTest = new ContentFile(file, 
"/fs-test/folder2/content", "jcr:content/jcr:title", contentFileCache);
         assertEquals(file, underTest.getFile());
         assertEquals("jcr:content/jcr:title", underTest.getSubPath());
         
@@ -105,7 +108,7 @@ public class ContentFileTest {
     @Test
     public void testInvalidFile() {
         File file = new File("src/test/resources/fs-test/folder1/file1a.txt");
-        ContentFile underTest = new ContentFile(file, null);
+        ContentFile underTest = new ContentFile(file, 
"/fs-test/folder1/file1a", null, contentFileCache);
         assertFalse(underTest.hasContent());
     }
 
diff --git 
a/src/test/java/org/apache/sling/fsprovider/internal/parser/ContentFileCacheTest.java
 
b/src/test/java/org/apache/sling/fsprovider/internal/parser/ContentFileCacheTest.java
new file mode 100644
index 0000000..1eaf1e1
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/fsprovider/internal/parser/ContentFileCacheTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.experimental.theories.DataPoint;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public class ContentFileCacheTest {
+    
+    @DataPoint
+    public static final int NO_CACHE = 0;
+    @DataPoint
+    public static final int SMALL_CACHE = 1;
+    @DataPoint
+    public static final int HUGE_CACHE = 1000;
+
+    @Theory
+    public void testCache(int cacheSize) {
+        ContentFileCache underTest = new ContentFileCache(cacheSize);
+        
+        Map<String,Object> content1 = 
underTest.get("/fs-test/folder2/content", new 
File("src/test/resources/fs-test/folder2/content.json"));
+        assertNotNull(content1);
+        
+        switch (cacheSize) {
+        case NO_CACHE:
+            assertEquals(0, underTest.size());
+            break;
+        case SMALL_CACHE:
+        case HUGE_CACHE:
+            assertEquals(1, underTest.size());
+            break;
+        }
+
+        Map<String,Object> content2 = underTest.get("/fs-test/folder1/file1a", 
new File("src/test/resources/fs-test/folder1/file1a.txt"));
+        assertNull(content2);
+
+        switch (cacheSize) {
+        case NO_CACHE:
+            assertEquals(0, underTest.size());
+            break;
+        case SMALL_CACHE:
+            assertEquals(1, underTest.size());
+            break;
+        case HUGE_CACHE:
+            assertEquals(2, underTest.size());
+            break;
+        }
+
+        underTest.remove("/fs-test/folder1/file1a");
+
+        switch (cacheSize) {
+        case NO_CACHE:
+        case SMALL_CACHE:
+            assertEquals(0, underTest.size());
+            break;
+        case HUGE_CACHE:
+            assertEquals(1, underTest.size());
+            break;
+        }
+        
+        underTest.clear();
+
+        assertEquals(0, underTest.size());        
+    }
+
+}
diff --git a/src/test/resources/fs-test/folder2/content.json 
b/src/test/resources/fs-test/folder2/content.json
index e808ef8..f35baf8 100644
--- a/src/test/resources/fs-test/folder2/content.json
+++ b/src/test/resources/fs-test/folder2/content.json
@@ -102,7 +102,6 @@
         }
       },
       "title_2": {
-        "jcr:primaryType": "nt:unstructured",
         "jcr:createdBy": "admin",
         "jcr:title": "Shape Technology",
         "jcr:lastModifiedBy": "admin",
@@ -220,6 +219,7 @@
       "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
       "jcr:content": {
         "jcr:primaryType": "app:PageContent",
+        "jcr:mixinTypes": ["type1","type2"],
         "jcr:createdBy": "admin",
         "jcr:title": "Profiles",
         "app:template": "/apps/sample/templates/contentpage",
diff --git a/src/test/resources/fs-test/folder2/content/file2content.txt 
b/src/test/resources/fs-test/folder2/content/file2content.txt
new file mode 100644
index 0000000..667b547
--- /dev/null
+++ b/src/test/resources/fs-test/folder2/content/file2content.txt
@@ -0,0 +1 @@
+file2content
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"[email protected]" <[email protected]>.

Reply via email to