| /* |
| * 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.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. |
| */ |
| public class ImageCache { |
| |
| /** logger */ |
| protected static Log log = LogFactory.getLog(ImageCache.class); |
| |
| private Set invalidURIs = Collections.synchronizedSet(new java.util.HashSet()); |
| |
| private SoftMapCache imageInfos = new SoftMapCache(true); |
| private SoftMapCache images = new SoftMapCache(true); |
| |
| private ImageCacheListener cacheListener; |
| |
| /** |
| * 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) { |
| if (invalidURIs.contains(uri)) { |
| if (cacheListener != null) { |
| cacheListener.invalidHit(uri); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * Registers a URI as invalid so getImageInfo can indicate that quickly with no I/O access. |
| * @param uri the URI of the invalid image |
| */ |
| private void registerInvalidURI(String uri) { |
| synchronized (invalidURIs) { |
| // cap size of invalid list |
| if (invalidURIs.size() > 100) { |
| invalidURIs.clear(); |
| } |
| invalidURIs.add(uri); |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * Triggers some house-keeping, i.e. removes stale entries. |
| */ |
| public void doHouseKeeping() { |
| imageInfos.doHouseKeeping(); |
| images.doHouseKeeping(); |
| } |
| |
| } |