/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community 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.osedu.org/licenses/ECL-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. */ package tufts.vue; import tufts.Util; import java.net.URI; import java.net.URL; import java.io.File; /** this class in support of image loading in Images.java */ class ImageSource { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ImageSource.class); private static final File DO_NOT_CACHE_TO_DISK = new File(""); // Note: we use a URI, *not* a URL as the cache key. URL's can be very slow to compare: they use host name resolution // to produce the IP address and compare by that. final Object original; // any plausably covertable image source (e.g. a Resource, URL, File, stream) final URI key; // Unique key for caching final Resource resource;// if original was a resource, it goes here. Object readable; // the readable image source (not a Resource, and URL's converted to stream before ImageIO) private File _cacheFile; // If later stored in a file cache, is marked here. final int iconSize; // boolean nextLoadIsImmediate; // // interface Scaler { // // int size(); // // java.awt.Image getScaled(); // // } // void setImmediateRequest(boolean b) { // nextLoadIsImmediate = b; // } public static ImageSource create(Object o) { if (o instanceof ImageSource) { return (ImageSource) o; } else if (o instanceof URI) { return new ImageSource((URI)o, 0); } else { return new ImageSource(o); } } public static ImageSource createIconSource(ImageSource is, ImageRep fullRep, java.awt.Image hardFullImage, int size) { return new ImageSource(is, fullRep, hardFullImage, size); } /** @return a key that could be used for an icon version of this image */ URI getIconKey(int size) { if (key == null) { // todo: this will happen for a local files -- only if they're missing or always? if (DEBUG.IMAGE||DEBUG.WORK) Log.warn("can't create icon key w/null key: " + this, new Throwable("HERE")); else Log.warn("can't create icon key w/null key: " + this); return null; } if (isImageSourceForIcon()) throw new Error("can't make icon key from another icon image source"); return makeIconKey(this.key, size); } /** create an icon entry */ private ImageSource(ImageSource is, ImageRep softImageSource, java.awt.Image hardImageSource, int iconSize) { if (iconSize <= 0) throw new IllegalArgumentException("bad icon size " + iconSize); this.original = is.original; this.iconSize = iconSize; if (Images.DELAYED_ICONS) { // Even if we end up getting DELAYED_ICONS to work under low-mem conditions, // we may still want to keep the hard image source, tho that somewhat defeats // the purpose of the fancy low-memory recovery code we'd need anyway to handle // DELAYED_ICONS. E.g., hard references in a bunch of IconTasks at the back // of the queue when we run out of memory makes recovery even harder as we'd // really want to flush those IconTasks to allow GC and free up space before // proceeding single-threaded through outstanding image loads & icon tasks. this.readable = softImageSource; // The problem attempting the DELAYED_ICONS impl: ImageRep will go bad when // we clear this, tho that's easy to fix. The big problem is an icon // ImageRep would need to be able to trigger the reconstitute of the // lost full rep, and then trigger the icon generation again when it // comes back in. } else { this.readable = hardImageSource; // drawback to this impl: lots of memory contention when we run low (longer // to recover), tho mainly if we were attempting the DELAYED_ICONS impl. // advantage: we know sooner if we're running low on memory, and Images EOM // recovery doesn't need to be as sophisticated. // Note: this.readable must be cleared later to ensure disposability. It is // provided and held as a reference here exactly so that it is NOT // disposable until we're done with it (e.g., created an icon from it). } this.key = makeIconKey(is.key, iconSize); this.resource = null; this._cacheFile = new File(Images.keyToCacheFileName(this.key)); } // todo: would be better to use the actual CacheEntry.file to create // this, but we may not have that if there's a Loader in the Cache. private ImageSource(URI cacheKey, int iconSize) { if (cacheKey == null) throw new NullPointerException("cacheKey"); this.original = cacheKey; this.key = cacheKey; this.resource = null; this._cacheFile = new File(Images.keyToCacheFileName(this.key)); this.iconSize = iconSize; // okay if <=0: means not an icon // readable unset // todo: consistency check: key from file == incoming key } private ImageSource(Object original) { this.iconSize = -1; this.original = original; //Log.debug("NEW IMAGE SOURCE FROM " + Util.tags(original)); if (original instanceof Resource) { resource = (Resource) original; readable = resource.getImageSource(); if (DEBUG.RESOURCE) Log.debug("NEW IMAGE SOURCE FROM " + Util.tags(original) + "; readable=" + Util.tags(readable)); } else { resource = null; if (original instanceof File) { readable = (File) original; //setCacheFile((File) original); // note side effect } else if (original instanceof java.net.URL) { readable = (java.net.URL) original; if (readable.toString().startsWith(URLResource.THUMBSHOT_FETCH)) { _cacheFile = DO_NOT_CACHE_TO_DISK; //isThumbshot = true; } } // else if (original instanceof java.net.URI) { // cache key // this.key = (URI) original; // this.readable = keyToCacheFileName(this.key); // } // else if (original instanceof Image) { // Util.printStackTrace("SEEING IMAGE: HANDLE PRIOR " + original); // } else { throw new Error("can't use as an ImageSource: " + Util.tags(original)); } } if (readable instanceof java.net.URL) { final URL url = (URL) readable; this.key = makeKey(url); final File file = Resource.getLocalFileIfPresent(url); if (file != null) { //Log.debug("CONVERTED URL TO LOCAL FILE: " + file); this.readable = file; } else { //Log.debug("FAILED TO CONVERT URL TO LOCAL FILE: " + url); } } else if (readable instanceof java.io.File) { this.key = Images.makeKey((File) readable); } else { this.key = null; // will not be cacheable if (DEBUG.IMAGE) Log.debug("CREATED NULL-KEY NON-CACHEABLE IMAGE SOURCE:" + "\n\treadable="+Util.tags(readable) + "\n\toriginal="+Util.tags(original) , new Throwable(toString())); } // if (DEBUG.DR) { // if (resource != null) // resource.setDebugProperty("readable", Util.tags(readable)); // } } private static URI keyFromReadable(Object readable) { if (readable instanceof java.net.URL) { return makeKey((URL) readable); } else if (readable instanceof java.io.File) { return Images.makeKey((File) readable); } else { if (DEBUG.IMAGE) Log.debug("readable " + Util.tags(readable) + " has no key; will not enter cache"); return null; } } boolean mayBlockIndefinitely() { if (readable instanceof java.net.URL) { //Log.debug("MAY BLOCK INDEFINITELY: READABLE IS " + Util.tags(readable)); return true; } else return false; } /** @return true if this source is a raw image that can be used to create an icon at runtime. * This is contrasted with a regular ImageSource that may point to a icon already cached on disk. */ boolean isImageSourceForIcon() { return iconSize > 0; // tho resonable sizes would normally start at a min of 16 or 32 } void setCacheFile(File file) { if (_cacheFile == DO_NOT_CACHE_TO_DISK) throw new RuntimeException("attempt to cache non-cacheable: " + this); _cacheFile = file; if (DEBUG.IMAGE) { if (resource != null) resource.setDebugProperty("image.cache", file); } // if (resource != null) // resource.setCacheFile(file); } File getCacheFile() { return _cacheFile; } boolean hasCacheFile() { return _cacheFile != null && _cacheFile != DO_NOT_CACHE_TO_DISK; } boolean isDiskCacheEntry() { // todo: is this still used? return original == key; // TODO: check to see if semantics have changed } boolean useCacheFile() { if (isDiskCacheEntry() || _cacheFile == DO_NOT_CACHE_TO_DISK) return false; else return true; } String debugName() { if (key == null) return "[null ImageSource.key]"; try { // todo: put this in the image source? String basename = key.getPath(); final int lastSlash = basename.lastIndexOf('/'); if (lastSlash < (basename.length()-2)) basename = basename.substring(lastSlash+1, basename.length()); return basename; } catch (Throwable t) { return "[" + t.toString() + "]"; } } @Override public String toString() { final StringBuilder s = new StringBuilder("IS["); //s.append("<<<"); if (original instanceof Resource) s.append(original); else if (original instanceof URI) { s.append("URI@"); s.append(Util.tags(original.toString())); } else s.append(Util.tags(original)); //s.append(">>>"); if (readable != original) { s.append("; R="); if (readable instanceof File) { if (original instanceof Resource && ((Resource)original).getImageSource() == readable) { s.append("FF"); } else if (_cacheFile == readable) { s.append("FC"); } else { s.append("Fx"); s.append(readable.toString()); } } else s.append(Util.tags(readable)); } if (iconSize > 0) { s.append("; ICON"); s.append(iconSize); } if (_cacheFile != null) { s.append("; CF="); s.append(_cacheFile); } s.append(']'); return s.toString(); } static URI makeIconKey(URI cacheKey, int size) { if (cacheKey == null) throw new IllegalArgumentException("makeIconKey: null source key"); if (size <= 0) throw new IllegalArgumentException("makeIconKey: bad size " + size); try { return new URI(String.format("%s.i%d.png", cacheKey, size)); } catch (Throwable t) { Util.printStackTrace(t, "can't make URI icon cache key from key " + cacheKey); } return null; } private static URI makeKey(URL u) { try { if ("file".equals(u.getProtocol())) { return Resource.makeURI(u); } else { return new URI(u.getProtocol(), u.getUserInfo(), u.getHost(), u.getPort(), //u.getAuthority(), u.getPath(), u.getQuery(), u.getRef()).normalize(); } } catch (Throwable t) { Util.printStackTrace(t, "can't make URI cache key from URL " + u); } return null; } } // /** @return a source that could be used for an icon version of this image */ // ImageSource getIconSource() { // final URI iconKey = getIconKey(); // // final Object cr = Cache.get(iconKey); // // if (cr instanceof CacheEntry) { // // final CacheEntry ce = (CacheEntry) cr; // // iconImage = ce.getCachedImage(); // // if (iconImage == null) { // // // TODO: below is re-loading cache, as opposed to just filling cache from the existing entry // // // -- move load code to CacheEntry? Subseqent requests for this image may not be able to re-constitute // // iconImage = loadImage(ImageSource.create(ce.getFile()), listener); // todo: handle eom? // // // todo: stuff back into cache, or did loadImage do that? [yes, see above, handled badly] // // } // // } // } // private ImageSource(Object o, Resource r, Object readable) { // this.original = o; // this.resource = r; // this.readable = readable; // this.key = keyFromReadable(readable); // } // ImageSource(Resource r) { // this(r, r, r.getImageSource()); // } // ImageSource(File f) { // this(f, null, f); // } // ImageSource(URL u) { // this(u, null, u); // isThumbshot = u.toString().startsWith(URLResource.THUMBSHOT_FETCH); // } // /** @return a special source that refers to something in the local disk cache */ // public static ImageSource create(CacheEntry ce) { // return new ImageSource(ce); // }