// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: Cache2.java,v 1.1 2006/11/17 11:15:17 spyromus Exp $
//
package com.salas.bb.utils.uif.images;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ImageConsumer;
import java.awt.image.ColorModel;
import java.lang.ref.SoftReference;
import com.salas.bb.utils.concurrency.ExecutorFactory;
import com.salas.bb.utils.i18n.Strings;
import javax.imageio.ImageIO;
/**
* Cache of images. The cache is disk-based, but it also has memory map of images which are
* currently loaded by someone. The cache uses separate thread for flushing of images to disk.
* The maximum disk usage amount can be specified and the writer thread will check if current
* saved data is not exceeding the limit after writing the next image. If the limits are exceeded
* it will remove several last-used images to get back into the limits.
*/
public class Cache2
{
private static final Logger LOG = Logger.getLogger(Cache2.class.getName());
private File cacheFolder;
private long sizeLimit;
private Executor executor;
/**
* Creates cache.
*
* @param aCacheFolder folder to use for caching.
* @param aSizeLimit maximum folder contents size.
*/
public Cache2(File aCacheFolder, long aSizeLimit)
{
cacheFolder = aCacheFolder;
sizeLimit = aSizeLimit;
executor = ExecutorFactory.createPooledExecutor("Cached Images Writer", 1, 10000);
if (!cacheFolder.exists()) cacheFolder.mkdir();
}
/**
* Puts the image into cache.
*
* @param url URL of the image.
* @param image image to cache.
*/
public void put(URL url, Image image)
{
if (url == null || image == null || isLocal(url)) return;
// Record in memory map
recordMemoryImage(image, url);
if (LOG.isLoggable(Level.FINE)) LOG.fine("Put " + url);
image.getSource().addConsumer(new Cache2.ImageWaiter(url, image));
}
/**
* Invoked when image is ready for writing.
*
* @param url URL of the image.
* @param image image to cache.
*/
private void onImageReady(URL url, Image image)
{
Cache2.WriteTask writeTask = new Cache2.WriteTask(url, image);
try
{
executor.execute(writeTask);
} catch (InterruptedException e)
{
LOG.warning(Strings.error("img.image.caching.executed.directly"));
writeTask.run();
}
}
/**
* Gets the image from cache.
*
* @param url URL of the image.
*
* @return image or <code>NULL</code>.
*/
public Image get(URL url)
{
if (url == null) return null;
// Check the memory references list
Image image = lookupMemoryImage(url);
// Return <code>NULL</code> if the link is local or image if it was found
if (image != null || isLocal(url)) return image;
File file = new File(cacheFolder, urlToFilename(url));
if (file.exists())
{
try
{
url = file.toURI().toURL();
image = ImageFetcher.load(url);
file.setLastModified(System.currentTimeMillis());
// Record loaded image in the memory map
recordMemoryImage(image, url);
} catch (IOException e)
{
LOG.log(Level.SEVERE, Strings.error("img.failed.to.load.image"), e);
}
}
return image;
}
private static boolean isLocal(URL url)
{
return url.getProtocol().equals("file");
}
/**
* Converts URL into cached file name.
*
* @param url URL.
*
* @return file name.
*/
private static String urlToFilename(URL url)
{
int hashCode = url.toString().toLowerCase().hashCode();
return Integer.toHexString(hashCode).toUpperCase();
}
/**
* Task for the writer thread.
*/
private class WriteTask implements Runnable
{
private URL url;
private Image image;
/**
* Creates task.
*
* @param aUrl URL of the image.
* @param aImage image to write.
*/
public WriteTask(URL aUrl, Image aImage)
{
url = aUrl;
image = aImage;
}
/**
* Writes image data to disk.
*/
public void run()
{
File file = new File(cacheFolder, urlToFilename(url));
if (file.exists()) return;
if (LOG.isLoggable(Level.FINE)) LOG.fine("Writing " + url);
boolean created = false;
try
{
// Reserve file
created = file.createNewFile();
BufferedImage buf = null;
if (image instanceof BufferedImage)
{
buf = (BufferedImage)image;
} else
{
int width = image.getWidth(null);
int height = image.getHeight(null);
// We save only non-empty images
if (width > 0 && height > 0)
{
buf = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = buf.createGraphics();
g.drawImage(image, 0, 0, null);
}
}
// If buffer was successfully created and filled
if (buf != null)
{
ImageIO.write(buf, "PNG", file);
verifyLimits();
}
} catch (Throwable e)
{
LOG.log(Level.WARNING, Strings.error("img.cache.writing.failed"), e);
if (created) file.delete();
}
}
/** Checks if we still in limits and removes some old files if we aren't. */
private void verifyLimits()
{
long size = calcSize();
if (size > sizeLimit)
{
removeOldEntries(size - sizeLimit);
}
}
/**
* Removes some entries to free minimum <code>size</code> number of bytes.
*
* @param size amount to free.
*/
private void removeOldEntries(long size)
{
File[] files = cacheFolder.listFiles();
Arrays.sort(files, new Cache2.FileAccessComparator());
long leftToFree = size;
for (int i = 0; leftToFree > 0 && i < files.length - 1; i++)
{
File file = files[i];
if (file.isFile() && file.exists())
{
if (file.delete()) leftToFree -= file.length();
}
}
}
/**
* Returns the size of directory contents.
*
* @return size.
*/
private long calcSize()
{
long size = 0;
File[] files = cacheFolder.listFiles();
for (int i = 0; i < files.length; i++)
{
File file = files[i];
size += file.isFile() ? file.length() : 0;
}
return size;
}
}
/**
* Compares files by their modification times.
*/
private static class FileAccessComparator implements Comparator
{
/**
* Compares two files by their modification times.
*
* @param o1 first file.
* @param o2 second file.
*
* @return result.
*/
public int compare(Object o1, Object o2)
{
File f1 = (File)o1;
File f2 = (File)o2;
long l1 = f1.lastModified();
long l2 = f2.lastModified();
return l1 == l2 ? 0 : l1 < l2 ? -1 : 1;
}
}
/**
* Waits for image to be loaded and calls engine to save it on to drive.
*/
private class ImageWaiter implements ImageConsumer
{
private final URL imageURL;
private final Image image;
/**
* Creates consumer-waiter.
*
* @param aImageURL original image URL.
* @param anImage image object.
*/
public ImageWaiter(URL aImageURL, Image anImage)
{
imageURL = aImageURL;
image = anImage;
}
/**
* The imageComplete method is called when the ImageProducer is finished delivering all of
* the pixels that the source image contains, or when a single frame of a multi-frame
* animation has been completed, or when an error in loading or producing the image has
* occured. The ImageConsumer should remove itself from the list of consumers registered
* with the ImageProducer at this time, unless it is interested in successive frames.
*
* @param status the status of image loading.
*/
public void imageComplete(int status)
{
if (status == STATICIMAGEDONE || status == SINGLEFRAMEDONE)
{
image.getSource().removeConsumer(this);
if (status == STATICIMAGEDONE) onImageReady(imageURL, image);
}
}
/**
* Sets the ColorModel object used for the majority of the pixels reported using the
* setPixels method calls.
*
* @param model the specified <code>ColorModel</code>.
*/
public void setColorModel(ColorModel model)
{
}
/**
* The dimensions of the source image are reported using the setDimensions method call.
*
* @param width the width of the source image.
* @param height the height of the source image.
*/
public void setDimensions(int width, int height)
{
}
/**
* Sets the hints that the ImageConsumer uses to process the pixels delivered by the
* ImageProducer.
*
* @param hintflags a set of hints that the ImageConsumer uses to process the pixels.
*/
public void setHints(int hintflags)
{
}
/**
* Delivers the pixels of the image with one or more calls to this method.
*
* @param x the coordinate of the upper-left corner of the area of pixels to be set.
* @param y the coordinate of the upper-left corner of the area of pixels to be set.
* @param w the width of the area of pixels.
* @param h the height of the area of pixels.
* @param model the specified <code>ColorModel</code>.
* @param pixels the array of pixels.
* @param off the offset into the <code>pixels</code> array.
* @param scansize the distance from one row of pixels to the next in the.
* <code>pixels</code> array.
*/
public void setPixels(int x, int y, int w, int h, ColorModel model, byte pixels[], int off,
int scansize)
{
}
/**
* The pixels of the image are delivered using one or more calls to the setPixels method.
*
* @param x the coordinate of the upper-left corner of the area of pixels to be set.
* @param y the coordinate of the upper-left corner of the area of pixels to be set.
* @param w the width of the area of pixels.
* @param h the height of the area of pixels.
* @param model the specified <code>ColorModel</code>.
* @param pixels the array of pixels.
* @param off the offset into the <code>pixels</code> array.
* @param scansize the distance from one row of pixels to the next in the
* <code>pixels</code> array.
*/
public void setPixels(int x, int y, int w, int h, ColorModel model, int pixels[], int off,
int scansize)
{
}
/**
* Sets the extensible list of properties associated with this image.
*
* @param props the list of properties to be associated with this image.
*/
public void setProperties(Hashtable props)
{
}
}
// --------------------------------------------------------------------------------------------
// Memory map for loaded images
// --------------------------------------------------------------------------------------------
// Memory map
private final Map memoryMap = new HashMap();
/**
* Records an image in the memory map.
*
* @param image image.
* @param url its URL.
*/
private void recordMemoryImage(Image image, URL url)
{
if (image == null || url == null) return;
String key = url.toString();
synchronized (memoryMap)
{
memoryMap.put(key, new SoftReference(image));
}
cleanMemoryMap();
}
/**
* Looks up an image by its URL.
*
* @param url URL of an image.
*
* @return image or <code>NULL</code>.
*/
private Image lookupMemoryImage(URL url)
{
Image image = null;
if (url != null)
{
String key = url.toString();
synchronized (memoryMap)
{
SoftReference r = (SoftReference)memoryMap.get(key);
image = r == null ? null : (Image)r.get();
}
}
return image;
}
/** Runs through the memory map and removes zombi-records. */
private void cleanMemoryMap()
{
synchronized (memoryMap)
{
Iterator it = memoryMap.entrySet().iterator();
while (it.hasNext())
{
Map.Entry entry = (Map.Entry)it.next();
SoftReference r = (SoftReference)entry.getValue();
if (r.get() == null) it.remove();
}
}
}
}