sling-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jsedd...@apache.org
Subject [sling-org-apache-sling-fsresource] branch master updated: SLING-7577 - FS Resource Provider slow due to frequent file stat access
Date Tue, 17 Apr 2018 12:03:38 GMT
This is an automated email from the ASF dual-hosted git repository.

jsedding pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-fsresource.git


The following commit(s) were added to refs/heads/master by this push:
     new 221bcc1  SLING-7577 - FS Resource Provider slow due to frequent file stat access
221bcc1 is described below

commit 221bcc16a7fabe175504fe92a30fe46a451f3cb7
Author: Julian Sedding <jsedding@apache.org>
AuthorDate: Thu Mar 15 13:55:21 2018 +0100

    SLING-7577 - FS Resource Provider slow due to frequent file stat access
---
 pom.xml                                            |   1 +
 .../sling/fsprovider/internal/FileMonitor.java     |  10 +-
 .../sling/fsprovider/internal/FileStatCache.java   | 217 +++++++++++++++++++++
 .../fsprovider/internal/FsResourceProvider.java    |  15 +-
 .../internal/mapper/ContentFileResource.java       |   4 +-
 .../internal/mapper/ContentFileResourceMapper.java |  14 +-
 .../fsprovider/internal/mapper/FileResource.java   |  23 ++-
 .../internal/mapper/FileResourceMapper.java        |  20 +-
 .../internal/mapper/FileVaultResourceMapper.java   |  23 ++-
 .../mapper/LazyModifiedDateResourceMetadata.java   |  44 +++++
 .../sling/fsprovider/internal/JsonContentTest.java |  27 ++-
 .../content/fileWithOverwrittenMimeType.scss       |   3 +
 .../content/fileWithOverwrittenMimeType.scss.json  |   7 +
 13 files changed, 364 insertions(+), 44 deletions(-)

diff --git a/pom.xml b/pom.xml
index 016c2d1..34ebb92 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,7 @@
                             geronimo-json_1.0_spec;scope=compile;inline=false,
                             org.apache.sling.jcr.contentparser;scope=compile;inline=false
                         </Embed-Dependency>
