/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2011-2014
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4che3.filecache;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Gunter Zeilinger <gunterze@gmail.com>
*
*/
public class FileCache {
private static final Logger LOG = LoggerFactory.getLogger(FileCache.class);
private static final Charset UTF_8 = Charset.forName("UTF-8");
private Path fileCacheRootDirectory;
private Path journalRootDirectory;
private String journalFileName = "journal";
private String orphanedFileName = "orphaned";
private String journalDirectoryName = "journal.d";
private SimpleDateFormat journalFileNamePattern =
new SimpleDateFormat("yyyyMMdd/HHmmss.SSS");
private int journalMaxEntries = 100;
private boolean leastRecentlyUsed;
private int currentJournalNumEntries = -1;
private final AtomicBoolean freeIsRunning = new AtomicBoolean();
public Path getFileCacheRootDirectory() {
return fileCacheRootDirectory;
}
public void setFileCacheRootDirectory(Path fileCacheRootDirectory) {
this.fileCacheRootDirectory = fileCacheRootDirectory;
}
public Path getJournalRootDirectory() {
return journalRootDirectory;
}
public void setJournalRootDirectory(Path journalRootDirectory) {
this.journalRootDirectory = journalRootDirectory;
}
public String getJournalFileName() {
return journalFileName;
}
public void setJournalFileName(String journalFileName) {
this.journalFileName = journalFileName;
}
public Path getJournalFile() {
return journalRootDirectory.resolve(journalFileName);
}
public String getJournalDirectoryName() {
return journalDirectoryName;
}
public void setJournalDirectoryName(String journalDirectoryName) {
this.journalDirectoryName = journalDirectoryName;
}
public Path getJournalDirectory() {
return journalRootDirectory.resolve(journalDirectoryName);
}
public String getJournalFileNamePattern() {
return journalFileNamePattern.toPattern();
}
public void setJournalFileNamePattern(String pattern) {
this.journalFileNamePattern = new SimpleDateFormat(pattern);
}
public int getJournalMaxEntries() {
return journalMaxEntries;
}
public void setJournalMaxEntries(int journalMaxEntries) {
this.journalMaxEntries = journalMaxEntries;
}
public String getOrphanedFileName() {
return orphanedFileName;
}
public void setOrphanedFileName(String orphanedFileName) {
this.orphanedFileName = orphanedFileName;
}
public Path getOrphanedFile() {
return journalRootDirectory.resolve(orphanedFileName);
}
public Collection<Path> getOrphanedFiles() throws IOException {
Path orphanFile = getOrphanedFile();
if (Files.notExists(orphanFile))
return Collections.emptyList();
ArrayList<Path> files = new ArrayList<Path>();
try (BufferedReader r = Files.newBufferedReader(
orphanFile, UTF_8)) {
String fileName;
while ((fileName = r.readLine()) != null) {
files.add(fileCacheRootDirectory.resolve(fileName));
}
}
return files;
}
public boolean isLeastRecentlyUsed() {
return leastRecentlyUsed;
}
public void setLeastRecentlyUsed(boolean leastRecentlyUsed) {
this.leastRecentlyUsed = leastRecentlyUsed;
}
@Override
public String toString() {
return "FileCache[cacheDir=" + fileCacheRootDirectory
+ ", journalDir=" + journalRootDirectory + "]";
}
public synchronized void register(Path path) throws IOException {
LOG.debug("{}: registering - {}", this, path);
Files.createDirectories(journalRootDirectory);
Path journalFile = getJournalFile();
String entry = fileCacheRootDirectory.relativize(path).toString();
int numEntries = currentJournalNumEntries;
if (numEntries < 0)
numEntries = countLines(journalFile);
if (numEntries >= journalMaxEntries) {
moveJournalFile(journalFile);
numEntries = 0;
}
Files.write(journalFile, Collections.singleton(entry), UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
currentJournalNumEntries = numEntries + 1;
if (leastRecentlyUsed) {
try {
LOG.debug("{}: update modification time of - {}", this, path);
Files.setLastModifiedTime(path, Files.getLastModifiedTime(journalFile));
} catch (IOException e) {
LOG.info("{}: failed to update modification time of - {}", this, path, e);
}
}
LOG.debug("{}: registered - {}", this, path);
}
private void moveJournalFile(Path journalFile) throws IOException {
Path target = getJournalDirectory().resolve(
journalFileNamePattern.format(new Date()));
LOG.debug("{}: maximal number of journal entries [{}] exeeded, move {} to {}",
this, journalMaxEntries, journalFile, target);
Files.createDirectories(target.getParent());
Files.move(journalFile, target);
}
public boolean access(Path path) throws IOException {
if (!Files.exists(path))
return false;
if (leastRecentlyUsed)
register(path);
return true;
}
public long free(long size) throws IOException {
LOG.info("{}: try to free {} bytes", this, size);
if (!freeIsRunning.compareAndSet(false, true)) {
LOG.info("{}: free already running", this);
return -1;
}
try {
long freed = free(getJournalDirectory(), size);
LOG.info("{}: freed {} bytes", this, freed);
return freed;
} finally {
freeIsRunning.set(false);
}
}
private long free(Path dir, long size) throws IOException {
long remaining = size;
for (Path file : listFiles(dir)) {
if (Files.isDirectory(file)) {
remaining -= free(file, remaining);
} else {
remaining -= free(file);
}
if (remaining <= 0)
break;
}
return size - remaining;
}
private Collection<Path> listFiles(Path dir) throws IOException {
TreeSet<Path> files = new TreeSet<Path>();
try (DirectoryStream<Path> dirPath = Files.newDirectoryStream(dir)) {
for (Path path : dirPath)
files.add(path);
}
return files;
}
public void clear() throws IOException {
LOG.info("{}: clearing", this);
deleteDirContent(fileCacheRootDirectory);
deleteDirContent(journalRootDirectory);
LOG.info("{}: cleared", this);
}
private int countLines(Path journalFile) throws IOException {
int lines = 0;
if (Files.exists(journalFile))
try (BufferedReader r = Files.newBufferedReader(journalFile, UTF_8)) {
while (r.readLine() != null)
lines ++;
}
return lines;
}
private static void deleteDirContent(Path dir) throws IOException {
if (Files.exists(dir)) {
try ( DirectoryStream<Path> in = Files.newDirectoryStream(dir) ) {
for (Path path : in) {
if (Files.isDirectory(path))
deleteDirContent(path);
Files.delete(path);
}
}
}
}
private long free(Path journalFile) throws IOException {
LOG.debug("{}: deleting files referenced by journal - {}",
this, journalFile);
long freed = 0L;
FileTime lastModifiedTime = Files.getLastModifiedTime(journalFile);
try (BufferedReader r = Files.newBufferedReader(
journalFile, UTF_8)) {
String fileName;
while ((fileName = r.readLine()) != null) {
Path path = fileCacheRootDirectory.resolve(fileName);
if (Files.notExists(path)) {
LOG.debug("{}: {} already deleted");
continue;
}
if (leastRecentlyUsed) {
try {
if (Files.getLastModifiedTime(path)
.compareTo(lastModifiedTime) > 0) {
LOG.debug("{}: {} recently accessed - do not delete",
this, path);
continue;
}
} catch (IOException e) {
LOG.info("{}: failed to get modification time of - {}",
this, path, e);
}
}
try {
LOG.debug("{}: delete - {}", this, path);
long fileSize = Files.size(path);
Files.delete(path);
freed += fileSize;
purgeEmptyDirectories(path.getParent(), fileCacheRootDirectory);
} catch (IOException e) {
LOG.warn("{}: failed to delete - {}", this, path, e);
try {
Path orphanedFile = getOrphanedFile();
Files.write(orphanedFile, Collections.singleton(fileName), UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e2) {
LOG.warn("{}: failed to record orphaned file - {}",
this, path, e2);
}
}
}
}
try {
LOG.debug("{}: delete journal - {}", this, journalFile);
Files.delete(journalFile);
purgeEmptyDirectories(journalFile.getParent(), getJournalDirectory());
} catch (IOException e) {
LOG.warn("{}: failed to delete journal - {}", this, journalFile, e);
}
LOG.debug("{}: deleted files referenced by journal - {} - freed {} bytes",
this, journalFile, freed);
return freed;
}
private void purgeEmptyDirectories(Path dir, Path root) {
while (!dir.equals(root))
try {
Files.delete(dir);
LOG.debug("{}: purged empty directory - {}", this, dir);
dir = dir.getParent();
} catch (DirectoryNotEmptyException e) {
return;
} catch (IOException e) {
LOG.warn("{}: failed to purge empty directory {}", this, dir, e);
return;
}
}
}