// BlogBridge -- RSS feed reader, manager, and web based service // Copyright (C) 2002-2007 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: SavingImageSource.java,v 1.7 2007/11/23 15:24:33 spyromus Exp $ // package com.salas.bb.utils.uif.images; import com.salas.bb.networking.manager.NetManager; import com.salas.bb.networking.manager.NetTask; import com.salas.bb.utils.net.URLInputStream; import sun.awt.image.ImageDecoder; import sun.awt.image.URLImageSource; import java.awt.image.ColorModel; import java.awt.image.ImageConsumer; import java.io.*; import java.net.URL; import java.util.Hashtable; import java.util.concurrent.*; import java.util.logging.Level; import java.util.logging.Logger; /** * Image source that saves image data loaded from remote location. */ public class SavingImageSource extends URLImageSource implements Runnable { private static final Logger LOG = Logger.getLogger(SavingImageSource.class.getName()); private static LinkedBlockingQueue<Runnable> queue; private static Executor executor; // Running remote producers private static LinkedBlockingQueue<Runnable> queueLocals; private static Executor executorLocals; // Running local producers private File fileSource; // Source file (input is created from it) private URL urlSource; // Source URL (input is created from it) private InputStream input; // Input stream private OutputStream output; // File stream private volatile boolean producing; // TRUE when producer started private volatile boolean produced; // TRUE when initial production is finished private volatile boolean abort; // TRUE when processing should be terminated private File temp; // Temporary image data private File dest; // Destination image file private NetTask netTask; // Associated net task // Initialize the executor static { queue = new LinkedBlockingQueue<Runnable>(); executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, queue, new ThreadFactory() { public Thread newThread(Runnable r) { Thread th = new Thread(r, "Image Loader NG"); th.setDaemon(true); th.setPriority(Thread.MIN_PRIORITY); return th; } }); queueLocals = new LinkedBlockingQueue<Runnable>(); executorLocals = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, queueLocals, new ThreadFactory() { public Thread newThread(Runnable r) { Thread th = new Thread(r, "Image Loader NG (Locals)"); th.setDaemon(true); th.setPriority(Thread.MIN_PRIORITY); return th; } }); } /** * Creates an image by loading data directly from the given file. Caching is disabled. * * @param source source file. * * @throws IOException in case of I/O problem. */ public SavingImageSource(File source) throws IOException { super("http://localhost/"); this.fileSource = source; } /** * Creates an image from the remote source and saves it first into the temp, and * then (upon completion) into the destination file. * * @param source source URL. * @param temp temp file. * @param dest destination file. * * @throws IOException in case of I/O problem. */ public SavingImageSource(URL source, File temp, File dest) throws IOException { super("http://localhost/"); this.urlSource = source; this.dest = dest; this.temp = temp; } /** * Removes tasks from queues. */ public static void clearQueues() { synchronized (SavingImageSource.class) { for (Runnable runnable : queue) ((SavingImageSource)runnable).abort(); for (Runnable runnable : queueLocals) ((SavingImageSource)runnable).abort(); } } /** * Aborts the operation of this image source before it's started. */ private void abort() { abort = true; } @Override public synchronized void startProduction(ImageConsumer imageConsumer) { addConsumer(imageConsumer); if (producing) return; // Start producing producing = true; try { // If the image was produced once, we need to disable caching and // reused cached image if possible. if (produced) { // If originally downloaded from URL to a cache file, use the cache now if (fileSource == null && dest != null && dest.exists()) fileSource = dest; // Remove links to the cache and temp so that they aren't overwritten dest = null; temp = null; } // Open input stream input = openInputStream(fileSource, urlSource); // Create an output stream File fl = temp == null ? dest : temp; output = fl == null ? null : new BufferedOutputStream(new FileOutputStream(fl)); // Add an observer addConsumer(new Consumer()); // Choose the right executor Executor ex = fileSource == null ? executor : executorLocals; synchronized (SavingImageSource.class) { ex.execute(this); } } catch (FileNotFoundException e) { LOG.log(Level.WARNING, "Error starting production of an image (file=" + fileSource + ", url=" + urlSource + ")", e); } } /** * Returns a buffered input stream created from a file or an URL. * * @param file file source. * @param url URL source. * * @return buffered input stream. * * @throws FileNotFoundException if file is not found. */ private BufferedInputStream openInputStream(File file, URL url) throws FileNotFoundException { InputStream inp; if (file == null) { // Create from URL URLInputStream in = new URLInputStream(url); netTask = NetManager.register(NetManager.TYPE_ARTICLE_IMAGE, url.toString(), "", in); inp = in; } else { // Create from local file inp = new FileInputStream(file); } return new BufferedInputStream(inp); } @Override protected ImageDecoder getDecoder() { return getDecoder(new SavingInputStream(input, output)); } /** * Executed to produce an image. */ public void run() { try { if (abort) { // Abort net task if (netTask != null) netTask.abort(); // Close streams if (input != null) input.close(); if (output != null) output.close(); // Remove temp if (temp != null) temp.delete(); producing = false; } else { doFetch(); } } catch (Throwable e) { // Ignore } finally { netTask = null; } } /** * Invoked when an image is completely loaded and ready for saving. */ private void onImageComplete() { try { try { if (output != null) { output.flush(); output.close(); } if (input != null) input.close(); // Move data from temp to destination if (temp != null) { if (abort) temp.delete(); else temp.renameTo(dest); } } finally { // Image is produced producing = false; produced = !abort; // Unregister if (dest != null) ImageFetcher.unregisterDownloadedImage(dest.getName()); } } catch (Throwable e) { LOG.log(Level.WARNING, "Error closing output", e); } } /** * A stream that is reading data from an input stream and writing the same data * to the given output stream while returning it to the reader. */ private class SavingInputStream extends FilterInputStream { private final OutputStream out; /** * Creates a stream. * * @param in stream to read from. * @param out stream to write to. */ private SavingInputStream(InputStream in, OutputStream out) { super(in); this.out = out; } @Override public int read() throws IOException { checkAbort(); int b = super.read(); if (out != null && b != -1) out.write(b); return b; } /** * Closes an input stream if aborting is requested. * * @throws IOException I/O error. */ private void checkAbort() throws IOException { if (abort) in.close(); } @Override public int read(byte b[]) throws IOException { checkAbort(); int read = super.read(b); if (out != null && read != -1) out.write(b, 0, read); return read; } @Override public int read(byte b[], int off, int len) throws IOException { checkAbort(); int read = super.read(b, off, len); if (out != null && read != -1) out.write(b, off, read); return read; } @Override public boolean markSupported() { // We don't support marking so far as it would require skipping bytes when reading // them again from the underlying stream. return false; } } /** * Image consumer whose goal is to look for image completion and * invoke the <code>onImageComplete</code> method. */ private class Consumer implements ImageConsumer { /** * 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 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) { } /** * Sets the ColorModel object used for the majority of the pixels reported using the setPixels method calls. Note * that each set of pixels delivered using setPixels contains its own ColorModel object, so no assumption should be * made that this model will be the only one used in delivering pixel values. A notable case where multiple * ColorModel objects may be seen is a filtered image when for each set of pixels that it filters, the filter * determines whether the pixels can be sent on untouched, using the original ColorModel, or whether the pixels * should be modified (filtered) and passed on using a ColorModel more convenient for the filtering process. * * @param model the specified <code>ColorModel</code> * * @see java.awt.image.ColorModel */ public void setColorModel(ColorModel model) { } /** * Sets the hints that the ImageConsumer uses to process the pixels delivered by the ImageProducer. The * ImageProducer can deliver the pixels in any order, but the ImageConsumer may be able to scale or convert the * pixels to the destination ColorModel more efficiently or with higher quality if it knows some information about * how the pixels will be delivered up front. The setHints method should be called before any calls to any of the * setPixels methods with a bit mask of hints about the manner in which the pixels will be delivered. If the * ImageProducer does not follow the guidelines for the indicated hint, the results are undefined. * * @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. Each call specifies the location and * size of the rectangle of source pixels that are contained in the array of pixels. The specified ColorModel * object should be used to convert the pixels into their corresponding color and alpha components. Pixel (m,n) is * stored in the pixels array at index (n * scansize + m + off). The pixels delivered using this method are all * stored as bytes. * * @param x, y the coordinates 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 * * @see java.awt.image.ColorModel */ 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. Each call specifies the * location and size of the rectangle of source pixels that are contained in the array of pixels. The specified * ColorModel object should be used to convert the pixels into their corresponding color and alpha components. * Pixel (m,n) is stored in the pixels array at index (n * scansize + m + off). The pixels delivered using this * method are all stored as ints. this method are all stored as ints. * * @param x, y the coordinates 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 * * @see java.awt.image.ColorModel */ public void setPixels(int x, int y, int w, int h, ColorModel model, int pixels[], int off, int scansize) { } /** * 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 * * @see java.awt.image.ImageProducer#removeConsumer */ public void imageComplete(int status) { if (status == STATICIMAGEDONE || status == SINGLEFRAMEDONE) { removeConsumer(Consumer.this); if (status == STATICIMAGEDONE) onImageComplete(); } } } }