001/** 002 * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ 003 * 004 * This file is part of Dicoogle/dicoogle. 005 * 006 * Dicoogle/dicoogle is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Dicoogle/dicoogle is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Dicoogle. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package pt.ua.dicoogle.server.web.utils; 020 021import java.io.ByteArrayInputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.FileOutputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.net.URI; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.Paths; 032import java.util.Collection; 033import java.util.Objects; 034import java.util.concurrent.ConcurrentLinkedQueue; 035import java.util.concurrent.ConcurrentMap; 036import java.util.concurrent.ConcurrentSkipListMap; 037import pt.ua.dicoogle.server.web.dicom.Convert2PNG; 038 039/** 040 * Handles the caching of PNG images generated by the Image Servlet. 041 * The cached images are kept inside a temporary directory created inside the user (or system) temporary directory. 042 * These are maintained for a maximum period of time if they are not used, and deleted on a regular basis to save disk space while not hurting the cache performance. 043 * 044 * @author António Novo <antonio.novo@ua.pt> 045 */ 046public class LocalImageCache extends Thread implements ImageRetriever 047{ 048 /** 049 * The number of milliseconds to wait between pool cache directory pooling. 050 */ 051 private volatile int interval; 052 /** 053 * The number of milliseconds that a file can stay in the cache without being used/read. 054 */ 055 private volatile int maxAge; 056 057 private final File cacheFolder; 058 private volatile boolean running; 059 060 private final ConcurrentMap<String,File> filesBeingWritten; 061 062 private final ImageRetriever under; 063 064 /** 065 * Creates a local image cache that pools its cache directory at interval rates and deletes files older than maxAge. 066 * 067 * @param name the name of the cache directory. 068 * @param interval the number of seconds to wait between pool cache directory pooling. 069 * @param maxAge the number of seconds that a file can stay in the cache without being used/read. 070 * @param under the underlying image retriever 071 */ 072 public LocalImageCache(String name, int interval, int maxAge, ImageRetriever under) 073 { 074 super("cache-" + name); 075 Objects.requireNonNull(under); 076 077 if (interval < 1) { 078 this.interval = 1; 079 } else { 080 this.interval = interval; 081 } 082 this.interval *= 1000; 083 084 if (maxAge < 1) { 085 throw new IllegalArgumentException("Illegal maxAge"); 086 } 087 this.maxAge = maxAge * 1000; 088 089 filesBeingWritten = new ConcurrentSkipListMap<>(); 090 running = false; 091 092 this.setDaemon(true); 093 this.under = under; 094 095 // create the temporary directory 096 File sysTmpDir = new File(System.getProperty("java.io.tmpdir")); 097 cacheFolder = new File(sysTmpDir, name); 098 } 099 100 /** 101 * Deletes a directory and all its contents. 102 * 103 * @param dir the directory to delete. 104 */ 105 private static void deleteDirectory(File dir) 106 { 107 if ((dir == null) || (! dir.exists())) { 108 return; 109 } 110 111 // delete all the files and folders inside this folder 112 for (File f : dir.listFiles()) { 113 if (f.isDirectory()) { 114 // if it's a sub-directory then delete its content 115 deleteDirectory(f); 116 } else { 117 f.delete(); 118 } 119 } 120 121 // remove the folder 122 dir.delete(); 123 } 124 125 @Override 126 public void start() 127 { 128 // abort if we couldn't make the temp dir 129 if (cacheFolder == null) 130 return; 131 if (! cacheFolder.exists()) 132 if (! cacheFolder.mkdirs()) 133 return; 134 cacheFolder.deleteOnExit(); 135 136 // start running 137 super.start(); 138 } 139 140 @Override 141 public void run() 142 { 143 // if the cache isn't setup abort 144 if (! cacheFolder.exists()) 145 return; 146 147 running = true; 148 do 149 { 150 // check if there are any files worh deleting and if so do it 151 checkAndRemoveOldFiles(); 152 153 // wait for the defined interval to be over 154 try { 155 Thread.sleep(interval); 156 } catch (InterruptedException ex) { 157 // do nothing 158 } 159 } 160 while (isRunning()); 161 } 162 163 /** 164 * Stop this cache from checking for old files. 165 */ 166 public synchronized void terminate() 167 { 168 this.running = false; 169 170 // if needed wake the thread from its sleeping state 171 this.interrupt(); 172 173 // clear and delete the temporary folder 174 deleteDirectory(cacheFolder); 175 } 176 177 /** 178 * Loops through the cache folder and tries to removes old/un-used files. 179 */ 180 private synchronized void checkAndRemoveOldFiles() 181 { 182 // if the cache isn't setup abort 183 if (! cacheFolder.exists()) 184 return; 185 186 long currentTime = System.currentTimeMillis(); 187 188 // go through all the files inside the temp folder and check their last access 189 for (File f : cacheFolder.listFiles()) 190 { 191 // skip if the current file is a folder 192 if (f.isDirectory()) 193 continue; 194 195 // check the last access done to the file, and if it's "past its due" tries to delete it 196 if (currentTime - f.lastModified() > maxAge) 197 f.delete(); 198 } 199 } 200 201 /** 202 * @return the interval 203 */ 204 public int getInterval() 205 { 206 return interval; 207 } 208 209 /** 210 * @param interval the interval to set 211 */ 212 public void setInterval(int interval) 213 { 214 if (interval < 1) { 215 this.interval = 1; 216 } else { 217 this.interval = interval; 218 } 219 this.interval *= 1000; 220 } 221 222 /** 223 * @return the maxAge 224 */ 225 public int getMaxAge() 226 { 227 return maxAge; 228 } 229 230 /** 231 * @param maxAge the maxAge to set 232 */ 233 public void setMaxAge(int maxAge) 234 { 235 if (maxAge > 1) { 236 this.maxAge = maxAge; 237 } 238 } 239 240 protected static String toFileName(String imageUri, int frameNumber, boolean thumbnail) { 241 String filename = imageUri.replace('/', '_') + "_" + frameNumber; 242 filename = filename.replace(':', '_') ; // only for windows. 243 if (thumbnail) { 244 filename += "__thumb"; 245 } 246 return filename + ".png"; 247 } 248 249 @Override 250 public InputStream get(URI uri, final int frameNumber, final boolean thumbnail) throws IOException { 251 File f = this.implGetFile(toFileName(uri.toString(), frameNumber, thumbnail)); 252 synchronized(f) { 253 if (f.exists()) { 254 return new FileInputStream(f); 255 } 256 } 257 this.implCreateFile(f); 258 byte[] imageArray; 259 synchronized (f) { 260 InputStream istream = this.under.get(uri, frameNumber, thumbnail); 261 ByteArrayOutputStream result = Convert2PNG.DICOM2PNGStream(istream, frameNumber); 262 imageArray = result.toByteArray(); 263 264 // create new cache file with the converted image 265 try (FileOutputStream fout = new FileOutputStream(f)) { 266 fout.write(imageArray); 267 } 268 filesBeingWritten.remove(f.getAbsolutePath()); 269 } 270 271 // and return it 272 return new ByteArrayInputStream(imageArray); 273 } 274 275 private File implGetFile(String fileName) throws IOException { 276 // check if the file is currently being written to 277 String thatFilePath = new File(fileName).getAbsolutePath(); 278 File f = this.filesBeingWritten.get(thatFilePath); 279 if (f == null) { 280 f = new File(cacheFolder, fileName); 281 } 282 return f; 283 } 284 285 private File implCreateFile(File f) throws IOException { 286 //f.createNewFile(); 287 f.deleteOnExit(); 288 // if it does not exist add it to the list of files being written (because new content is going to be written to it outside this class) and create the empty file 289 filesBeingWritten.put(f.getAbsolutePath(), f); 290 // return the common handle to the currently created and empty file 291 return f; 292 } 293 294 /** 295 * @return if the caching mechanism for checking and removing old/un-used cache files is still running 296 */ 297 public boolean isRunning() 298 { 299 return running; 300 } 301}