001/* ***** BEGIN LICENSE BLOCK ***** 002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 003 * 004 * The contents of this file are subject to the Mozilla Public License Version 005 * 1.1 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * http://www.mozilla.org/MPL/ 008 * 009 * Software distributed under the License is distributed on an "AS IS" basis, 010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 011 * for the specific language governing rights and limitations under the 012 * License. 013 * 014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in 015 * Java(TM), hosted at https://github.com/gunterze/dcm4che. 016 * 017 * The Initial Developer of the Original Code is 018 * Agfa Healthcare. 019 * Portions created by the Initial Developer are Copyright (C) 2011-2014 020 * the Initial Developer. All Rights Reserved. 021 * 022 * Contributor(s): 023 * See @authors listed below 024 * 025 * Alternatively, the contents of this file may be used under the terms of 026 * either the GNU General Public License Version 2 or later (the "GPL"), or 027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 028 * in which case the provisions of the GPL or the LGPL are applicable instead 029 * of those above. If you wish to allow use of your version of this file only 030 * under the terms of either the GPL or the LGPL, and not to allow others to 031 * use your version of this file under the terms of the MPL, indicate your 032 * decision by deleting the provisions above and replace them with the notice 033 * and other provisions required by the GPL or the LGPL. If you do not delete 034 * the provisions above, a recipient may use your version of this file under 035 * the terms of any one of the MPL, the GPL or the LGPL. 036 * 037 * ***** END LICENSE BLOCK ***** */ 038 039package org.dcm4che3.filecache; 040 041import java.io.BufferedReader; 042import java.io.IOException; 043import java.nio.charset.Charset; 044import java.nio.file.DirectoryNotEmptyException; 045import java.nio.file.DirectoryStream; 046import java.nio.file.Files; 047import java.nio.file.Path; 048import java.nio.file.StandardOpenOption; 049import java.nio.file.attribute.FileTime; 050import java.text.SimpleDateFormat; 051import java.util.ArrayList; 052import java.util.Collection; 053import java.util.Collections; 054import java.util.Date; 055import java.util.TreeSet; 056import java.util.concurrent.atomic.AtomicBoolean; 057 058import org.slf4j.Logger; 059import org.slf4j.LoggerFactory; 060 061/** 062 * @author Gunter Zeilinger <gunterze@gmail.com> 063 * 064 */ 065public class FileCache { 066 067 private static final Logger LOG = LoggerFactory.getLogger(FileCache.class); 068 private static final Charset UTF_8 = Charset.forName("UTF-8"); 069 070 private Path fileCacheRootDirectory; 071 private Path journalRootDirectory; 072 private String journalFileName = "journal"; 073 private String orphanedFileName = "orphaned"; 074 private String journalDirectoryName = "journal.d"; 075 private SimpleDateFormat journalFileNamePattern = 076 new SimpleDateFormat("yyyyMMdd/HHmmss.SSS"); 077 private int journalMaxEntries = 100; 078 private boolean leastRecentlyUsed; 079 private int currentJournalNumEntries = -1; 080 private final AtomicBoolean freeIsRunning = new AtomicBoolean(); 081 082 public Path getFileCacheRootDirectory() { 083 return fileCacheRootDirectory; 084 } 085 086 public void setFileCacheRootDirectory(Path fileCacheRootDirectory) { 087 this.fileCacheRootDirectory = fileCacheRootDirectory; 088 } 089 090 public Path getJournalRootDirectory() { 091 return journalRootDirectory; 092 } 093 094 public void setJournalRootDirectory(Path journalRootDirectory) { 095 this.journalRootDirectory = journalRootDirectory; 096 } 097 098 public String getJournalFileName() { 099 return journalFileName; 100 } 101 102 public void setJournalFileName(String journalFileName) { 103 this.journalFileName = journalFileName; 104 } 105 106 public Path getJournalFile() { 107 return journalRootDirectory.resolve(journalFileName); 108 } 109 110 public String getJournalDirectoryName() { 111 return journalDirectoryName; 112 } 113 114 public void setJournalDirectoryName(String journalDirectoryName) { 115 this.journalDirectoryName = journalDirectoryName; 116 } 117 118 public Path getJournalDirectory() { 119 return journalRootDirectory.resolve(journalDirectoryName); 120 } 121 122 public String getJournalFileNamePattern() { 123 return journalFileNamePattern.toPattern(); 124 } 125 126 public void setJournalFileNamePattern(String pattern) { 127 this.journalFileNamePattern = new SimpleDateFormat(pattern); 128 } 129 130 public int getJournalMaxEntries() { 131 return journalMaxEntries; 132 } 133 134 public void setJournalMaxEntries(int journalMaxEntries) { 135 this.journalMaxEntries = journalMaxEntries; 136 } 137 138 public String getOrphanedFileName() { 139 return orphanedFileName; 140 } 141 142 public void setOrphanedFileName(String orphanedFileName) { 143 this.orphanedFileName = orphanedFileName; 144 } 145 146 public Path getOrphanedFile() { 147 return journalRootDirectory.resolve(orphanedFileName); 148 } 149 150 public Collection<Path> getOrphanedFiles() throws IOException { 151 Path orphanFile = getOrphanedFile(); 152 if (Files.notExists(orphanFile)) 153 return Collections.emptyList(); 154 155 ArrayList<Path> files = new ArrayList<Path>(); 156 try (BufferedReader r = Files.newBufferedReader( 157 orphanFile, UTF_8)) { 158 String fileName; 159 while ((fileName = r.readLine()) != null) { 160 files.add(fileCacheRootDirectory.resolve(fileName)); 161 } 162 } 163 return files; 164 } 165 166 public boolean isLeastRecentlyUsed() { 167 return leastRecentlyUsed; 168 } 169 170 public void setLeastRecentlyUsed(boolean leastRecentlyUsed) { 171 this.leastRecentlyUsed = leastRecentlyUsed; 172 } 173 174 @Override 175 public String toString() { 176 return "FileCache[cacheDir=" + fileCacheRootDirectory 177 + ", journalDir=" + journalRootDirectory + "]"; 178 } 179 180 public synchronized void register(Path path) throws IOException { 181 LOG.debug("{}: registering - {}", this, path); 182 Files.createDirectories(journalRootDirectory); 183 Path journalFile = getJournalFile(); 184 String entry = fileCacheRootDirectory.relativize(path).toString(); 185 int numEntries = currentJournalNumEntries; 186 if (numEntries < 0) 187 numEntries = countLines(journalFile); 188 if (numEntries >= journalMaxEntries) { 189 moveJournalFile(journalFile); 190 numEntries = 0; 191 } 192 Files.write(journalFile, Collections.singleton(entry), UTF_8, 193 StandardOpenOption.CREATE, StandardOpenOption.APPEND); 194 currentJournalNumEntries = numEntries + 1; 195 if (leastRecentlyUsed) { 196 try { 197 LOG.debug("{}: update modification time of - {}", this, path); 198 Files.setLastModifiedTime(path, Files.getLastModifiedTime(journalFile)); 199 } catch (IOException e) { 200 LOG.info("{}: failed to update modification time of - {}", this, path, e); 201 } 202 } 203 LOG.debug("{}: registered - {}", this, path); 204 } 205 206 private void moveJournalFile(Path journalFile) throws IOException { 207 Path target = getJournalDirectory().resolve( 208 journalFileNamePattern.format(new Date())); 209 LOG.debug("{}: maximal number of journal entries [{}] exeeded, move {} to {}", 210 this, journalMaxEntries, journalFile, target); 211 Files.createDirectories(target.getParent()); 212 Files.move(journalFile, target); 213 } 214 215 public boolean access(Path path) throws IOException { 216 if (!Files.exists(path)) 217 return false; 218 219 if (leastRecentlyUsed) 220 register(path); 221 222 return true; 223 } 224 225 public long free(long size) throws IOException { 226 LOG.info("{}: try to free {} bytes", this, size); 227 if (!freeIsRunning.compareAndSet(false, true)) { 228 LOG.info("{}: free already running", this); 229 return -1; 230 } 231 232 try { 233 long freed = free(getJournalDirectory(), size); 234 LOG.info("{}: freed {} bytes", this, freed); 235 return freed; 236 } finally { 237 freeIsRunning.set(false); 238 } 239 } 240 241 private long free(Path dir, long size) throws IOException { 242 long remaining = size; 243 for (Path file : listFiles(dir)) { 244 if (Files.isDirectory(file)) { 245 remaining -= free(file, remaining); 246 } else { 247 remaining -= free(file); 248 } 249 if (remaining <= 0) 250 break; 251 } 252 return size - remaining; 253 } 254 255 private Collection<Path> listFiles(Path dir) throws IOException { 256 TreeSet<Path> files = new TreeSet<Path>(); 257 try (DirectoryStream<Path> dirPath = Files.newDirectoryStream(dir)) { 258 for (Path path : dirPath) 259 files.add(path); 260 } 261 return files; 262 } 263 264 public void clear() throws IOException { 265 LOG.info("{}: clearing", this); 266 deleteDirContent(fileCacheRootDirectory); 267 deleteDirContent(journalRootDirectory); 268 LOG.info("{}: cleared", this); 269 } 270 271 private int countLines(Path journalFile) throws IOException { 272 int lines = 0; 273 if (Files.exists(journalFile)) 274 try (BufferedReader r = Files.newBufferedReader(journalFile, UTF_8)) { 275 while (r.readLine() != null) 276 lines ++; 277 } 278 return lines; 279 } 280 281 private static void deleteDirContent(Path dir) throws IOException { 282 if (Files.exists(dir)) { 283 try ( DirectoryStream<Path> in = Files.newDirectoryStream(dir) ) { 284 for (Path path : in) { 285 if (Files.isDirectory(path)) 286 deleteDirContent(path); 287 Files.delete(path); 288 } 289 } 290 } 291 } 292 293 private long free(Path journalFile) throws IOException { 294 LOG.debug("{}: deleting files referenced by journal - {}", 295 this, journalFile); 296 long freed = 0L; 297 FileTime lastModifiedTime = Files.getLastModifiedTime(journalFile); 298 try (BufferedReader r = Files.newBufferedReader( 299 journalFile, UTF_8)) { 300 String fileName; 301 while ((fileName = r.readLine()) != null) { 302 Path path = fileCacheRootDirectory.resolve(fileName); 303 if (Files.notExists(path)) { 304 LOG.debug("{}: {} already deleted"); 305 continue; 306 } 307 if (leastRecentlyUsed) { 308 try { 309 if (Files.getLastModifiedTime(path) 310 .compareTo(lastModifiedTime) > 0) { 311 LOG.debug("{}: {} recently accessed - do not delete", 312 this, path); 313 continue; 314 } 315 } catch (IOException e) { 316 LOG.info("{}: failed to get modification time of - {}", 317 this, path, e); 318 } 319 } 320 try { 321 LOG.debug("{}: delete - {}", this, path); 322 long fileSize = Files.size(path); 323 Files.delete(path); 324 freed += fileSize; 325 purgeEmptyDirectories(path.getParent(), fileCacheRootDirectory); 326 } catch (IOException e) { 327 LOG.warn("{}: failed to delete - {}", this, path, e); 328 try { 329 Path orphanedFile = getOrphanedFile(); 330 Files.write(orphanedFile, Collections.singleton(fileName), UTF_8, 331 StandardOpenOption.CREATE, StandardOpenOption.APPEND); 332 } catch (IOException e2) { 333 LOG.warn("{}: failed to record orphaned file - {}", 334 this, path, e2); 335 } 336 } 337 } 338 } 339 try { 340 LOG.debug("{}: delete journal - {}", this, journalFile); 341 Files.delete(journalFile); 342 purgeEmptyDirectories(journalFile.getParent(), getJournalDirectory()); 343 } catch (IOException e) { 344 LOG.warn("{}: failed to delete journal - {}", this, journalFile, e); 345 } 346 LOG.debug("{}: deleted files referenced by journal - {} - freed {} bytes", 347 this, journalFile, freed); 348 return freed; 349 } 350 351 private void purgeEmptyDirectories(Path dir, Path root) { 352 while (!dir.equals(root)) 353 try { 354 Files.delete(dir); 355 LOG.debug("{}: purged empty directory - {}", this, dir); 356 dir = dir.getParent(); 357 } catch (DirectoryNotEmptyException e) { 358 return; 359 } catch (IOException e) { 360 LOG.warn("{}: failed to purge empty directory {}", this, dir, e); 361 return; 362 } 363 } 364 365}