Coverage Report - com.buckosoft.PicMan.image.ThumbCache
 
Classes in this File Line Coverage Branch Coverage Complexity
ThumbCache
0%
0/205
0%
0/88
5.765
 
 1  
 /******************************************************************************
 2  
  * ThumbCache.java - Manage a cache of thumnails
 3  
  * 
 4  
  * PicMan - The BuckoSoft Picture Manager in Java
 5  
  * Copyright(c) 2006 - Dick Balaska
 6  
  * 
 7  
  */
 8  
 package com.buckosoft.PicMan.image;
 9  
 
 10  
 import java.awt.Graphics2D;
 11  
 import java.awt.Image;
 12  
 import java.awt.image.BufferedImage;
 13  
 import java.io.File;
 14  
 import java.io.IOException;
 15  
 import java.util.Date;
 16  
 import java.util.HashMap;
 17  
 import java.util.Iterator;
 18  
 import java.util.List;
 19  
 
 20  
 import javax.imageio.ImageIO;
 21  
 import javax.imageio.ImageReader;
 22  
 import javax.imageio.ImageWriter;
 23  
 import javax.imageio.stream.ImageInputStream;
 24  
 import javax.imageio.stream.ImageOutputStream;
 25  
 
 26  
 import org.apache.commons.logging.Log;
 27  
 import org.apache.commons.logging.LogFactory;
 28  
 
 29  
 import com.buckosoft.PicMan.business.PicManFacade;
 30  
 import com.buckosoft.PicMan.domain.JobLogEntry;
 31  
 import com.buckosoft.PicMan.domain.Pic;
 32  
 import com.buckosoft.PicMan.domain.Thumbnail;
 33  
 
 34  
 /** Manage the Thumb Cache.  If a Pic is requested that is equal to or smaller than the cache height,
 35  
  * see if there is a cached version on the disk.  It is <i>much</i> faster to read a 3KB file than
 36  
  * a 1MB file.
 37  
  *   
 38  
  * @author Dick Balaska
 39  
  * @since 2006/01/27
 40  
  * @see <a href="http://cvs.buckosoft.com/Projects/java/PicMan/PicMan/src/com/buckosoft/PicMan/image/ThumbCache.java">ThumbCache.java</a>
 41  
  */ 
 42  0
 public class ThumbCache {
 43  
         private        static final boolean DEBUG = false;
 44  
         private        static final boolean DEBUGHITS = false;
 45  0
         protected final Log logger = LogFactory.getLog(getClass());
 46  
 
 47  
         private        PicManFacade        pmf;
 48  
         //private        DatabaseFacade        db;
 49  0
         private        boolean                active = false;
 50  0
         private        String                thumbExt = "jpg";
 51  0
         private        int                        cachedHeight = 0;
 52  
         private        String                cacheDirectory;
 53  0
         private        int                        maxEntriesPerCache = 1000;
 54  0
         private        int                        currentCacheFillDir = 0;
 55  0
         private        int                        currentCacheFillCount = 0;
 56  0
         private        boolean                buildThumbCache = false;
 57  0
         private        boolean                mosaicThumbCache = false;        // special rules
 58  0
         private        int                        mosaicSet = 0;                                // if we are a mosaic cache, what set did we use?
 59  
 
 60  0
         private        HashMap<Pic, Thumbnail>                        mosaicSuperCache = null;
 61  0
         private        int                                                                mosaicSuperCacheMaxSize = 6300;
 62  
 
 63  
         /** Set the reference to the main API
 64  
          * @param pmf The PicManFacade
 65  
          */
 66  
         public        void setPicMan(PicManFacade pmf) {
 67  0
                 this.pmf = pmf;
 68  
                 //this.db = pmf.getDB();
 69  0
         }
 70  
 
 71  
         /** Are we wanting to build the thumb cache?
 72  
          * @return the buildThumbCache flag
 73  
          */
 74  
         public boolean isBuildThumbCache() {
 75  0
                 return buildThumbCache;
 76  
         }
 77  
 
 78  
         /** Fetch the size of the thumbnails that we are managing
 79  
          * @return The local cachedHeight
 80  
          */
 81  
         public int getCachedHeight() {
 82  0
                 return(this.cachedHeight);
 83  
         }
 84  
 
 85  
         
 86  
         /**
 87  
          * @return the mosaicSet
 88  
          */
 89  
         public int getMosaicSet() {
 90  0
                 return mosaicSet;
 91  
         }
 92  
 
 93  
         /**
 94  
          * @param mosaicSet the mosaicSet to set
 95  
          */
 96  
         public void setMosaicSet(int mosaicSet) {
 97  0
                 this.mosaicSet = mosaicSet;
 98  0
                 this.mosaicThumbCache = true;
 99  
                 //this.mosaicSuperCache = new HashMap<Pic,Thumbnail>();
 100  0
         }
 101  
 
 102  
         /**
 103  
          * @return the mosaicSuperCacheMaxSize
 104  
          */
 105  
         public int getMosaicSuperCacheMaxSize() {
 106  0
                 return mosaicSuperCacheMaxSize;
 107  
         }
 108  
 
 109  
         /** Release any cache buffers we are holding
 110  
          */
 111  
         public void release() {
 112  0
                 this.mosaicSuperCache = null;
 113  0
         }
 114  
         /**
 115  
          * @param mosaicSuperCacheMaxSize the mosaicSuperCacheMaxSize to set
 116  
          */
 117  
         public void setMosaicSuperCacheMaxSize(int mosaicSuperCacheMaxSize) {
 118  0
                 this.mosaicSuperCacheMaxSize = mosaicSuperCacheMaxSize;
 119  0
         }
 120  
 
 121  
         /** Build the thumb cache.  Signal to the batch manager to run the builder.
 122  
          * @param buildThumbCache the buildThumbCache to set
 123  
          */
 124  
         public void setBuildThumbCache(boolean buildThumbCache) {
 125  0
                 this.buildThumbCache = buildThumbCache;
 126  0
         }
 127  
 
 128  
         /** Get a cached Thumbnail. <br>
 129  
          * If the requested height is smaller than the cached height, then scale the Thumbnail to size. 
 130  
          * @param pic The Pic who's Thumbnail we want
 131  
          * @param height The height of the thumbnail we want
 132  
          * @return A Thumbnail, or null if not cached or if the cached size is smaller than the requested size.
 133  
          */
 134  
         public         Thumbnail        getThumbNail(Pic pic, int height) {
 135  0
                 if (!active)
 136  0
                         return(null);
 137  
 //                if (cachedHeight == 0)
 138  
 //                        prepareCache();
 139  0
                 if (height > cachedHeight)
 140  0
                         return(null);
 141  0
                 if (!this.mosaicThumbCache && pic.getCacheDir() == 0)
 142  0
                         return(null);
 143  0
                 if (this.mosaicSuperCache != null && this.mosaicSuperCache.containsKey(pic))
 144  0
                         return(this.mosaicSuperCache.get(pic));
 145  
                 File f;
 146  0
                 if (this.mosaicThumbCache)
 147  0
                         f = new File(this.cacheDirectory,
 148  0
                                                  pic.getName()+ "." + thumbExt);
 149  
                 else
 150  0
                         f = new File(this.cacheDirectory + File.separator + pic.getCacheDir(),
 151  0
                                                  pic.getName()+ "." + thumbExt);
 152  0
                 Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(thumbExt);
 153  0
                 ImageReader reader = (ImageReader)readers.next();
 154  
                 BufferedImage bi;
 155  
                 if (DEBUGHITS)
 156  
                         logger.info("read: '" + f + "'");
 157  0
                 ImageInputStream iis = null;
 158  
                 try {
 159  0
                         iis = ImageIO.createImageInputStream(f);
 160  
                         if (DEBUGHITS)
 161  
                                 logger.info("cache " + (iis != null ? "hit " : "miss ") + pic.getName());
 162  0
                         if (iis == null) {
 163  0
                                 return(null);
 164  
                         }
 165  0
                         reader.setInput(iis, true);
 166  0
                         bi = reader.read(0);
 167  0
                         iis.close();
 168  0
                 } catch (IllegalStateException ise) {
 169  
                         if (DEBUG)
 170  
                                 logger.info("caught IllegalStateException");
 171  0
                         bi = null;
 172  
                         try {
 173  0
                                 iis.close();
 174  0
                         } catch (IOException e) {}
 175  0
                         return(null);
 176  0
                 } catch (Exception e) {
 177  
                         if (DEBUG)
 178  
                                 logger.info("caught Exception");
 179  0
                         bi = null;
 180  
                         try {
 181  0
                                 iis.close();
 182  0
                         } catch (IOException e1) {}
 183  0
                         return(null);
 184  0
                 }
 185  
 
 186  
                 // Are we making a thumbnail of a thumbnail?  (like, for mosaic)
 187  
                 // If so, then rescale the cached thumbnail, which is much faster than reading the original big pic.
 188  0
                 if (height < cachedHeight) {
 189  0
                         double        dW = ((double)height/(double)bi.getHeight()) * bi.getWidth();
 190  0
                         int newW = (int)dW;
 191  0
                         BufferedImage small = new BufferedImage(newW, height, BufferedImage.TYPE_INT_BGR);
 192  
                         Graphics2D        g;
 193  0
                         g = small.createGraphics();
 194  0
                         g.drawImage(bi.getScaledInstance(-1, height, Image.SCALE_SMOOTH), null, null);
 195  0
                         bi = small;
 196  
                 }
 197  
 
 198  
                 Thumbnail tn;
 199  0
                 tn = new Thumbnail();
 200  0
                 tn.setName(pic.getName());
 201  0
                 tn.setImage(bi);
 202  0
                 bi = null;
 203  0
                 if (this.mosaicSuperCache != null && this.mosaicSuperCache.size() < this.mosaicSuperCacheMaxSize)
 204  0
                         this.mosaicSuperCache.put(pic, tn);
 205  0
                 return(tn);
 206  
         }
 207  
 
 208  
         /** Add a thumbnail to the cache
 209  
          * @param pic The pic that is being cached
 210  
          * @param tn The thumb to cache
 211  
          */
 212  
         public        void        addToCache(Pic pic, Thumbnail tn) {
 213  0
                 if (tn.isXThumb())
 214  0
                         return;                                                // don't cache broken thumbs.
 215  0
                 if (!this.active)
 216  0
                         return;
 217  
 
 218  0
                 boolean savePic = false;                // must flush pic if cache info changed
 219  
 
 220  0
                 if (tn.getName() == null || tn.getName().length() < 1)
 221  0
                         throw new RuntimeException("Can't add thumb to cache with no name");
 222  0
                 if (tn.getImage().getHeight() != cachedHeight)
 223  0
                         return;
 224  0
                 if (!this.mosaicThumbCache && pic.getCacheDir() == 0) {
 225  0
                         pic.setCacheDir(this.currentCacheFillDir);
 226  0
                         savePic = true;
 227  
                 }
 228  
                 File file;
 229  0
                 if (this.mosaicThumbCache)
 230  0
                         file = new File(this.cacheDirectory,
 231  0
                                                         tn.getName() + "." + thumbExt);
 232  
                 else
 233  0
                         file = new File(this.cacheDirectory + File.separator + this.currentCacheFillDir,
 234  0
                                                         tn.getName() + "." + thumbExt);
 235  0
                 if (!file.exists()) {
 236  0
                         Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(thumbExt);
 237  0
                         ImageWriter writer = (ImageWriter)writers.next();
 238  
                         if (DEBUGHITS)
 239  
                                 logger.info("Writing '" + file.getPath() + "'");
 240  0
                         ImageOutputStream oos = null;
 241  
                         try {
 242  0
                                 oos = ImageIO.createImageOutputStream(file);
 243  0
                                 writer.setOutput(oos);
 244  0
                                 writer.write(tn.getImage());
 245  0
                                 oos.close();
 246  0
                         } catch (IOException e) {
 247  0
                                 if (oos != null)
 248  
                                         try {
 249  0
                                                 oos.close();
 250  0
                                         } catch (IOException e1) {
 251  0
                                         }
 252  0
                                 Exception ex = new Exception("Failed to save cache thumb", e);
 253  0
                                 pmf.addError(ex);
 254  0
                                 return;
 255  0
                         }
 256  
                 }
 257  0
                 if (savePic) {
 258  0
                         pmf.getDB().updatePic(pic);
 259  0
                         this.currentCacheFillCount++;
 260  0
                         if (this.currentCacheFillCount >= this.maxEntriesPerCache) {
 261  0
                                 this.currentCacheFillDir++;
 262  0
                                 prepareCacheDir();
 263  0
                                 this.currentCacheFillCount = pmf.getDB().getPicThumbCacheFillCount(this.currentCacheFillDir);
 264  
                                 if (DEBUGHITS)
 265  
                                         logger.info("getPicThumbCacheFillCount returned " + this.currentCacheFillCount);
 266  
                         }
 267  
                 }
 268  0
         }
 269  
 
 270  
         /** Initialize the thumbCache */
 271  
         public        void        setupCache(String cacheDir, int cacheHeight, int maxEntriesPerCache) {
 272  0
                 this.cacheDirectory = cacheDir;
 273  0
                 this.cachedHeight = cacheHeight;
 274  0
                 this.maxEntriesPerCache = maxEntriesPerCache;
 275  0
                 this.active = pmf.getDB().getSystem().isUseThumbCache();
 276  0
                 if (!this.active)
 277  0
                         return;
 278  
 
 279  
                 File f;
 280  0
                 f = new File(this.cacheDirectory);
 281  0
                 if (!f.isDirectory()) {
 282  0
                         JobLogEntry jle = new JobLogEntry();
 283  0
                         jle.setType(JobLogEntry.INFO);
 284  0
                         jle.setName("Create Thumb cache");
 285  0
                         if (!f.mkdirs()) {
 286  0
                                 jle.setName("Failed to create Thumb cache");
 287  0
                                 jle.setError();
 288  
                         }
 289  0
                         jle.setEndTime();
 290  0
                         pmf.addJobToLog(jle);
 291  0
                         if (jle.isError())
 292  0
                                 return;
 293  
                 }
 294  0
                 this.currentCacheFillDir = pmf.getDB().getPicMaxThumbCacheDirUsed();
 295  
                 if (DEBUG)
 296  
                         logger.info("getPicMaxThumbCacheDirUsed returned " + this.currentCacheFillDir);
 297  0
                 if (this.currentCacheFillDir == 0)
 298  0
                         this.currentCacheFillDir = 1;
 299  0
                 this.currentCacheFillCount = pmf.getDB().getPicThumbCacheFillCount(this.currentCacheFillDir);
 300  
                 if (DEBUG)
 301  
                         logger.info("getPicThumbCacheFillCount returned " + this.currentCacheFillCount);
 302  0
                 prepareCacheDir();
 303  0
         }
 304  
         
 305  
         private void prepareCacheDir() {
 306  
 //                if (this.currentCacheFillCount > this.maxEntriesPerCache)
 307  
 //                        this.currentCacheFillDir++;
 308  0
                 File dir = new File(this.cacheDirectory + File.separator + this.currentCacheFillDir);
 309  0
                 if (!dir.isDirectory()) {
 310  0
                         if (!dir.mkdir()) {
 311  
                                 // XXX: Error
 312  
                         }
 313  
                 }
 314  0
         }
 315  
         
 316  
         /** Delete the thumb cache. <br>
 317  
          * Delete all of the files and subdirectories under the thumb cache directory. <br>
 318  
          * If we are not a mosaicThumbCache, which is an auxillary cache, 
 319  
          *                then set to 0 all of the cacheDir entries for each pic.
 320  
          */
 321  
         public void deleteCache() {
 322  0
                 JobLogEntry jle = new JobLogEntry();
 323  0
                 if (!mosaicThumbCache) {
 324  0
                         jle.setType(JobLogEntry.INFO);
 325  0
                         jle.setName("Delete Cache");
 326  0
                         pmf.addJobToLog(jle);
 327  
                 }
 328  0
                 File f = new File(this.cacheDirectory);
 329  0
                 _deleteCache(f);
 330  0
                 if (mosaicThumbCache)
 331  0
                         return;
 332  0
                 List<Pic> list = pmf.getDB().getPics();
 333  0
                 Iterator<Pic> iter = list.iterator();
 334  
                 Pic pic;
 335  0
                 while (iter.hasNext()) {
 336  0
                         pic = iter.next();
 337  0
                         if (pic.getCacheDir() != 0) {
 338  0
                                 pic.setCacheDir(0);
 339  0
                                 pmf.getDB().updatePic(pic);
 340  
                         }
 341  
                 }
 342  0
                 jle.setEndTime(new Date());
 343  0
         }
 344  
 
 345  
         private void _deleteCache(File f) {
 346  0
                 File[] list = f.listFiles();
 347  0
                 if (list == null)
 348  0
                         return;
 349  
                 File ff;
 350  
                 int        i;
 351  0
                 for (i=0; i<list.length; i++) {
 352  0
                         ff = list[i];
 353  0
                         if (ff.isDirectory())
 354  0
                                 _deleteCache(ff);
 355  0
                         boolean res = ff.delete();
 356  0
                         if (!res)
 357  0
                                 ff.delete();
 358  
                         if (DEBUG) {
 359  
                                 if (!res)
 360  
                                         logger.info("Failed to delete '" + ff.getName() + "'");
 361  
                         }
 362  
                 }
 363  0
         }
 364  
         
 365  
         /** Read each pic with the sole purpose of creating a cache entry for it.
 366  
          */
 367  
         public void batchBuildCache() throws Exception {
 368  0
                 JobLogEntry jle = new JobLogEntry();
 369  0
                 jle.setType(JobLogEntry.INFO);
 370  0
                 jle.setName("Build Cache");
 371  0
                 pmf.addJobToLog(jle);
 372  
                 List<Pic> list;
 373  0
                 if (this.mosaicThumbCache)
 374  0
                         list = pmf.getDB().getPics(pmf.getDB().getSet(this.mosaicSet), this.cachedHeight);
 375  
                 else
 376  0
                         list = pmf.getDB().getPics();
 377  0
                 Iterator<Pic> iter = list.iterator();
 378  0
                 Pic pic = null;
 379  0
                 picProcessing = 0;
 380  
                 try {
 381  0
                         while (iter.hasNext()) {
 382  0
                                 pic = iter.next();
 383  0
                                 picProcessing++;
 384  0
                                 if (pic.getRid() == 0)
 385  0
                                         continue;
 386  0
                                 if (this.mosaicThumbCache) {
 387  
                                         if (DEBUG)
 388  
                                                 logger.info("Create Mosaic cache entry for '" + pic.getName() + "'");
 389  0
                                         Thumbnail tn = pmf.getPicReader().getThumbNail(pic, this.cachedHeight, null);
 390  0
                                         if (this.mosaicThumbCache)
 391  0
                                                 this.addToCache(pic, tn);
 392  
                                         
 393  0
                                 }
 394  0
                                 else if (pic.getCacheDir() == 0) {
 395  
                                         if (DEBUG)
 396  
                                                 logger.info("Create cache entry for '" + pic.getName() + "'");
 397  0
                                         pmf.getPicReader().getThumbNail(pic, this.cachedHeight, null);
 398  
                                 }
 399  
                         }
 400  0
                 } catch (Exception e) {
 401  0
                         logger.error("batchBuildCache failed on " + pic.getName());
 402  0
                         jle.setEndTime(new Date());
 403  0
                         throw new Exception("batchBuildCache failed on " + pic.getName(), e);
 404  0
                 }                
 405  0
                 jle.setEndTime(new Date());
 406  0
         }
 407  
 
 408  0
         private int picProcessing = 0;
 409  
         
 410  
         /** Return which pic number we are building during batchBuildCache.
 411  
          * This is just a status indicator.
 412  
          * @return The index into the list as we process pics.
 413  
          */
 414  
         public int getPicProcessing() {
 415  0
                 return(picProcessing);
 416  
         }
 417  
 }