/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and contributors.
*
* Licensed 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.common.file;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.commons.io.FileCleaningTracker;
import org.apache.commons.io.IOUtils;
/**
* A LRU cache of {@link File}s with capped filesystem size.
* <p>
* When a new file is put in the cache, if the total size becomes more that the
* maximum size then least recently access entries are removed until the new
* file fits.
* <p>
* A file will never be actually removed from the filesystem while the File
* object returned by {@link #getFile} is still referenced.
* <p>
* The cache keys are restricted to a subset of ASCII: letters, digits and
* dashes. Usually a MD5 or SHA1 hash is used.
*/
public class LRUFileCache implements FileCache {
/** Allowed key pattern, used as file path. */
public static final Pattern SIMPLE_ASCII = Pattern.compile("[-_a-zA-Z0-9]+");
protected final File dir;
protected final long maxSize;
/** Cached files. */
protected final Map<String, LRUFileCacheEntry> cache;
/**
* Referenced files on the filesystem. Contains all the cached files, plus
* all those that have been marked for deletion but haven't been deleted
* yet. Because of the latter, this is a weak value map.
*/
protected final Map<String, File> files;
/** Size of the cached files. */
protected long cacheSize;
/** Most recently used entries from the cache are first. */
protected final LinkedList<String> lru;
// this creates a new thread
private static final FileCleaningTracker fileCleaningTracker = new FileCleaningTracker();
/**
* In-memory entry for a cached file.
*/
protected static class LRUFileCacheEntry {
public File file;
public long size;
}
/**
* Constructs a cache in the given directory with the given maximum size (in
* bytes).
*
* @param dir the directory to use to store cached files
* @param maxSize the maximum size of the cache (in bytes)
*/
@SuppressWarnings("unchecked")
public LRUFileCache(File dir, long maxSize) {
this.dir = dir;
this.maxSize = maxSize;
cache = new HashMap<String, LRUFileCacheEntry>();
// use a weak reference for the values: don't hold values longer than
// they need to be referenced elsewhere
files = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK);
lru = new LinkedList<String>();
}
@Override
public long getSize() {
return cacheSize;
}
@Override
public int getNumberOfItems() {
return lru.size();
}
@Override
public File getTempFile() throws IOException {
File tmp = File.createTempFile("nxbin_", null, dir);
tmp.deleteOnExit();
return tmp;
}
/**
* {@inheritDoc}
* <p>
* The key is used as a file name in the directory cache.
*/
@Override
public synchronized File putFile(String key, InputStream in)
throws IOException {
try {
// check the cache
LRUFileCacheEntry entry = cache.get(key);
if (entry != null) {
return entry.file;
}
// maybe the cache entry was just deleted but the file is still
// there?
File file = files.get(key);
if (file != null) {
// use not-yet-deleted file
return putFileInCache(key, file);
}
// store the stream in a temporary file
file = getTempFile();
FileOutputStream out = new FileOutputStream(file);
try {
IOUtils.copy(in, out);
} finally {
out.close();
}
return putFile(key, file);
} finally {
in.close();
}
}
/**
* {@inheritDoc}
* <p>
* The key is used as a file name in the directory cache.
*/
@Override
public synchronized File putFile(String key, File file)
throws IllegalArgumentException, IOException {
// check the cache
LRUFileCacheEntry entry = cache.get(key);
if (entry != null) {
file.delete(); // tmp file not used
return entry.file;
}
// maybe the cache entry was just deleted but the file is still
// there?
File dest = files.get(key);
if (dest != null) {
// use not-yet-deleted file
return putFileInCache(key, dest);
}
// put file in cache with standard name
checkKey(key);
dest = new File(dir, key);
if (!file.renameTo(dest)) {
// already something there
file.delete();
}
return putFileInCache(key, dest);
}
/**
* Puts a file that's already in the correct filesystem location in the
* internal cache datastructures.
*/
protected File putFileInCache(String key, File file) {
// remove oldest entries until size fits
long size = file.length();
ensureCapacity(size);
// put new entry in cache
LRUFileCacheEntry entry = new LRUFileCacheEntry();
entry.size = size;
entry.file = file;
add(key, entry);
return file;
}
protected void checkKey(String key) throws IllegalArgumentException {
if (!SIMPLE_ASCII.matcher(key).matches() || ".".equals(key)
|| "..".equals(key)) {
throw new IllegalArgumentException("Invalid key: " + key);
}
}
@Override
public synchronized File getFile(String key) {
// check the cache
LRUFileCacheEntry entry = cache.get(key);
if (entry != null) {
// note access in most recently used list
recordAccess(key);
return entry.file;
}
// maybe the cache entry was just deleted but the file is still
// there?
return files.get(key);
}
@Override
public synchronized void clear() {
for (String key : lru) {
remove(key);
}
lru.clear();
cache.clear();
files.clear();
}
protected void recordAccess(String key) {
lru.remove(key); // TODO does a linear scan
lru.addFirst(key);
}
protected void add(String key, LRUFileCacheEntry entry) {
cache.put(key, entry);
files.put(key, entry.file);
lru.addFirst(key);
cacheSize += entry.size;
}
protected void remove(String key) {
LRUFileCacheEntry entry = cache.remove(key);
// don't remove from files here, the GC will do it
cacheSize -= entry.size;
// delete file when not referenced anymore
fileCleaningTracker.track(entry.file, entry.file);
}
protected void ensureCapacity(long size) {
while (cacheSize + size > maxSize && !lru.isEmpty()) {
remove(lru.removeLast());
}
}
}