+                        <_plugin>java.lang.String</_plugin>
                     </instructions>
                 </configuration>
             </plugin>
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 08af507..4b71865 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FileMonitor.java
@@ -55,6 +55,7 @@ public final class FileMonitor extends TimerTask {
     private final FsMode fsMode;
     private final ContentFileExtensions contentFileExtensions;
     private final ContentFileCache contentFileCache;
+    private final FileStatCache fileStatCache;
 
     /**
      * Creates a new instance of this class.
@@ -63,14 +64,17 @@ public final class FileMonitor extends TimerTask {
      * @param fsMode FS mode
      * @param contentFileExtensions Content file extensions
      * @param contentFileCache Content file cache
+     * @param fileStatCache Cache reducing file-stat lookups (file exists or not, directory
vs file).
      */
     public FileMonitor(final FsResourceProvider provider, final long interval, FsMode fsMode,
-            final ContentFileExtensions contentFileExtensions, final ContentFileCache contentFileCache)
{
+                       final ContentFileExtensions contentFileExtensions, final ContentFileCache
contentFileCache,
+                       final FileStatCache fileStatCache) {
         this.provider = provider;
         this.fsMode = fsMode;
         this.contentFileExtensions = contentFileExtensions;
         this.contentFileCache = contentFileCache;
-        
+        this.fileStatCache = fileStatCache;
+
         File rootFile = this.provider.getRootFile();
         if (fsMode == FsMode.FILEVAULT_XML) {
             rootFile = new File(this.provider.getRootFile(), "." + PlatformNameFormat.getPlatformPath(this.provider.getProviderRoot()));
@@ -153,6 +157,7 @@ public final class FileMonitor extends TimerTask {
             if ( monitorable.file.exists() ) {
                 // new file and reset status
                 createStatus(monitorable, contentFileExtensions, contentFileCache);
+                fileStatCache.clear();
                 sendEvents(monitorable, ChangeType.ADDED, reporter);
                 final FileStatus fs = (FileStatus)monitorable.status;
                 if ( fs instanceof DirStatus ) {
@@ -168,6 +173,7 @@ public final class FileMonitor extends TimerTask {
                 // removed file and update status
                 contentFileCache.remove(transformPath(monitorable.path));
                 monitorable.status = NonExistingStatus.SINGLETON;
+                fileStatCache.clear();
                 sendEvents(monitorable, ChangeType.REMOVED, reporter);
             } else {
                 // check for changes
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/FileStatCache.java b/src/main/java/org/apache/sling/fsprovider/internal/FileStatCache.java
new file mode 100644
index 0000000..769382d
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FileStatCache.java
@@ -0,0 +1,217 @@
+/*
+ * 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 org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A cache that caches whether files exist or don't exist.
+ */
+public class FileStatCache {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FileStatCache.class);
+
+    private final Map<String, FileStat> fileStates = new ConcurrentHashMap<>();
+
+    private enum FileStat {
+        EXISTING_DIRECTORY(true, true),
+        EXISTING_FILE(true, false),
+        NON_EXISTING(false, false);
+
+        private final boolean exists;
+
+        private final boolean isDirectory;
+
+        public static FileStat compute(File file) {
+            if (file.isDirectory()) {
+                return EXISTING_DIRECTORY;
+            } else if (file.exists()) {
+                return EXISTING_FILE;
+            } else {
+                return NON_EXISTING;
+            }
+        }
+
+        FileStat(boolean exists, boolean isDirectory) {
+            this.exists = exists;
+            this.isDirectory = isDirectory;
+        }
+
+        public boolean exists() {
+            return exists;
+        }
+
+        public boolean isDirectory() {
+            return isDirectory;
+        }
+
+        public boolean isFile() {
+            return exists && !isDirectory;
+        }
+
+
+        @Override
+        public String toString() {
+            return exists() ? (isDirectory() ? "existing directory" : "existing file") :
"non existing file";
+        }
+    }
+
+    private final String providerFilePath;
+
+    @SuppressWarnings("unchecked")
+    FileStatCache(final File providerFile) {
+        this.providerFilePath = providerFile.getPath();
+    }
+
+    // used by FileMonitor to notify for changes (added or deleted only)
+    void clear() {
+        fileStates.clear();
+    }
+
+    public boolean isDirectory(final File file) {
+        return getFileState(file).isDirectory();
+    }
+
+    public boolean isFile(final File file) {
+        return getFileState(file).isFile();
+    }
+
+    public boolean exists(final File file) {
+        return getFileState(file).exists();
+    }
+
+    private FileStat getFileState(File file) {
+        String path = relativePath(providerFilePath, file.getPath());
+        if (StringUtils.isBlank(path)) {
+            return FileStat.EXISTING_DIRECTORY;
+        }
+        FileStat fileStat = fileStates.get(path);
+        if (fileStat == null) {
+            if (!parentExists(path, file)) {
+                LOG.trace("Does not exist (via parent): {}", path);
+                return FileStat.NON_EXISTING;
+            }
+            fileStat = FileStat.compute(file);
+            fileStates.put(path, fileStat);
+            if (fileStat.exists()) {
+                CacheStatistics.EXISTS_ACCESS.uncachedAccess(path);
+            } else {
+                CacheStatistics.NOT_EXISTS_ACCESS.uncachedAccess(path);
+            }
+        } else if (fileStat.exists()) {
+            CacheStatistics.EXISTS_ACCESS.cachedAccess(path);
+        } else {
+            CacheStatistics.NOT_EXISTS_ACCESS.cachedAccess(path);
+        }
+        return fileStat;
+    }
+
+    private String relativePath(final String basePath, final String path) {
+        final String suffix = StringUtils.removeStart(path, basePath);
+        final String normalizedPath = StringUtils.replaceChars(suffix, '\\', '/');
+
+        // If path starts with a '.', it is likely the descriptor file that
+        // is a sibling of the providerFilePath, i.e. most likely a .json file.
+        // We need to fix that up, as it is not a child of the providerFilePath.
+        if (normalizedPath == null || normalizedPath.startsWith(".") && normalizedPath.length()
> 2) {
+            return "../" + ResourceUtil.getName(path);
+        }
+
+        return normalizedPath;
+    }
+
+    private FileStat getClosestCachedAncestorState(String path) {
+        String ancestorPath = path;
+        FileStat fileStat = null;
+        do {
+            String nextAncestorPath = StringUtils.substringBeforeLast(ancestorPath, "/");
+            if (StringUtils.equals(ancestorPath, nextAncestorPath)) {
+                break;
+            }
+            ancestorPath = nextAncestorPath;
+            if (ancestorPath != null) {
+                fileStat = fileStates.get(ancestorPath);
+            }
+        } while (ancestorPath != null && fileStat == null);
+        return fileStat;
+    }
+
+    private boolean parentExists(final String path, final File file) {
+        FileStat cachedAncestorState = getClosestCachedAncestorState(path);
+        if (cachedAncestorState != null && !cachedAncestorState.exists()) {
+            return false;
+        } else {
+            File parentFile = file.getParentFile();
+            return parentFile == null || exists(parentFile);
+        }
+    }
+
+
+    private enum CacheStatistics {
+        EXISTS_ACCESS("Does exist (cached: {}/{}): {}", "Does exist (added to cache: {}/{}):
{}"),
+        NOT_EXISTS_ACCESS("Does not exist (cached: {}/{}): {}", "Does not exist (added to
cache: {}/{}): {}");
+
+        private final String cachedAccessLogStatement;
+
+        private final String uncachedAccessLogStatement;
+
+        private final AtomicLong cachedAccess;
+
+        private final AtomicLong uncachedAccess;
+
+        CacheStatistics(final String cachedAccessLogStatement, final String uncachedAccessLogStatement)
{
+            this.cachedAccessLogStatement = cachedAccessLogStatement;
+            this.uncachedAccessLogStatement = uncachedAccessLogStatement;
+            this.cachedAccess = new AtomicLong(0);
+            this.uncachedAccess = new AtomicLong(0);
+        }
+
+        public void cachedAccess(String path) {
+            long cached = cachedAccess.incrementAndGet();
+            long uncached = uncachedAccess.get();
+            log(cached, uncached, true, path);
+        }
+
+        public void uncachedAccess(String path) {
+            long cached = cachedAccess.get();
+            long uncached = uncachedAccess.incrementAndGet();
+            log(cached, uncached, false, path);
+        }
+
+        private void log(long cached, long uncached, boolean logCached, String path) {
+            if (LOG.isDebugEnabled()) {
+                String statement = logCached ? cachedAccessLogStatement : uncachedAccessLogStatement;
+                long all = uncached + uncached;
+                long count = logCached ? cached : uncached;
+                LOG.trace(statement, count, all, path);
+                if (!LOG.isTraceEnabled() && all % 100_000 == 0) {
+                    LOG.debug(statement, count, all, path);
+                }
+            }
+        }
+    }
+}
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 62446d5..c6eff9b 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/FsResourceProvider.java
@@ -80,7 +80,7 @@ public final class FsResourceProvider extends ResourceProvider<Object>
{
      * if the underlying file reference is a directory.
      */
     public static final String RESOURCE_METADATA_FILE_DIRECTORY = ":org.apache.sling.fsprovider.file.directory";
-    
+
     @ObjectClassDefinition(name = "Apache Sling File System Resource Provider",
             description = "Configure an instance of the file system " +
                           "resource provider in terms of provider root and file system location")
@@ -155,6 +155,9 @@ public final class FsResourceProvider extends ResourceProvider<Object>
{
     // cache for parsed content files
     private ContentFileCache contentFileCache;
 
+    // cache for files that were requested but don't exist
+    private FileStatCache fileStatCache;
+
     /**
      * Returns a resource wrapping a file system file or folder for the given
      * path. If the <code>path</code> is equal to the configured resource tree
@@ -337,19 +340,21 @@ public final class FsResourceProvider extends ResourceProvider<Object>
{
         ContentFileExtensions contentFileExtensions = new ContentFileExtensions(contentFileSuffixes);
         
         this.contentFileCache = new ContentFileCache(config.provider_cache_size());
+        this.fileStatCache = new FileStatCache(this.providerFile);
+
         if (fsMode == FsMode.FILEVAULT_XML) {
-            this.fileVaultMapper = new FileVaultResourceMapper(this.providerFile, filterXmlFile,
this.contentFileCache);
+            this.fileVaultMapper = new FileVaultResourceMapper(this.providerFile, filterXmlFile,
this.contentFileCache, this.fileStatCache);
         }
         else {
-            this.fileMapper = new FileResourceMapper(this.providerRoot, this.providerFile,
contentFileExtensions, this.contentFileCache);
+            this.fileMapper = new FileResourceMapper(this.providerRoot, this.providerFile,
contentFileExtensions, this.contentFileCache, this.fileStatCache);
             this.contentFileMapper = new ContentFileResourceMapper(this.providerRoot, this.providerFile,
-                    contentFileExtensions, this.contentFileCache);
+                    contentFileExtensions, this.contentFileCache, this.fileStatCache);
         }
         
         // start background monitor if check interval is higher than 100
         if (config.provider_checkinterval() > 100) {
             this.monitor = new FileMonitor(this, config.provider_checkinterval(), fsMode,
-                    contentFileExtensions, this.contentFileCache);
+                    contentFileExtensions, this.contentFileCache, this.fileStatCache);
         }
     }
 
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 eb8c938..fc1d17a 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
@@ -52,7 +52,6 @@ public final class ContentFileResource extends AbstractResource {
 
     /**
      * @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, ContentFile contentFile) {
@@ -68,8 +67,7 @@ public final class ContentFileResource extends AbstractResource {
 
     public ResourceMetadata getResourceMetadata() {
         if (metaData == null) {
-            metaData = new ResourceMetadata();
-            metaData.setModificationTime(contentFile.getFile().lastModified());
+            metaData = new LazyModifiedDateResourceMetadata(contentFile.getFile());
             metaData.setResolutionPath(resourcePath);
         }
         return metaData;
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 e7daf29..509bd34 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
@@ -31,6 +31,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.FileStatCache;
 import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 
 public final class ContentFileResourceMapper implements FsResourceMapper {
@@ -43,13 +44,16 @@ public final class ContentFileResourceMapper implements FsResourceMapper
{
     
     private final ContentFileExtensions contentFileExtensions;
     private final ContentFileCache contentFileCache;
+    private FileStatCache fileStatCache;
 
     public ContentFileResourceMapper(String providerRoot, File providerFile,
-            ContentFileExtensions contentFileExtensions, ContentFileCache contentFileCache)
{
+                                     ContentFileExtensions contentFileExtensions, ContentFileCache
contentFileCache,
+                                     FileStatCache fileStatCache) {
         this.providerRootPrefix = providerRoot.concat("/");
         this.providerFile = providerFile;
         this.contentFileExtensions = contentFileExtensions;
         this.contentFileCache = contentFileCache;
+        this.fileStatCache = fileStatCache;
     }
     
     @Override
@@ -92,7 +96,7 @@ public final class ContentFileResourceMapper implements FsResourceMapper
{
 
         // add children from filesystem folder
         File parentDir = new File(providerFile, StringUtils.removeStart(parentPath, providerRootPrefix));
-        if (parentDir.isDirectory()) {
+        if (fileStatCache.isDirectory(parentDir)) {
             File[] files = parentDir.listFiles();
             if (files != null) {
                 childIterators.add(IteratorUtils.transformedIterator(IteratorUtils.arrayIterator(files),
new Transformer() {
@@ -106,7 +110,7 @@ public final class ContentFileResourceMapper implements FsResourceMapper
{
                             ContentFile contentFile = new ContentFile(file, path, null, contentFileCache);
                             return new ContentFileResource(resolver, contentFile);
                         } else {
-                            return new FileResource(resolver, path, file, contentFileExtensions,
contentFileCache);
+                            return new FileResource(resolver, path, file, contentFileExtensions,
contentFileCache, fileStatCache);
                         }
                     }
                 }));
@@ -127,7 +131,7 @@ public final class ContentFileResourceMapper implements FsResourceMapper
{
         String relPath = Escape.resourceToFileName(path.substring(providerRootPrefix.length()));
         for (String filenameSuffix : contentFileExtensions.getSuffixes()) {
             File file = new File(providerFile, relPath + filenameSuffix);
-            if (file.exists()) {
+            if (fileStatCache.exists(file)) {
                 return new ContentFile(file, path, subPath, contentFileCache);
             }
         }
@@ -142,7 +146,7 @@ public final class ContentFileResourceMapper implements FsResourceMapper
{
         for (String filenameSuffix : contentFileExtensions.getSuffixes()) {
             if (StringUtils.endsWith(file.getPath(), filenameSuffix)) {
                 File fileWithoutSuffix = new File(StringUtils.substringBeforeLast(file.getPath(),
filenameSuffix));
-                return fileWithoutSuffix.exists();
+                return fileStatCache.exists(fileWithoutSuffix);
             }
         }
         return false;
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResource.java b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResource.java
index d67f0eb..c77d42f 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResource.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResource.java
@@ -42,6 +42,7 @@ import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.api.wrappers.DeepReadValueMapDecorator;
 import org.apache.sling.fsprovider.internal.ContentFileExtensions;
+import org.apache.sling.fsprovider.internal.FileStatCache;
 import org.apache.sling.fsprovider.internal.FsResourceProvider;
 import org.apache.sling.fsprovider.internal.mapper.jcr.FsNode;
 import org.apache.sling.fsprovider.internal.mapper.valuemap.ValueMapDecorator;
@@ -93,6 +94,7 @@ public final class FileResource extends AbstractResource {
 
     private final ContentFileExtensions contentFileExtensions;
     private final ContentFileCache contentFileCache;
+    private final FileStatCache fileStatCache;
 
     private static final Logger log = LoggerFactory.getLogger(FileResource.class);
     
@@ -103,17 +105,19 @@ public final class FileResource extends AbstractResource {
      * @param resourcePath The resource path in the resource tree
      * @param file The wrapped file
      */
-    FileResource(ResourceResolver resolver, String resourcePath, File file) {
-        this(resolver, resourcePath, file, null, null);
+    FileResource(ResourceResolver resolver, String resourcePath, File file, FileStatCache
fileStatCache) {
+        this(resolver, resourcePath, file, null, null, fileStatCache);
     }
     
     FileResource(ResourceResolver resolver, String resourcePath, File file,
-            ContentFileExtensions contentFileExtensions, ContentFileCache contentFileCache)
{
+                 ContentFileExtensions contentFileExtensions, ContentFileCache contentFileCache,
+                 FileStatCache fileStatCache) {
         this.resolver = resolver;
         this.resourcePath = resourcePath;
         this.file = file;
         this.contentFileExtensions = contentFileExtensions;
         this.contentFileCache = contentFileCache;
+        this.fileStatCache = fileStatCache;
     }
 
     /**
@@ -130,11 +134,10 @@ public final class FileResource extends AbstractResource {
      */
     public ResourceMetadata getResourceMetadata() {
         if (metaData == null) {
-            metaData = new ResourceMetadata();
+            metaData = new LazyModifiedDateResourceMetadata(file);
             metaData.setContentLength(file.length());
-            metaData.setModificationTime(file.lastModified());
             metaData.setResolutionPath(resourcePath);
-            if ( this.file.isDirectory() ) {
+            if (fileStatCache.isDirectory(file)) {
                 metaData.put(FsResourceProvider.RESOURCE_METADATA_FILE_DIRECTORY, Boolean.TRUE);
             }
         }
@@ -180,7 +183,7 @@ public final class FileResource extends AbstractResource {
             return (AdapterType) file;
         }
         else if (type == InputStream.class) {
-            if (!file.isDirectory() && file.canRead()) {
+            if (fileStatCache.isFile(file) && file.canRead()) {
                 try {
                     return (AdapterType) new FileInputStream(file);
                 }
@@ -227,9 +230,9 @@ public final class FileResource extends AbstractResource {
         if (valueMap == null) {
             // 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()) {
+            if (fileStatCache.exists(file) && file.canRead()) {
                 Map<String,Object> props = new HashMap<String, Object>();
-                props.put("jcr:primaryType", file.isFile() ? RESOURCE_TYPE_FILE : RESOURCE_TYPE_FOLDER);
+                props.put("jcr:primaryType", fileStatCache.isFile(file) ? RESOURCE_TYPE_FILE
: RESOURCE_TYPE_FOLDER);
                 props.put("jcr:createdBy", "system");
                 
                 Calendar lastModifed = Calendar.getInstance();
@@ -261,7 +264,7 @@ public final class FileResource extends AbstractResource {
         }
         for (String fileNameSuffix : contentFileExtensions.getSuffixes()) {
             File fileWithSuffix = new File(file.getPath() + fileNameSuffix);
-            if (fileWithSuffix.exists() && fileWithSuffix.canRead()) {
+            if (fileStatCache.exists(fileWithSuffix) && fileWithSuffix.canRead())
{
                 return new ContentFile(fileWithSuffix, resourcePath, null, contentFileCache);
             }
         }
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
index e0a1429..61ed48c 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResourceMapper.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileResourceMapper.java
@@ -28,6 +28,7 @@ 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.FileStatCache;
 import org.apache.sling.fsprovider.internal.FsResourceMapper;
 import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
 
@@ -44,21 +45,24 @@ public final class FileResourceMapper implements FsResourceMapper {
     
     private final ContentFileExtensions contentFileExtensions;
     private final ContentFileCache contentFileCache;
-    
+    private FileStatCache fileStatCache;
+
     public FileResourceMapper(String providerRoot, File providerFile,
-            ContentFileExtensions contentFileExtensions, ContentFileCache contentFileCache)
{
+                              ContentFileExtensions contentFileExtensions, ContentFileCache
contentFileCache,
+                              FileStatCache fileStatCache) {
         this.providerRoot = providerRoot;
         this.providerRootPrefix = providerRoot.concat("/");
         this.providerFile = providerFile;
         this.contentFileExtensions = contentFileExtensions;
         this.contentFileCache = contentFileCache;
+        this.fileStatCache = fileStatCache;
     }
     
     @Override
     public Resource getResource(final ResourceResolver resolver, final String resourcePath)
{
         File file = getFile(resourcePath);
         if (file != null) {
-            return new FileResource(resolver, resourcePath, file, contentFileExtensions,
contentFileCache);
+            return new FileResource(resolver, resourcePath, file, contentFileExtensions,
contentFileCache, fileStatCache);
         }
         else {
             return null;
@@ -83,12 +87,12 @@ public final class FileResourceMapper implements FsResourceMapper {
             // a repository item with the same path actually exists
             if (parentFile == null) {
 
-                if (providerFile.exists() && !StringUtils.startsWith(parentPath,
providerRoot)) {
+                if (!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,
contentFileExtensions, contentFileCache);
+                            Resource res = new FileResource(resolver, providerRoot, providerFile,
contentFileExtensions, contentFileCache, fileStatCache);
                             return IteratorUtils.singletonIterator(res);
                         }
                     }
@@ -100,7 +104,7 @@ public final class FileResourceMapper implements FsResourceMapper {
         }
         
         // ensure parent is a directory
-        if (!parentFile.isDirectory()) {
+        if (!fileStatCache.isDirectory(parentFile)) {
             return null;
         }
 
@@ -119,7 +123,7 @@ public final class FileResourceMapper implements FsResourceMapper {
             public Object transform(Object input) {
                 File file = (File)input;
                 String path = parentPath + "/" + Escape.fileToResourceName(file.getName());
-                return new FileResource(resolver, path, file, contentFileExtensions, contentFileCache);
+                return new FileResource(resolver, path, file, contentFileExtensions, contentFileCache,
fileStatCache);
             }
         });
     }
@@ -139,7 +143,7 @@ public final class FileResourceMapper implements FsResourceMapper {
         if (path.startsWith(providerRootPrefix)) {
             String relPath = Escape.resourceToFileName(path.substring(providerRootPrefix.length()));
             File file = new File(providerFile, relPath);
-            if (file.exists() && !contentFileExtensions.matchesSuffix(file)) {
+            if (!contentFileExtensions.matchesSuffix(file) && fileStatCache.exists(file))
{
                 return file;
             }
         }
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java
index 24dc458..90e66fd 100644
--- a/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/FileVaultResourceMapper.java
@@ -38,6 +38,7 @@ import org.apache.jackrabbit.vault.util.PlatformNameFormat;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.fsprovider.internal.FileStatCache;
 import org.apache.sling.fsprovider.internal.FsResourceMapper;
 import org.apache.sling.fsprovider.internal.parser.ContentElement;
 import org.apache.sling.fsprovider.internal.parser.ContentFileCache;
@@ -54,14 +55,16 @@ public final class FileVaultResourceMapper implements FsResourceMapper
{
     private final File providerFile;
     private final File filterXmlFile;
     private final ContentFileCache contentFileCache;
+    private FileStatCache fileStatCache;
     private final WorkspaceFilter workspaceFilter;
     
     private static final Logger log = LoggerFactory.getLogger(FileVaultResourceMapper.class);
     
-    public FileVaultResourceMapper(File providerFile, File filterXmlFile, ContentFileCache
contentFileCache) {
+    public FileVaultResourceMapper(File providerFile, File filterXmlFile, ContentFileCache
contentFileCache, FileStatCache fileStatCache) {
         this.providerFile = providerFile;
         this.filterXmlFile = filterXmlFile;
         this.contentFileCache = contentFileCache;
+        this.fileStatCache = fileStatCache;
         this.workspaceFilter = getWorkspaceFilter();
     }
     
@@ -70,8 +73,8 @@ public final class FileVaultResourceMapper implements FsResourceMapper {
         
         // direct file
         File file = getFile(resourcePath);
-        if (file != null && file.isFile()) {
-            return new FileResource(resolver, resourcePath, file);
+        if (file != null && fileStatCache.isFile(file)) {
+            return new FileResource(resolver, resourcePath, file, fileStatCache);
         }
         
         // content file
@@ -81,8 +84,8 @@ public final class FileVaultResourceMapper implements FsResourceMapper {
         }
         
         // fallback to directory resource if folder was found but nothing else
-        if (file != null && file.isDirectory()) {
-            return new FileResource(resolver, resourcePath, file);
+        if (file != null && fileStatCache.isDirectory(file)) {
+            return new FileResource(resolver, resourcePath, file, fileStatCache);
         }
         
         return null;
@@ -110,7 +113,7 @@ public final class FileVaultResourceMapper implements FsResourceMapper
{
         
         // additional check for children in file system
         File parentFile = getFile(parentPath);
-        if (parentFile != null && parentFile.isDirectory()) {
+        if (parentFile != null && fileStatCache.isDirectory(parentFile)) {
             for (File childFile : parentFile.listFiles()) {
                 String childPath = parentPath + "/" + PlatformNameFormat.getRepositoryName(childFile.getName());
                 File file = getFile(childPath);
@@ -186,7 +189,7 @@ public final class FileVaultResourceMapper implements FsResourceMapper
{
             return null;
         }
         File file = new File(providerFile, "." + PlatformNameFormat.getPlatformPath(path));
-        if (file.exists()) {
+        if (fileStatCache.exists(file)) {
             if (StringUtils.endsWith(path, XML_SUFFIX) && !hasDotDirFile(file)) {
                 return null;
             }
@@ -197,7 +200,7 @@ public final class FileVaultResourceMapper implements FsResourceMapper
{
     
     private ContentFile getContentFile(String path, String subPath) {
         File file = new File(providerFile, "." + PlatformNameFormat.getPlatformPath(path)
+ DOT_CONTENT_XML_SUFFIX);
-        if (file.exists()) {
+        if (fileStatCache.exists(file)) {
             ContentFile contentFile = new ContentFile(file, path, subPath, contentFileCache,
ContentType.JCR_XML);
             if (contentFile.hasContent()) {
                 return contentFile;
@@ -205,7 +208,7 @@ public final class FileVaultResourceMapper implements FsResourceMapper
{
         }
         
         file = new File(providerFile, "." + PlatformNameFormat.getPlatformPath(path) + XML_SUFFIX);
-        if (file.exists() && !hasDotDirFile(file)) {
+        if (fileStatCache.exists(file) && !hasDotDirFile(file)) {
             ContentFile contentFile = new ContentFile(file, path, subPath, contentFileCache,
ContentType.JCR_XML);
             if (contentFile.hasContent()) {
                 return contentFile;
@@ -224,7 +227,7 @@ public final class FileVaultResourceMapper implements FsResourceMapper
{
     
     private boolean hasDotDirFile(File file) {
         File dotDir = new File(file.getPath() + DOT_DIR);
-        if (dotDir.exists() && dotDir.isDirectory()) {
+        if (fileStatCache.isDirectory(dotDir)) {
             return true;
         }
         return false;
diff --git a/src/main/java/org/apache/sling/fsprovider/internal/mapper/LazyModifiedDateResourceMetadata.java
b/src/main/java/org/apache/sling/fsprovider/internal/mapper/LazyModifiedDateResourceMetadata.java
new file mode 100644
index 0000000..54443e6
--- /dev/null
+++ b/src/main/java/org/apache/sling/fsprovider/internal/mapper/LazyModifiedDateResourceMetadata.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.fsprovider.internal.mapper;
+
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+class LazyModifiedDateResourceMetadata extends ResourceMetadata {
+
+    private volatile long lastModified = -1;
+    private File file;
+
+    public LazyModifiedDateResourceMetadata(final File file) {
+        this.file = file;
+    }
+
+    @Override
+    public long getModificationTime() {
+        if (lastModified == -1) {
+            // lazy init - fs access is quite slow
+            lastModified = file.lastModified();
+        }
+        return lastModified;
+    }
+}
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 e33f1d1..5c38ef1 100644
--- a/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
+++ b/src/test/java/org/apache/sling/fsprovider/internal/JsonContentTest.java
@@ -28,6 +28,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import java.io.InputStream;
 import java.math.BigDecimal;
 import java.util.Collections;
 import java.util.HashSet;
@@ -51,6 +52,7 @@ 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.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -102,7 +104,8 @@ public class JsonContentTest {
         assertThat(fsroot, ResourceMatchers.hasChildren("folder1", "folder2"));
         assertThat(fsroot.getChild("folder1"), ResourceMatchers.containsChildrenInAnyOrder("folder11",
"file1a.txt", "sling:file1b.txt"));
         assertThat(fsroot.getChild("folder2"), ResourceMatchers.containsChildrenInAnyOrder("folder21",
"content"));
-        assertThat(fsroot.getChild("folder2/content"), ResourceMatchers.containsChildrenInAnyOrder("jcr:content",
"toolbar", "child", "file2content.txt", "sling:content2"));
+        assertThat(fsroot.getChild("folder2/content"), ResourceMatchers.containsChildrenInAnyOrder(
+                "jcr:content", "toolbar", "child", "file2content.txt", "sling:content2",
"fileWithOverwrittenMimeType.scss"));
         assertThat(fsroot.getChild("folder2/content/child"), ResourceMatchers.containsChildrenInAnyOrder("jcr:content",
"grandchild"));
     }
 
@@ -304,4 +307,26 @@ public class JsonContentTest {
         String profilesTitle = properties.get("profiles/jcr:content/jcr:title", String.class);
         assertEquals("Profiles", profilesTitle);
     }
+
+    @Test @Ignore
+    public void testFileHasJcrContentAndData() {
+        Resource underTest = fsroot.getChild("folder2/content/file2content.txt");
+        assertNotNull("failed adapting file2content.txt to InputStream", underTest.adaptTo(InputStream.class));
+        //assertThat(underTest, ResourceMatchers.hasChildren("jcr:content"));
+        Resource content = underTest.getChild("jcr:content");
+        ValueMap props = content.getValueMap();
+        assertNotNull("jcr:data is missing", props.get("jcr:data", InputStream.class));
+    }
+
+    @Test @Ignore
+    public void testFileWithOverwrittenMimeType() {
+        Resource underTest = fsroot.getChild("folder2/content/fileWithOverwrittenMimeType.scss");
+        assertNotNull("failed adapting fileWithOverwrittenMimeType.scss to InputStream",
+                underTest.adaptTo(InputStream.class));
+        Resource content = underTest.getChild("jcr:content");
+        ValueMap props = content.getValueMap();
+        assertEquals("nt:unstructured", props.get("jcr:primaryType", "[missing]"));
+        assertEquals("text/css", props.get("jcr:mimeType", "[missing]"));
+        assertNotNull("jcr:data is missing", props.get("jcr:data", InputStream.class));
+    }
 }
diff --git a/src/test/resources/fs-test/folder2/content/fileWithOverwrittenMimeType.scss b/src/test/resources/fs-test/folder2/content/fileWithOverwrittenMimeType.scss
new file mode 100644
index 0000000..7dc854a
--- /dev/null
+++ b/src/test/resources/fs-test/folder2/content/fileWithOverwrittenMimeType.scss
@@ -0,0 +1,3 @@
+.fileWithOverwrittenMimeType {
+    position: absolute;
+}
\ No newline at end of file
diff --git a/src/test/resources/fs-test/folder2/content/fileWithOverwrittenMimeType.scss.json
b/src/test/resources/fs-test/folder2/content/fileWithOverwrittenMimeType.scss.json
new file mode 100644
index 0000000..2955f7d
--- /dev/null
+++ b/src/test/resources/fs-test/folder2/content/fileWithOverwrittenMimeType.scss.json
@@ -0,0 +1,7 @@
+{
+  "jcr:primaryType": "nt:file",
+  "jcr:content": {
+    "jcr:primaryType": "nt:unstructured",
+    "jcr:mimeType":"text/css"
+  }
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
jsedding@apache.org.

Mime
View raw message