| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.sling.fsprovider.internal; |
| |
| import java.io.File; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.atomic.AtomicLong; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.sling.api.resource.ResourceUtil; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * 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; |
| |
| 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); |
| } |
| } |
| } |
| } |
| } |