blob: 58a8911c7106a7b8fd1811ad193eb5c5621ee07b [file] [log] [blame]
/*
* 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);
}
}
}
}
}