| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.xmlgraphics.image.loader.cache; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.xml.transform.Source; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.xmlgraphics.image.loader.Image; |
| import org.apache.xmlgraphics.image.loader.ImageException; |
| import org.apache.xmlgraphics.image.loader.ImageFlavor; |
| import org.apache.xmlgraphics.image.loader.ImageInfo; |
| import org.apache.xmlgraphics.image.loader.ImageManager; |
| import org.apache.xmlgraphics.image.loader.ImageSessionContext; |
| import org.apache.xmlgraphics.image.loader.util.SoftMapCache; |
| |
| |
| /** |
| * This class provides a cache for images. The main key into the images is the original URI the |
| * image was accessed with. |
| * <p> |
| * Don't use one ImageCache instance in the context of multiple base URIs because relative URIs |
| * would not work correctly anymore. |
| * <p> |
| * By default, the URIs of inaccessible images are remembered but these entries are discarded |
| * after 60 seconds (which causes a retry next time the same URI is requested). This allows |
| * to counteract performance loss when accessing invalid or temporarily unavailable images |
| * over slow connections. |
| */ |
| public class ImageCache { |
| |
| /** logger */ |
| protected static Log log = LogFactory.getLog(ImageCache.class); |
| |
| //Handling of invalid URIs |
| private Map invalidURIs = Collections.synchronizedMap(new java.util.HashMap()); |
| private ExpirationPolicy invalidURIExpirationPolicy; |
| |
| //Actual image cache |
| private SoftMapCache imageInfos = new SoftMapCache(true); |
| private SoftMapCache images = new SoftMapCache(true); |
| |
| private ImageCacheListener cacheListener; |
| private TimeStampProvider timeStampProvider; |
| private long lastHouseKeeping; |
| |
| /** |
| * Default constructor with default settings. |
| */ |
| public ImageCache() { |
| this(new TimeStampProvider(), new DefaultExpirationPolicy()); |
| } |
| |
| /** |
| * Constructor for customized behaviour and testing. |
| * @param timeStampProvider the time stamp provider to use |
| * @param invalidURIExpirationPolicy the expiration policy for invalid URIs |
| */ |
| public ImageCache(TimeStampProvider timeStampProvider, |
| ExpirationPolicy invalidURIExpirationPolicy) { |
| this.timeStampProvider = timeStampProvider; |
| this.invalidURIExpirationPolicy = invalidURIExpirationPolicy; |
| this.lastHouseKeeping = this.timeStampProvider.getTimeStamp(); |
| } |
| |
| /** |
| * Sets an ImageCacheListener instance so the events in the image cache can be observed. |
| * @param listener the listener instance |
| */ |
| public void setCacheListener(ImageCacheListener listener) { |
| this.cacheListener = listener; |
| } |
| |
| /** |
| * Returns an ImageInfo instance for a given URI. |
| * @param uri the image's URI |
| * @param session the session context |
| * @param manager the ImageManager handling the images |
| * @return the ImageInfo instance |
| * @throws ImageException if an error occurs while parsing image data |
| * @throws IOException if an I/O error occurs while loading image data |
| */ |
| public ImageInfo needImageInfo(String uri, ImageSessionContext session, ImageManager manager) |
| throws ImageException, IOException { |
| //Fetch unique version of the URI and use it for synchronization so we have some sort of |
| //"row-level" locking instead of "table-level" locking (to use a database analogy). |
| //The fine locking strategy is necessary since preloading an image is a potentially long |
| //operation. |
| if (isInvalidURI(uri)) { |
| throw new FileNotFoundException("Image not found: " + uri); |
| } |
| String lockURI = uri.intern(); |
| synchronized (lockURI) { |
| ImageInfo info = getImageInfo(uri); |
| if (info == null) { |
| try { |
| Source src = session.needSource(uri); |
| if (src == null) { |
| registerInvalidURI(uri); |
| throw new FileNotFoundException("Image not found: " + uri); |
| } |
| info = manager.preloadImage(uri, src); |
| session.returnSource(uri, src); |
| } catch (IOException ioe) { |
| registerInvalidURI(uri); |
| throw ioe; |
| } catch (ImageException e) { |
| registerInvalidURI(uri); |
| throw e; |
| } |
| putImageInfo(info); |
| } |
| return info; |
| } |
| } |
| |
| /** |
| * Indicates whether a URI has previously been identified as an invalid URI. |
| * @param uri the image's URI |
| * @return true if the URI is invalid |
| */ |
| public boolean isInvalidURI(String uri) { |
| boolean expired = removeInvalidURIIfExpired(uri); |
| if (expired) { |
| return false; |
| } else { |
| if (cacheListener != null) { |
| cacheListener.invalidHit(uri); |
| } |
| return true; |
| } |
| } |
| |
| private boolean removeInvalidURIIfExpired(String uri) { |
| Long timestamp = (Long) invalidURIs.get(uri); |
| boolean expired = (timestamp == null) |
| || this.invalidURIExpirationPolicy.isExpired( |
| this.timeStampProvider, timestamp.longValue()); |
| if (expired) { |
| this.invalidURIs.remove(uri); |
| } |
| return expired; |
| } |
| |
| /** |
| * Returns an ImageInfo instance from the cache or null if none is found. |
| * @param uri the image's URI |
| * @return the ImageInfo instance or null if the requested information is not in the cache |
| */ |
| protected ImageInfo getImageInfo(String uri) { |
| ImageInfo info = (ImageInfo)imageInfos.get(uri); |
| if (cacheListener != null) { |
| if (info != null) { |
| cacheListener.cacheHitImageInfo(uri); |
| } else { |
| if (!isInvalidURI(uri)) { |
| cacheListener.cacheMissImageInfo(uri); |
| } |
| } |
| } |
| return info; |
| } |
| |
| /** |
| * Registers an ImageInfo instance with the cache. |
| * @param info the ImageInfo instance |
| */ |
| protected void putImageInfo(ImageInfo info) { |
| //An already existing ImageInfo is replaced. |
| imageInfos.put(info.getOriginalURI(), info); |
| } |
| |
| private static final long ONE_HOUR = 60 * 60 * 1000; |
| |
| /** |
| * Registers a URI as invalid so getImageInfo can indicate that quickly with no I/O access. |
| * @param uri the URI of the invalid image |
| */ |
| void registerInvalidURI(String uri) { |
| invalidURIs.put(uri, new Long(timeStampProvider.getTimeStamp())); |
| |
| considerHouseKeeping(); |
| } |
| |
| /** |
| * Returns an image from the cache or null if it wasn't found. |
| * @param info the ImageInfo instance representing the image |
| * @param flavor the requested ImageFlavor for the image |
| * @return the requested image or null if the image is not in the cache |
| */ |
| public Image getImage(ImageInfo info, ImageFlavor flavor) { |
| return getImage(info.getOriginalURI(), flavor); |
| } |
| |
| /** |
| * Returns an image from the cache or null if it wasn't found. |
| * @param uri the image's URI |
| * @param flavor the requested ImageFlavor for the image |
| * @return the requested image or null if the image is not in the cache |
| */ |
| public Image getImage(String uri, ImageFlavor flavor) { |
| if (uri == null || "".equals(uri)) { |
| return null; |
| } |
| ImageKey key = new ImageKey(uri, flavor); |
| Image img = (Image)images.get(key); |
| if (cacheListener != null) { |
| if (img != null) { |
| cacheListener.cacheHitImage(key); |
| } else { |
| cacheListener.cacheMissImage(key); |
| } |
| } |
| return img; |
| } |
| |
| /** |
| * Registers an image with the cache. |
| * @param img the image |
| */ |
| public void putImage(Image img) { |
| String originalURI = img.getInfo().getOriginalURI(); |
| if (originalURI == null || "".equals(originalURI)) { |
| return; //Don't cache if there's no URI |
| } |
| //An already existing Image is replaced. |
| if (!img.isCacheable()) { |
| throw new IllegalArgumentException( |
| "Image is not cacheable! (Flavor: " + img.getFlavor() + ")"); |
| } |
| ImageKey key = new ImageKey(originalURI, img.getFlavor()); |
| images.put(key, img); |
| } |
| |
| /** |
| * Clears the image cache (all ImageInfo and Image objects). |
| */ |
| public void clearCache() { |
| invalidURIs.clear(); |
| imageInfos.clear(); |
| images.clear(); |
| doHouseKeeping(); |
| } |
| |
| private void considerHouseKeeping() { |
| long ts = timeStampProvider.getTimeStamp(); |
| if (this.lastHouseKeeping + ONE_HOUR > ts) { |
| //Housekeeping is only triggered through registration of an invalid URI at the moment. |
| //Depending on the environment this could be triggered next to never. |
| //Doing this check for every image access could be relatively costly. |
| //The only alternative is a cleanup thread which is rather heavy-weight. |
| this.lastHouseKeeping = ts; |
| doHouseKeeping(); |
| } |
| } |
| |
| /** |
| * Triggers some house-keeping, i.e. removes stale entries. |
| */ |
| public void doHouseKeeping() { |
| imageInfos.doHouseKeeping(); |
| images.doHouseKeeping(); |
| doInvalidURIHouseKeeping(); |
| } |
| |
| private void doInvalidURIHouseKeeping() { |
| final Set currentEntries = new HashSet(this.invalidURIs.keySet()); |
| final Iterator iter = currentEntries.iterator(); |
| while (iter.hasNext()) { |
| final String key = (String) iter.next(); |
| removeInvalidURIIfExpired(key); |
| } |
| } |
| |
| } |