/*******************************************************************************
* Copyright (c) 2000, 2004 IBM Corporation and others. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Common Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors: IBM Corporation - initial API and implementation
******************************************************************************/
package org.locationtech.udig.ui;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
/**
* A weakly referenced cache of image descriptors to arrays of image instances
* (representing normal, gray and disabled images). This is used to hold images
* in memory while their descriptors are defined. When the image descriptors
* become weakly referred to, the corresponding images in the array of images
* will be disposed.
*
* Weak references of equivalent image descriptors are mapped to the same array
* of images (where equivalent descriptors are <code>equals(Object)</code>).
*
* It is recommended to use this class as a singleton, since it creates a thread
* for cleaning out images.
*
* It is the responsibility of the user to ensure that the image descriptors are
* kept around as long as the images are needed. The users of this cache should
* not explicitly dispose the images.
*
* Upon request of a disabled or gray image, the normal image will be created as
* well (if it was not already in the cache) in order to create the disabled or
* gray version of the image.
*
* This cache makes no guarantees on how long the cleaning process will take, or
* when exactly it will occur.
*
*
* This class may be instantiated; it is not intended to be subclassed.
*
* @since 3.1
*/
public final class ImageCache {
/**
* An equivalent set of weak references to equivalent descriptors. The
* equivalence of image descriptors is determined through
* <code>equals(Object)</code>.
*
* @since 3.1
*/
private static final class EquivalenceSet {
/**
* The equivalence set's hash code is the hash code of the first weak
* reference added to the set.
*/
private final int equivalenceHashCode;
/**
* A list of weak references to equivalent image descriptors.
*/
private final ArrayList imageCacheWeakReferences;
/**
* Create an equivalence set and add the weak reference to the list.
*
* @param referenceToAdd
* The weak reference to add to the list of weak references.
*/
private EquivalenceSet(ImageCacheWeakReference referenceToAdd) {
imageCacheWeakReferences = new ArrayList();
imageCacheWeakReferences.add(referenceToAdd);
// The equivalence hash code will be the hash code of the first
// inserted weak reference
equivalenceHashCode = referenceToAdd.getCachedHashCode();
}
/**
* Add a weak refrence to the equivalence set. This method assumes that
* the reference to add does belong in this set.
*
* @param referenceToAdd
* The weak reference to add.
* @return true if the weak reference was added to the set, and false if
* the reference already exists in the set.
*/
public boolean addWeakReference(ImageCacheWeakReference referenceToAdd) {
// Only add the weak reference if it does not already exist
ImageCacheWeakReference weakReference = null;
for (Iterator i = imageCacheWeakReferences.iterator(); i.hasNext();) {
weakReference = (ImageCacheWeakReference) i.next();
// "referenceToAdd.get()" will not be null, but
// "weakReference.get()" could be null, which is ok since we
// should add the element since its "identity" will be removed
// shortly.
if (referenceToAdd.get() == weakReference.get()) {
return false;
}
}
imageCacheWeakReferences.add(referenceToAdd);
return true;
}
/**
* Clear the weak references in this equivalence set.
*
*/
public void clear() {
ImageCacheWeakReference currentReference = null;
for (Iterator i = imageCacheWeakReferences.iterator(); i.hasNext();) {
currentReference = (ImageCacheWeakReference) i.next();
// Cleaner thread could've have cleared the reference
if (currentReference != null) {
currentReference.clear();
}
}
imageCacheWeakReferences.clear();
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object object) {
// Two sets are equivalent if their descriptors
// are "equal"
ImageDescriptor reachableDescriptor = null;
if (!(object instanceof EquivalenceSet)) {
return false;
}
// Retrieve an image descriptor in the set of weak references
// that has not been enqueued
reachableDescriptor = ((EquivalenceSet) object)
.getFirstReachableDescriptor();
if (reachableDescriptor == null) {
return false;
}
// Manipulating descriptors themselves just in case the referent
// gets cleaned by the time we reach this part.
return reachableDescriptor.equals(getFirstReachableDescriptor());
}
/**
* Get a non-null image descriptor from the list of weak references to
* image descriptors.
*
* @return a non null image descriptor, or null if none could be found.
*/
public ImageDescriptor getFirstReachableDescriptor() {
ImageDescriptor referent = null;
for (Iterator i = imageCacheWeakReferences.iterator(); i.hasNext();) {
referent = (ImageDescriptor) ((ImageCacheWeakReference) i
.next()).get();
if (referent != null) {
// return descriptor itself. This way, we have a reference
// to it and it won't be cleared by the time we return from
// this method
return referent;
}
}
// no reachable descriptors found
return null;
}
/**
* Return the number of items in the list of weak references.
*
* @return the number of items in the list of weak references.
*/
public int getSize() {
return imageCacheWeakReferences.size();
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return equivalenceHashCode;
}
/**
* Remove a hashable weak reference from the list. This method makes no
* assumptions as to whether the reference to remove belongs in this
* equivalence set or not.
*
* @param referenceToRemove
* The weak reference to remove.
* @return true if the reference was removed succesfully.
*/
public boolean removeReference(ImageCacheWeakReference referenceToRemove) {
return imageCacheWeakReferences.remove(referenceToRemove);
}
}
/**
* A wrapper around the weak reference to imae descriptors in order to be
* able to store the referrent's hash code since it will be null when
* enqueued.
*
* @since 3.1
*/
private static final class ImageCacheWeakReference extends WeakReference {
/**
* Referent's hash code since it will not be available once the
* reference has been enqueued.
*/
private final int referentHashCode;
/**
* Creates a weak reference for an image descriptor.
*
* @param referent
* The image descriptor. Will not be <code>null</code>.
* @param queue
* The reference queue.
*/
public ImageCacheWeakReference(Object referent, ReferenceQueue queue) {
super(referent, queue);
referentHashCode = referent.hashCode();
}
/**
* The referent's cached hash code value.
*
* @return the referent's cached hash code value.
*/
public int getCachedHashCode() {
return referentHashCode;
}
}
/**
* An entry in the image map, which consists of the array of images (the
* value), as well as the key. This allows to retrieve BOTH the key (the
* equivalence set) and the value (the array of images) from the map
* directly.
*
* @since 3.1
*/
private static final class ImageMapEntry {
/**
* The array of images.
*/
private final Image[] entryImages;
/**
* The equivalence set.
*/
private final EquivalenceSet entrySet;
/**
* Create an entry that consists of the equivalence set (key) as well as
* the array of images.
*
* @param equivalenceSet
* The equivalence set.
* @param images
* The array of images.
*/
public ImageMapEntry(EquivalenceSet equivalenceSet, Image[] images) {
this.entrySet = equivalenceSet;
this.entryImages = images;
}
/**
* Return the equivalence set in this entry. Should not be
* <code>null</code>.
*
* @return the entry set.
*/
public EquivalenceSet getEquivalenceSet() {
return entrySet;
}
/**
* Return the array of images in this entry. Should not be
* <code>null</code>.
*
* @return the array of images.
*/
public Image[] getImages() {
return entryImages;
}
}
/**
* A thread for cleaning up the reference queues as the garbage collector
* fills them. It takes an image map and a reference queue. When an item
* appears in the reference queue, it uses it as a key to remove values from
* the map. If the value is an array of images, then the defined images in
* that array are is disposed. To shutdown the thread, call
* <code>stopCleaning()</code>.
*
* @since 3.1
*/
private static class ReferenceCleanerThread extends Thread {
/**
* The number of reference cleaner threads created.
*/
private static int threads = 0;
/**
* A marker indicating that the reference cleaner thread should exit.
* This is enqueued when the thread is told to stop. Any referenced
* enqueued after the thread is told to stop will not be cleaned up.
*/
private final ImageCacheWeakReference endMarker;
/**
* A map of equivalence sets to ImageMapEntry (Image[3],
* EquivalenceSet).
*/
private final Map imageMap;
/**
* The reference queue to check.
*/
private final ReferenceQueue referenceQueue;
private final StaleImages staleImages;
/**
* Constructs a new instance of <code>ReferenceCleanerThread</code>.
*
* @param referenceQueue
* The reference queue to check for garbage.
* @param map
* Map of equivalence sets to ImageMapEntry (Image[3],
* EquivalenceSet).
*/
private ReferenceCleanerThread(final ImageCache imageCache) {
super("Reference Cleaner: " + ++threads); //$NON-NLS-1$
this.referenceQueue = imageCache.imageReferenceQueue;
this.imageMap = imageCache.imageMap;
this.endMarker = new ImageCacheWeakReference(referenceQueue,
referenceQueue);
this.staleImages = imageCache.staleImages;
}
/**
* Remove the reference enqueued by iterating through the set of keys in
* the map.
*
* @param currentReference
* The current reference.
*/
private void removeReferenceEnqueued(
final ImageCacheWeakReference currentReference) {
EquivalenceSet currentSet = null;
Set keySet = imageMap.keySet();
Image[] images = null;
// Ensure that the image map is locked until the removal of the
// reference has finished
synchronized (imageMap) {
// Traverse the set of keys to find corresponding
// equivalence set
for (Iterator i = keySet.iterator(); i.hasNext();) {
currentSet = (EquivalenceSet) i.next();
boolean removed = currentSet
.removeReference(currentReference);
if (removed) {
// Clean up needed since the set is now empty
if (currentSet.getSize() == 0) {
images = ((ImageMapEntry) imageMap
.remove(currentSet)).getImages();
if (images == null) {
throw new NullPointerException(
"The array of images removed from the map on clean up should not be null."); //$NON-NLS-1$
}
}
// break out of for loop since the reference has
// been removed
break;
}
}
}
// Images need disposal
if (images != null) {
staleImages.addImagesToDispose(images);
// Run async to avoid deadlock from dispose
Display display = Display.getDefault();
if (display != null) {
display.asyncExec(new Runnable() {
public void run() {
staleImages.disposeStaleImages();
}
});
}
}
}
/**
* Wait for new garbage. When new garbage arrives, remove it, clear it,
* and dispose of any corresponding images.
*/
public final void run() {
while (true) {
// Get the next reference to dispose.
Reference reference = null;
// Block until a reference becomes available in the queue
try {
reference = referenceQueue.remove();
} catch (final InterruptedException e) {
// Reference will be null.
}
// Check to see if we've been told to stop.
if (reference == endMarker) {
// Clean up the image map
break;
}
// Image disposal - need to traverse the set of keys, since the
// image descriptor has been cleaned. No way to directly
// retrieve the equivalence set from the map . This could be
// improved (with better search/sort).
if (reference instanceof ImageCacheWeakReference) {
removeReferenceEnqueued((ImageCacheWeakReference) reference);
}
// Clear the reference.
if (reference != null) {
reference.clear();
}
}
}
/**
* Tells this thread to stop trying to clean up. This is usually run
* when the cache is shutting down.
*/
private final void stopCleaning() {
endMarker.enqueue();
}
}
/**
* A container class to hold a list of array of images that have been
* identified as requiring disposal. This class was added to ensure that if
* the image cache's dispose method is called while the cleaner thread is in
* the process of cleaning images, stopping the thread will not prevent
* those images from being disposed. They will be disposed by the image
* cache's dispose method.
*
*/
private static class StaleImages {
/**
* List of array of images the require disposal.
*/
private final List staleImages;
/**
* Create the list of stale images.
*
*/
public StaleImages() {
staleImages = Collections.synchronizedList(new ArrayList());
}
/**
* Add the array of images to the list of images to dispose. This is
* called only from the cleaner thread.
*
* @param images
* The array of images.
*/
public void addImagesToDispose(final Image[] images) {
staleImages.add(images);
}
/**
* Dispose images that require disposal.
*
*/
public void disposeStaleImages() {
Image[] imagesToDispose = null;
// Ensure only one thread at a time accesses the stale images list
synchronized (staleImages) {
for (Iterator i = staleImages.iterator(); i.hasNext();) {
imagesToDispose = (Image[]) i.next();
for (int j = 0; j < imagesToDispose.length; j++) {
final Image image = imagesToDispose[j];
if ((image != null) && (!image.isDisposed())) {
image.dispose();
}
}
}
staleImages.clear();
}
}
}
/**
* Types of images supported by the image cache.
*/
public static final int DISABLE = 0;
public static final int GRAY = 1;
public static final int REGULAR = 2;
private static final int TYPES_OF_IMAGES = 3;
/**
* The thread responsible for cleaning out images that are no longer needed.
* The images in Image[3] will be cleaned if the corresponding equivalence
* set contains no more weak references to image descriptor.
*/
private final ReferenceCleanerThread imageCleaner;
/**
* A map of equivalence sets to ImageMapEntry (Image[3], EquivalenceSet).
* The equivalence set represents a list of weakly referenced image
* descriptors that are equivalent ("equal"). The equivalence set will
* contain no duplicate image descriptor references (check for identical
* descriptors on addition using "==").
*/
private final Map imageMap;
/**
* A queue of references (<code>HashableWeakReference</code>) waiting to
* be garbage collected. This value is never <code>null</code>. This is
* the queue for <code>imageMap</code>.
*/
private final ReferenceQueue imageReferenceQueue;
/**
* The image to display when no image is available. This value is
* <code>null</code> until it is first used, and will not get disposed
* until the image cache itself is disposed.
*/
private Image missingImage = null;
/**
* Stale images that the cleaner thread might not have the opportunity to
* dispose. The latter images will be disposed by the image cache's dispose.
*/
private StaleImages staleImages;
/**
* Constructs a new instance of <code>ImageCache</code>, and starts a
* thread to monitor the reference queue for image clean up.
*/
public ImageCache() {
imageMap = Collections.synchronizedMap(new HashMap());
staleImages = new StaleImages();
imageReferenceQueue = new ReferenceQueue();
imageCleaner = new ReferenceCleanerThread(this);
imageCleaner.start();
}
/**
* Constructs a new instance of <code>ImageCache</code>, and starts a
* thread to monitor the reference queue for image clean up. If the passed
* initial load capacity is negative, the image map is created with the
* default <code>HashMap</code> constructor.
*
* @param initialLoadCapacity
* Initial load capacity for the image hash map.
*/
public ImageCache(final int initialLoadCapacity) {
if (initialLoadCapacity < 0) {
imageMap = Collections.synchronizedMap(new HashMap());
} else {
imageMap = Collections.synchronizedMap(new HashMap(
initialLoadCapacity));
}
staleImages = new StaleImages();
imageReferenceQueue = new ReferenceQueue();
imageCleaner = new ReferenceCleanerThread(this);
imageCleaner.start();
}
/**
* Constructs a new instance of <code>ImageCache</code>, and starts a
* thread to monitor the reference queue for image clean up. If the passed
* initial load capacity is negative or if the load factor is nonpositive,
* the image map is created with the default <code>HashMap</code>
* constructor.
*
* @param initialLoadCapacity
* Initial load capacity for the image hash map.
* @param loadFactor
* Load factor for the image hash map.
*/
public ImageCache(final int initialLoadCapacity, final float loadFactor) {
if (initialLoadCapacity < 0 || loadFactor <= 0) {
imageMap = Collections.synchronizedMap(new HashMap());
} else {
imageMap = Collections.synchronizedMap(new HashMap(
initialLoadCapacity, loadFactor));
}
staleImages = new StaleImages();
imageReferenceQueue = new ReferenceQueue();
imageCleaner = new ReferenceCleanerThread(this);
imageCleaner.start();
}
/**
* Add a new equivalence set to the imag map.
*
* @param imageDescriptor
* The image descriptor.
* @param temporaryKey
* The temporary key.
* @param typeOfImage
* The type of image requested.
* @return the requested image, or the missing image if an error occurs in
* the creation of the image.
*/
private Image addNewEquivalenceSet(final ImageDescriptor imageDescriptor,
EquivalenceSet equivalenceKey, int typeOfImage) {
// Create the array of images, as well as the regular image
// since it will be need to create gray or disable
final Image[] images = new Image[TYPES_OF_IMAGES];
images[REGULAR] = imageDescriptor.createImage(false);
// If the image creation fails, returns the missing image
if (images[REGULAR] == null) {
// clear the key (this will also clear the reference created)
equivalenceKey.clear();
return getMissingImage();
}
if (typeOfImage == DISABLE) {
images[typeOfImage] = new Image(null, images[REGULAR],
SWT.IMAGE_DISABLE);
} else if (typeOfImage == GRAY) {
images[typeOfImage] = new Image(null, images[REGULAR],
SWT.IMAGE_GRAY);
}
// Add the entry to the map
final ImageMapEntry mapEntry = new ImageMapEntry(equivalenceKey, images);
imageMap.put(equivalenceKey, mapEntry);
return images[typeOfImage];
}
/**
* Cleans up all images in the cache. This disposes of all of the images,
* and drops references to them. This should only be called when the images
* and the image cache are no longer needed (i.e.: shutdown). Note that the
* image disposal is handled by the cleaner thread.
*/
public final void dispose() {
// Clean up the missing image.
if ((missingImage != null) && (!missingImage.isDisposed())) {
missingImage.dispose();
missingImage = null;
}
// Stop the image cleaner thread
imageCleaner.stopCleaning();
try {
imageCleaner.join();
} catch (InterruptedException e) {
// Interrupted
}
// Clear all the references in the equivalence sets and
// dispose the corresponding images
for (Iterator imageItr = imageMap.entrySet().iterator(); imageItr
.hasNext();) {
final Map.Entry entry = (Map.Entry) imageItr.next();
final EquivalenceSet key = (EquivalenceSet) entry.getKey();
// Dispose the images if they have been created and have
// not been disposed yet
final Image[] images = ((ImageMapEntry) entry.getValue())
.getImages();
for (int i = 0; i < images.length; i++) {
final Image image = images[i];
if ((image != null) && (!image.isDisposed())) {
image.dispose();
}
}
// Clear all the references in the equivalence set
key.clear();
}
// Clear map
imageMap.clear();
// Clean up the stale images that the cleaner thread might have missed
staleImages.disposeStaleImages();
}
/**
* Returns the regular image for the given image descriptor. This caches the
* result so that future attempts to get the image for an equivalent or
* identical image descriptor will only access the cache. When all
* references to equivalent image descriptors are dropped, the images
* (regular, gray and disabled) will be cleaned up if they have been
* created. This clean up makes no guarantees about how long or when it will
* take place.
*
* @param descriptor
* The image descriptor with which a regular image should be
* created; may be <code>null</code>.
* @return The regular image, either newly created or from the cache. This
* value is <code>null</code> if the image descriptor passed in is
* <code>null</code>. Note that a missing image will be returned
* if a problem occurs in the creation of the image.
*/
public final Image getImage(final ImageDescriptor imageDescriptor) {
return getImage(imageDescriptor, REGULAR);
}
/**
* Returns the requested image for the given image descriptor and image
* type. This caches the result so that future attempts to get the image for
* an equivalent or identical image descriptor will only access the cache.
* When all references to equivalent image descriptors are dropped, the
* images (regular, gray and disabled) will be cleaned up if they have been
* created. This clean up makes no guarantees about how long or when it will
* take place.
*
* @param descriptor
* The image descriptor with which the requested image should be
* created; may be <code>null</code>.
* @param typeOfImage
* The type of the desired image:
* <code>ImageCache.DISABLED</code>,
* <code>ImageCache.GRAY</code> or
* <code>ImageCache.NORMAL</code>.
* @return The image for the requested image type, either newly created or
* from the cache. This value is <code>null</code> if the image
* descriptor passed in is <code>null</code>, or if the image
* type is invalid. Note that a missing image will be returned if a
* problem occurs in the creation of the image.
*/
public final Image getImage(final ImageDescriptor imageDescriptor,
final int typeOfImage) {
// Invalid descriptor
if (imageDescriptor == null) {
return null;
}
// Invalid type of image
if (typeOfImage < 0 || !(typeOfImage < TYPES_OF_IMAGES)) {
return null;
}
// Created a temporary key to query the image map
ImageCacheWeakReference referencedToAdd = new ImageCacheWeakReference(
imageDescriptor, imageReferenceQueue);
EquivalenceSet temporaryKey = new EquivalenceSet(referencedToAdd);
Image imageToReturn = null;
// Ensure that the image map is locked until the retrieving of the image
// process is finished
synchronized (imageMap) {
// Retrieve the corresponding entry in the map
ImageMapEntry mapEntry = (ImageMapEntry) imageMap.get(temporaryKey);
if (mapEntry != null) {
// The entry was found, retrieve the image from cache, or
// create it if it has not been created yet
imageToReturn = getImageFromEquivalenceSet(imageDescriptor,
mapEntry, referencedToAdd, typeOfImage);
} else {
// The entry was not found, create it.
imageToReturn = addNewEquivalenceSet(imageDescriptor,
temporaryKey, typeOfImage);
}
}
return imageToReturn;
}
/**
* Retrieve the image from the cache, or create it if it has not been
* created yet.
*
* @param imageDescriptor
* The image descriptor.
* @param mapEntry
* The mape entry.
* @param referenceToAdd
* The weak reference to add.
* @param typeOfImage
* The type of image to create.
* @return the requested image, or the missing image if an error occurs in
* the creation of the image.
*/
private Image getImageFromEquivalenceSet(ImageDescriptor imageDescriptor,
ImageMapEntry mapEntry, ImageCacheWeakReference referenceToAdd,
int typeOfImage) {
final Image[] images = mapEntry.getImages();
final EquivalenceSet equivalenceKey = mapEntry.getEquivalenceSet();
// Add the weak reference to the equivalence set
boolean added = equivalenceKey.addWeakReference(referenceToAdd);
if (!added) {
// The identical reference already exists in the set, clear it
referenceToAdd.clear();
}
// If the type of image requested is cached
if (images[typeOfImage] != null) {
return images[typeOfImage];
}
// Regular image shoudl not be null, since it gets created when the set
// is created
if (images[REGULAR] == null) {
throw new NullPointerException(
"The normal image from the equivalence set should not be null.");//$NON-NLS-1$
}
if (typeOfImage == GRAY) {
images[typeOfImage] = new Image(null, images[REGULAR],
SWT.IMAGE_GRAY);
} else if (typeOfImage == DISABLE) {
images[typeOfImage] = new Image(null, images[REGULAR],
SWT.IMAGE_DISABLE);
}
return images[typeOfImage];
}
/**
* Returns the image to display when no image can be found, or none is
* specified. This image is only disposed when the cache is disposed.
*
* @return The image to display for missing images. This value will never be
* <code>null</code>.
*/
public final Image getMissingImage() {
// Ensure that the missing image is not being accessed by another thread
if (missingImage == null) {
missingImage = ImageDescriptor.getMissingImageDescriptor()
.createImage();
}
return missingImage;
}
}