View Javadoc
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  public class ThumbCache {
43  	private	static final boolean DEBUG = false;
44  	private	static final boolean DEBUGHITS = false;
45  	protected final Log logger = LogFactory.getLog(getClass());
46  
47  	private	PicManFacade	pmf;
48  	//private	DatabaseFacade	db;
49  	private	boolean		active = false;
50  	private	String		thumbExt = "jpg";
51  	private	int			cachedHeight = 0;
52  	private	String		cacheDirectory;
53  	private	int			maxEntriesPerCache = 1000;
54  	private	int			currentCacheFillDir = 0;
55  	private	int			currentCacheFillCount = 0;
56  	private	boolean		buildThumbCache = false;
57  	private	boolean		mosaicThumbCache = false;	// special rules
58  	private	int			mosaicSet = 0;				// if we are a mosaic cache, what set did we use?
59  
60  	private	HashMap<Pic, Thumbnail>			mosaicSuperCache = null;
61  	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  		this.pmf = pmf;
68  		//this.db = pmf.getDB();
69  	}
70  
71  	/** Are we wanting to build the thumb cache?
72  	 * @return the buildThumbCache flag
73  	 */
74  	public boolean isBuildThumbCache() {
75  		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  		return(this.cachedHeight);
83  	}
84  
85  	
86  	/**
87  	 * @return the mosaicSet
88  	 */
89  	public int getMosaicSet() {
90  		return mosaicSet;
91  	}
92  
93  	/**
94  	 * @param mosaicSet the mosaicSet to set
95  	 */
96  	public void setMosaicSet(int mosaicSet) {
97  		this.mosaicSet = mosaicSet;
98  		this.mosaicThumbCache = true;
99  		//this.mosaicSuperCache = new HashMap<Pic,Thumbnail>();
100 	}
101 
102 	/**
103 	 * @return the mosaicSuperCacheMaxSize
104 	 */
105 	public int getMosaicSuperCacheMaxSize() {
106 		return mosaicSuperCacheMaxSize;
107 	}
108 
109 	/** Release any cache buffers we are holding
110 	 */
111 	public void release() {
112 		this.mosaicSuperCache = null;
113 	}
114 	/**
115 	 * @param mosaicSuperCacheMaxSize the mosaicSuperCacheMaxSize to set
116 	 */
117 	public void setMosaicSuperCacheMaxSize(int mosaicSuperCacheMaxSize) {
118 		this.mosaicSuperCacheMaxSize = mosaicSuperCacheMaxSize;
119 	}
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 		this.buildThumbCache = buildThumbCache;
126 	}
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 		if (!active)
136 			return(null);
137 //		if (cachedHeight == 0)
138 //			prepareCache();
139 		if (height > cachedHeight)
140 			return(null);
141 		if (!this.mosaicThumbCache && pic.getCacheDir() == 0)
142 			return(null);
143 		if (this.mosaicSuperCache != null && this.mosaicSuperCache.containsKey(pic))
144 			return(this.mosaicSuperCache.get(pic));
145 		File f;
146 		if (this.mosaicThumbCache)
147 			f = new File(this.cacheDirectory,
148 						 pic.getName()+ "." + thumbExt);
149 		else
150 			f = new File(this.cacheDirectory + File.separator + pic.getCacheDir(),
151 						 pic.getName()+ "." + thumbExt);
152 		Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(thumbExt);
153 		ImageReader reader = (ImageReader)readers.next();
154 		BufferedImage bi;
155 		if (DEBUGHITS)
156 			logger.info("read: '" + f + "'");
157 		ImageInputStream iis = null;
158 		try {
159 			iis = ImageIO.createImageInputStream(f);
160 			if (DEBUGHITS)
161 				logger.info("cache " + (iis != null ? "hit " : "miss ") + pic.getName());
162 			if (iis == null) {
163 				return(null);
164 			}
165 			reader.setInput(iis, true);
166 			bi = reader.read(0);
167 			iis.close();
168 		} catch (IllegalStateException ise) {
169 			if (DEBUG)
170 				logger.info("caught IllegalStateException");
171 			bi = null;
172 			try {
173 				iis.close();
174 			} catch (IOException e) {}
175 			return(null);
176 		} catch (Exception e) {
177 			if (DEBUG)
178 				logger.info("caught Exception");
179 			bi = null;
180 			try {
181 				iis.close();
182 			} catch (IOException e1) {}
183 			return(null);
184 		}
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 		if (height < cachedHeight) {
189 			double	dW = ((double)height/(double)bi.getHeight()) * bi.getWidth();
190 			int newW = (int)dW;
191 			BufferedImage small = new BufferedImage(newW, height, BufferedImage.TYPE_INT_BGR);
192 			Graphics2D	g;
193 			g = small.createGraphics();
194 			g.drawImage(bi.getScaledInstance(-1, height, Image.SCALE_SMOOTH), null, null);
195 			bi = small;
196 		}
197 
198 		Thumbnail tn;
199 		tn = new Thumbnail();
200 		tn.setName(pic.getName());
201 		tn.setImage(bi);
202 		bi = null;
203 		if (this.mosaicSuperCache != null && this.mosaicSuperCache.size() < this.mosaicSuperCacheMaxSize)
204 			this.mosaicSuperCache.put(pic, tn);
205 		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 		if (tn.isXThumb())
214 			return;						// don't cache broken thumbs.
215 		if (!this.active)
216 			return;
217 
218 		boolean savePic = false;		// must flush pic if cache info changed
219 
220 		if (tn.getName() == null || tn.getName().length() < 1)
221 			throw new RuntimeException("Can't add thumb to cache with no name");
222 		if (tn.getImage().getHeight() != cachedHeight)
223 			return;
224 		if (!this.mosaicThumbCache && pic.getCacheDir() == 0) {
225 			pic.setCacheDir(this.currentCacheFillDir);
226 			savePic = true;
227 		}
228 		File file;
229 		if (this.mosaicThumbCache)
230 			file = new File(this.cacheDirectory,
231 							tn.getName() + "." + thumbExt);
232 		else
233 			file = new File(this.cacheDirectory + File.separator + this.currentCacheFillDir,
234 							tn.getName() + "." + thumbExt);
235 		if (!file.exists()) {
236 			Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(thumbExt);
237 			ImageWriter writer = (ImageWriter)writers.next();
238 			if (DEBUGHITS)
239 				logger.info("Writing '" + file.getPath() + "'");
240 			ImageOutputStream oos = null;
241 			try {
242 				oos = ImageIO.createImageOutputStream(file);
243 				writer.setOutput(oos);
244 				writer.write(tn.getImage());
245 				oos.close();
246 			} catch (IOException e) {
247 				if (oos != null)
248 					try {
249 						oos.close();
250 					} catch (IOException e1) {
251 					}
252 				Exception ex = new Exception("Failed to save cache thumb", e);
253 				pmf.addError(ex);
254 				return;
255 			}
256 		}
257 		if (savePic) {
258 			pmf.getDB().updatePic(pic);
259 			this.currentCacheFillCount++;
260 			if (this.currentCacheFillCount >= this.maxEntriesPerCache) {
261 				this.currentCacheFillDir++;
262 				prepareCacheDir();
263 				this.currentCacheFillCount = pmf.getDB().getPicThumbCacheFillCount(this.currentCacheFillDir);
264 				if (DEBUGHITS)
265 					logger.info("getPicThumbCacheFillCount returned " + this.currentCacheFillCount);
266 			}
267 		}
268 	}
269 
270 	/** Initialize the thumbCache */
271 	public	void	setupCache(String cacheDir, int cacheHeight, int maxEntriesPerCache) {
272 		this.cacheDirectory = cacheDir;
273 		this.cachedHeight = cacheHeight;
274 		this.maxEntriesPerCache = maxEntriesPerCache;
275 		this.active = pmf.getDB().getSystem().isUseThumbCache();
276 		if (!this.active)
277 			return;
278 
279 		File f;
280 		f = new File(this.cacheDirectory);
281 		if (!f.isDirectory()) {
282 			JobLogEntry jle = new JobLogEntry();
283 			jle.setType(JobLogEntry.INFO);
284 			jle.setName("Create Thumb cache");
285 			if (!f.mkdirs()) {
286 				jle.setName("Failed to create Thumb cache");
287 				jle.setError();
288 			}
289 			jle.setEndTime();
290 			pmf.addJobToLog(jle);
291 			if (jle.isError())
292 				return;
293 		}
294 		this.currentCacheFillDir = pmf.getDB().getPicMaxThumbCacheDirUsed();
295 		if (DEBUG)
296 			logger.info("getPicMaxThumbCacheDirUsed returned " + this.currentCacheFillDir);
297 		if (this.currentCacheFillDir == 0)
298 			this.currentCacheFillDir = 1;
299 		this.currentCacheFillCount = pmf.getDB().getPicThumbCacheFillCount(this.currentCacheFillDir);
300 		if (DEBUG)
301 			logger.info("getPicThumbCacheFillCount returned " + this.currentCacheFillCount);
302 		prepareCacheDir();
303 	}
304 	
305 	private void prepareCacheDir() {
306 //		if (this.currentCacheFillCount > this.maxEntriesPerCache)
307 //			this.currentCacheFillDir++;
308 		File dir = new File(this.cacheDirectory + File.separator + this.currentCacheFillDir);
309 		if (!dir.isDirectory()) {
310 			if (!dir.mkdir()) {
311 				// XXX: Error
312 			}
313 		}
314 	}
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 		JobLogEntry jle = new JobLogEntry();
323 		if (!mosaicThumbCache) {
324 			jle.setType(JobLogEntry.INFO);
325 			jle.setName("Delete Cache");
326 			pmf.addJobToLog(jle);
327 		}
328 		File f = new File(this.cacheDirectory);
329 		_deleteCache(f);
330 		if (mosaicThumbCache)
331 			return;
332 		List<Pic> list = pmf.getDB().getPics();
333 		Iterator<Pic> iter = list.iterator();
334 		Pic pic;
335 		while (iter.hasNext()) {
336 			pic = iter.next();
337 			if (pic.getCacheDir() != 0) {
338 				pic.setCacheDir(0);
339 				pmf.getDB().updatePic(pic);
340 			}
341 		}
342 		jle.setEndTime(new Date());
343 	}
344 
345 	private void _deleteCache(File f) {
346 		File[] list = f.listFiles();
347 		if (list == null)
348 			return;
349 		File ff;
350 		int	i;
351 		for (i=0; i<list.length; i++) {
352 			ff = list[i];
353 			if (ff.isDirectory())
354 				_deleteCache(ff);
355 			boolean res = ff.delete();
356 			if (!res)
357 				ff.delete();
358 			if (DEBUG) {
359 				if (!res)
360 					logger.info("Failed to delete '" + ff.getName() + "'");
361 			}
362 		}
363 	}
364 	
365 	/** Read each pic with the sole purpose of creating a cache entry for it.
366 	 */
367 	public void batchBuildCache() throws Exception {
368 		JobLogEntry jle = new JobLogEntry();
369 		jle.setType(JobLogEntry.INFO);
370 		jle.setName("Build Cache");
371 		pmf.addJobToLog(jle);
372 		List<Pic> list;
373 		if (this.mosaicThumbCache)
374 			list = pmf.getDB().getPics(pmf.getDB().getSet(this.mosaicSet), this.cachedHeight);
375 		else
376 			list = pmf.getDB().getPics();
377 		Iterator<Pic> iter = list.iterator();
378 		Pic pic = null;
379 		picProcessing = 0;
380 		try {
381 			while (iter.hasNext()) {
382 				pic = iter.next();
383 				picProcessing++;
384 				if (pic.getRid() == 0)
385 					continue;
386 				if (this.mosaicThumbCache) {
387 					if (DEBUG)
388 						logger.info("Create Mosaic cache entry for '" + pic.getName() + "'");
389 					Thumbnail tn = pmf.getPicReader().getThumbNail(pic, this.cachedHeight, null);
390 					if (this.mosaicThumbCache)
391 						this.addToCache(pic, tn);
392 					
393 				}
394 				else if (pic.getCacheDir() == 0) {
395 					if (DEBUG)
396 						logger.info("Create cache entry for '" + pic.getName() + "'");
397 					pmf.getPicReader().getThumbNail(pic, this.cachedHeight, null);
398 				}
399 			}
400 		} catch (Exception e) {
401 			logger.error("batchBuildCache failed on " + pic.getName());
402 			jle.setEndTime(new Date());
403 			throw new Exception("batchBuildCache failed on " + pic.getName(), e);
404 		}		
405 		jle.setEndTime(new Date());
406 	}
407 
408 	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 		return(picProcessing);
416 	}
417 }