tcurdt 2003/01/05 22:10:25 Modified: . changes.xml src/documentation/xdocs/userdocs/readers image-reader.xml resource-reader.xml src/java/org/apache/cocoon/reading ResourceReader.java src/scratchpad/src/org/apache/cocoon/reading ImageReader.java Added: src/java/org/apache/cocoon/util ByteRange.java Removed: src/scratchpad/src/org/apache/cocoon/reading ByteRangeResourceReader.java src/scratchpad/src/org/apache/cocoon/util ByteRange.java Log: moved the byte range support from the ByteRangeReader in scratchpad into the ResourceReader, ImageReader now extends the ResourceReader, because of that parameter "expire-time" becomes "expires", no longer set byte range header for on-the-fly images (reader did not support it anyway), Revision Changes Path 1.328 +15 -1 xml-cocoon2/changes.xml Index: changes.xml =================================================================== RCS file: /home/cvs/xml-cocoon2/changes.xml,v retrieving revision 1.327 retrieving revision 1.328 diff -u -r1.327 -r1.328 --- changes.xml 31 Dec 2002 16:26:05 -0000 1.327 +++ changes.xml 6 Jan 2003 06:10:24 -0000 1.328 @@ -40,6 +40,20 @@ </devs> <release version="@version@" date="@date@"> + <action dev="TC" type="update"> + ImageReader extends now ResourceReader and therefor the + "expire-time" parameter is now "expires". Also removed the + setting of the byte range header for on-the-fly images. + (did not support it anyway) + </action> + <action dev="TC" type="update"> + Moved the byte range support from the ByteRangeReader in scratchpad + into the ResourceReader. Added the optional parameters "buffer-size" + and "byte-range". Byte range support is enable by default. + </action> + <action dev="TC" type="update"> + Move the image inspection into a ImageUtils class + </action> <action dev="TC" type="update"> Major cleanup of the ImageDirectoryGenerator, removed the RuntimeExceptions, added support for the JPEG comment marker 1.2 +8 -6 xml-cocoon2/src/documentation/xdocs/userdocs/readers/image-reader.xml Index: image-reader.xml =================================================================== RCS file: /home/cvs/xml-cocoon2/src/documentation/xdocs/userdocs/readers/image-reader.xml,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- image-reader.xml 25 Dec 2002 12:56:07 -0000 1.1 +++ image-reader.xml 6 Jan 2003 06:10:24 -0000 1.2 @@ -26,7 +26,7 @@ <td>TYPE</td><td>Reader, Sitemap Component</td> </tr> <tr> - <td>BLOCK</td><td>Scratchpad</td> + <td>BLOCK</td><td>Core</td> </tr> <tr> <td>CLASS</td><td>org.apache.cocoon.reading.ImageReader</td> @@ -80,7 +80,7 @@ <map:readers default="resource"> ... <map:reader name="image" - src="org.apache.cocoon.reading.ImageReader" + src="org.apache.cocoon.reading.ImageReader" logger="sitemap.reader.image" pool-max="32" pool-min="1" pool-grow="4"/> <!-- optional reader configuration --> @@ -101,7 +101,7 @@ </p> <table> <tr><th>Parametername</th><th>Type</th><th>Comment</th></tr> - <tr><td>expire-time</td><td>Time in milliseconds</td> + <tr><td>expires</td><td>Time in milliseconds</td> <td> This parameter is optional. When specified it determines how long in miliseconds the resources can be cached by any proxy or browser @@ -134,7 +134,7 @@ <source><![CDATA[ <map:match pattern="*.jpg"> <map:reader type="image" - <map:parameter name="expire-time" value="86400000"/> + <map:parameter name="expires" value="86400000"/> <map:parameter name="width" value="300"/> </map:reader> ... @@ -154,8 +154,8 @@ Nevertheless it can serve any image data in a non transforming mode. </p> <p> - The <code>ImageReader</code> does support HTTP ranges, thus - it sets <code>Accept-Ranges</code> to <code>bytes</code>. + The <code>ImageReader</code> does NOT support HTTP ranges, thus + it sets <code>Accept-Ranges</code> to <code>none</code>. </p> <p> The java Bug Id 4502892 (which is found in *all* JVM implementations from @@ -167,6 +167,8 @@ <s1 title="History"> <p> 12-25-02: Initial document creation by Bernhard Huber + 01-06-03: Renamed the expire-time -> expires parameter, + Fixed the statement about the byte range support, Torsten Curdt </p> </s1> <s1 title="Copyright"> 1.2 +13 -4 xml-cocoon2/src/documentation/xdocs/userdocs/readers/resource-reader.xml Index: resource-reader.xml =================================================================== RCS file: /home/cvs/xml-cocoon2/src/documentation/xdocs/userdocs/readers/resource-reader.xml,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- resource-reader.xml 25 Dec 2002 12:56:07 -0000 1.1 +++ resource-reader.xml 6 Jan 2003 06:10:24 -0000 1.2 @@ -116,6 +116,18 @@ same source is used as last time. </td> </tr> + <tr><td>byte-ranges</td><td>boolean</td> + <td> + This parameter is optional. This boolean parameter enables or disables + support for the byte ranges. + </td> + </tr> + <tr><td>buffer-size</td><td>integer</td> + <td> + This parameter is optional. It specifies the buffer/block size when + reading from a resource. + </td> + </tr> </table> <p> The following <code>ResourceReader</code> declaration snippet @@ -142,14 +154,11 @@ </s2> </s1> <s1 title="Bugs/Caveats"> - <p> - The <code>ResourceReader</code> does not support HTTP ranges, thus - it sets <code>Accept-Ranges</code> to <code>none</code>. - </p> </s1> <s1 title="History"> <p> 12-25-02: Initial document creation by Bernhard Huber + 01-06-03: Added new parameters and byte range support, Torsten Curdt </p> </s1> <s1 title="Copyright"> 1.22 +146 -66 xml-cocoon2/src/java/org/apache/cocoon/reading/ResourceReader.java Index: ResourceReader.java =================================================================== RCS file: /home/cvs/xml-cocoon2/src/java/org/apache/cocoon/reading/ResourceReader.java,v retrieving revision 1.21 retrieving revision 1.22 diff -u -r1.21 -r1.22 --- ResourceReader.java 18 Dec 2002 08:09:24 -0000 1.21 +++ ResourceReader.java 6 Jan 2003 06:10:25 -0000 1.22 @@ -52,6 +52,7 @@ import org.apache.avalon.framework.parameters.Parameters; import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.util.ByteRange; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.components.source.SourceUtil; import org.apache.cocoon.environment.Context; @@ -59,6 +60,7 @@ import org.apache.cocoon.environment.Request; import org.apache.cocoon.environment.Response; import org.apache.cocoon.environment.SourceResolver; +import org.apache.cocoon.environment.http.HttpResponse; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.SourceValidity; @@ -88,38 +90,51 @@ * <dd>This parameter is optional. This boolean parameter controlls the * last modified test. If set to true (default is false), only the * last modified of the current source is tested, but not if the - * same source is used as last time. + * same source is used as last time. (see http://marc.theaimsgroup.com/?l=xml-cocoon-dev&m=102921894301915&w=2 ) * </dd> * </dl> * * @author <a href="mailto:[EMAIL PROTECTED]">Giacomo Pati</a> - * @version CVS $Id$ + * @author <a href="mailto:[EMAIL PROTECTED]">Torsten Curdt</a> + * @version CVS $Id$ */ -public final class ResourceReader - extends AbstractReader - implements CacheableProcessingComponent { - - /** The source */ - private Source inputSource; +public class ResourceReader extends AbstractReader implements CacheableProcessingComponent { /** The list of generated documents */ private static final Map documents = new HashMap(); - - /** quick test */ - private boolean quickTest; - + + protected Source inputSource; + protected InputStream inputStream; + + protected boolean quickTest; + protected boolean byteRanges; + + protected Response response; + protected Request request; + protected long expires; + protected int bufferSize; + /** * Setup the reader. * The resource is opened to get an <code>InputStream</code>, * the length and the last modification date */ - public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) - throws ProcessingException, SAXException, IOException { + public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException { super.setup(resolver, objectModel, src, par); - this.quickTest = par.getParameterAsBoolean("quick-modified-test", false); + + request = ObjectModelHelper.getRequest(objectModel); + response = ObjectModelHelper.getResponse(objectModel); + + expires = par.getParameterAsInteger("expires", -1); + bufferSize = par.getParameterAsInteger("buffer-size",8192); + + byteRanges = par.getParameterAsBoolean("byte-ranges",true); + quickTest = par.getParameterAsBoolean("quick-modified-test", false); + try { - this.inputSource = resolver.resolveURI(src); - } catch (SourceException se) { + inputSource = resolver.resolveURI(src); + } + catch (SourceException se) { throw SourceUtil.handle("Error during resolving of '" + src + "'.", se); } } @@ -128,9 +143,9 @@ * Recyclable */ public void recycle() { - if (this.inputSource != null) { - super.resolver.release( this.inputSource ); - this.inputSource = null; + if (inputSource != null) { + super.resolver.release(inputSource); + inputSource = null; } super.recycle(); } @@ -142,7 +157,7 @@ * @return The generated key hashes the src */ public java.io.Serializable generateKey() { - return this.inputSource.getSystemId(); + return inputSource.getSystemId(); } /** @@ -152,7 +167,7 @@ * component is currently not cacheable. */ public SourceValidity generateValidity() { - return this.inputSource.getValidity(); + return inputSource.getValidity(); } /** @@ -160,78 +175,143 @@ * possible to detect */ public long getLastModified() { - if (this.quickTest) { - return this.inputSource.getLastModified(); - } - final Request request = ObjectModelHelper.getRequest(this.objectModel); - final String systemId = (String)documents.get(request.getRequestURI()); - if (this.inputSource.getSystemId().equals(systemId)) { - return this.inputSource.getLastModified(); - } else { - documents.remove(request.getRequestURI()); - return 0; + if (quickTest) { + return inputSource.getLastModified(); + } + final String systemId = (String) documents.get(request.getRequestURI()); + if (inputSource.getSystemId().equals(systemId)) { + return inputSource.getLastModified(); + } + else { + documents.remove(request.getRequestURI()); + return 0; } } - /** - * Generates the requested resource. - */ - public void generate() - throws IOException, ProcessingException { - final Response response = ObjectModelHelper.getResponse(this.objectModel); + protected void processStream() throws IOException, ProcessingException { + byte[] buffer = new byte[bufferSize]; + int length = -1; + + String ranges = request.getHeader("Ranges"); + + ByteRange byteRange; + if (ranges != null && byteRanges) { + try { + ranges = ranges.substring(ranges.indexOf('=') + 1); + byteRange = new ByteRange(ranges); + } catch (NumberFormatException e) { + byteRange = null; + + // TC: Hm.. why don't we have setStatus in the Response interface ? + if (response instanceof HttpResponse) { + // Respond with status 416 (Request range not satisfiable) + ((HttpResponse)response).setStatus(416); + if (getLogger().isDebugEnabled()) { + getLogger().debug("malformed byte range header [" + String.valueOf(ranges) + "]"); + } + } + } + } + else { + byteRange = null; + } - try { - final long expires = parameters.getParameterAsInteger("expires", -1); + long contentLength = inputSource.getContentLength(); - if (expires > 0) { - response.setDateHeader("Expires", System.currentTimeMillis() + expires); + if (byteRange != null) { + String entityLength; + String entityRange; + if (contentLength != -1) { + entityLength = "" + contentLength; + entityRange = byteRange.intersection(new ByteRange(0, contentLength)).toString(); + } else { + entityLength = "*"; + entityRange = byteRange.toString(); + } + + response.setHeader("Content-Range", entityRange + "/" + entityLength); + + if (response instanceof HttpResponse) { + // Response with status 206 (Partial content) + ((HttpResponse)response).setStatus(206); } - long contentLength = this.inputSource.getContentLength(); + response.setHeader("Accept-Ranges", "bytes"); + + int pos = 0; + int posEnd; + while ((length = inputStream.read(buffer)) > -1) { + posEnd = pos + length - 1; + ByteRange intersection = byteRange.intersection(new ByteRange(pos, posEnd)); + if (intersection != null) { + out.write(buffer, (int) intersection.getStart() - pos, (int) intersection.length()); + } + pos += length; + } + } + else { if (contentLength != -1) { - // FIXME (VG): Environment has setContentLength, and - // Response interface has not. Strange. response.setHeader("Content-Length", Long.toString(contentLength)); } // Bug #9539: This resource reader does not support ranges response.setHeader("Accept-Ranges", "none"); - byte[] buffer = new byte[8192]; - int length = -1; - - InputStream inputStream = this.inputSource.getInputStream(); while ((length = inputStream.read(buffer)) > -1) { out.write(buffer, 0, length); } + } + + out.flush(); + } + + /** + * Generates the requested resource. + */ + public void generate() throws IOException, ProcessingException { + try { + if (expires > 0) { + response.setDateHeader("Expires", System.currentTimeMillis() + expires); + } + + try { + inputStream = inputSource.getInputStream(); + } + catch (SourceException se) { + throw SourceUtil.handle("Error during resolving of the input stream", se); + } + + processStream(); + inputStream.close(); - inputStream = null; - out.flush(); - - if (!this.quickTest) { - // if everything is ok, add this to the list of generated documents - final Request request = ObjectModelHelper.getRequest(this.objectModel); - documents.put(request.getRequestURI(), this.inputSource.getSystemId()); + + if (!quickTest) { + // if everything is ok, add this to the list of generated documents + // (see http://marc.theaimsgroup.com/?l=xml-cocoon-dev&m=102921894301915&w=2 ) + documents.put(request.getRequestURI(), inputSource.getSystemId()); } - } catch (SourceException se) { - throw SourceUtil.handle("Exception during resolving of read source.", se); + } + catch (IOException e) { + getLogger().debug("Received an IOException, assuming client severed connection on purpose"); } } /** * Returns the mime-type of the resource in process. */ - public String getMimeType () { - Context ctx = ObjectModelHelper.getContext(this.objectModel); + public String getMimeType() { + Context ctx = ObjectModelHelper.getContext(objectModel); if (ctx != null) { - if (ctx.getMimeType(this.source)!=null) { - return ctx.getMimeType(this.source); - } else { - return this.inputSource.getMimeType(); + if (ctx.getMimeType(source) != null) { + return ctx.getMimeType(source); + } + else { + return inputSource.getMimeType(); } - } else { - return this.inputSource.getMimeType(); + } + else { + return inputSource.getMimeType(); } } 1.1 xml-cocoon2/src/java/org/apache/cocoon/util/ByteRange.java Index: ByteRange.java =================================================================== /* ============================================================================ The Apache Software License, Version 1.1 ============================================================================ Copyright (C) 1999-2002 The Apache Software Foundation. All rights reserved. Redistribution and use in source and binary forms, with or without modifica- tion, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The end-user documentation included with the redistribution, if any, must include the following acknowledgment: "This product includes software developed by the Apache Software Foundation (http://www.apache.org/)." Alternately, this acknowledgment may appear in the software itself, if and wherever such third-party acknowledgments normally appear. 4. The names "Apache Cocoon" and "Apache Software Foundation" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact [EMAIL PROTECTED] 5. Products derived from this software may not be called "Apache", nor may "Apache" appear in their name, without prior written permission of the Apache Software Foundation. THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This software consists of voluntary contributions made by many individuals on behalf of the Apache Software Foundation and was originally created by Stefano Mazzocchi <[EMAIL PROTECTED]>. For more information on the Apache Software Foundation, please see <http://www.apache.org/>. */ package org.apache.cocoon.util; /** * @author <a href="mailto:[EMAIL PROTECTED]">Stuart Roebuck</a> * @version CVS $Id: ByteRange.java,v 1.1 2003/01/06 06:10:25 tcurdt Exp $ */ final public class ByteRange { private final long start; private final long end; public ByteRange(long start, long end) { this.start = start; this.end = end; } public ByteRange(String string) throws NumberFormatException { string = string.trim(); int dashPos = string.indexOf('-'); int length = string.length(); if (string.indexOf(',') != -1) { throw new NumberFormatException("Simple ByteRange String contains a comma."); } if (dashPos > 0) { this.start = Integer.parseInt(string.substring(0, dashPos)); } else { this.start = Long.MIN_VALUE; } if (dashPos < length - 1) { this.end = Integer.parseInt(string.substring(dashPos + 1, length)); } else { this.end = Long.MAX_VALUE; } if (this.start > this.end) { throw new NumberFormatException("Start value is greater than end value."); } } public long getStart() { return this.start; } public long getEnd() { return this.end; } public long length() { return this.end - this.start + 1; } public ByteRange intersection(ByteRange range) { if (range.end < this.start || this.end < range.start) { return null; } else { long start = (this.start > range.start) ? this.start : range.start; long end = (this.end < range.end) ? this.end : range.end; return new ByteRange(start, end); } } public String toString() { return this.start + "-" + this.end; } } 1.6 +67 -156 xml-cocoon2/src/scratchpad/src/org/apache/cocoon/reading/ImageReader.java Index: ImageReader.java =================================================================== RCS file: /home/cvs/xml-cocoon2/src/scratchpad/src/org/apache/cocoon/reading/ImageReader.java,v retrieving revision 1.5 retrieving revision 1.6 diff -u -r1.5 -r1.6 --- ImageReader.java 2 Aug 2002 09:21:00 -0000 1.5 +++ ImageReader.java 6 Jan 2003 06:10:25 -0000 1.6 @@ -50,22 +50,10 @@ */ package org.apache.cocoon.reading; -import org.apache.avalon.framework.component.ComponentManager; -import org.apache.avalon.framework.component.Composable; import org.apache.avalon.framework.parameters.Parameters; import org.apache.cocoon.ProcessingException; -import org.apache.cocoon.caching.CacheableProcessingComponent; -import org.apache.cocoon.environment.Context; -import org.apache.cocoon.environment.ObjectModelHelper; -import org.apache.cocoon.environment.Request; -import org.apache.cocoon.environment.Response; import org.apache.cocoon.environment.SourceResolver; -import org.apache.cocoon.util.HashUtil; - -import org.apache.excalibur.source.Source; -import org.apache.excalibur.source.SourceException; -import org.apache.excalibur.source.SourceValidity; import com.sun.image.codec.jpeg.ImageFormatException; import com.sun.image.codec.jpeg.JPEGCodec; @@ -80,10 +68,6 @@ import java.awt.image.WritableRaster; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.Date; import java.util.Map; /** @@ -106,110 +90,23 @@ * * @author <a href="mailto:[EMAIL PROTECTED]">Stefano Mazzocchi</a> * @author <a href="mailto:[EMAIL PROTECTED]">Stephan Michels</a> + * @author <a href="mailto:[EMAIL PROTECTED]">Torsten Curdt</a> * @version CVS $Revision$ $Date$ */ -public class ImageReader extends AbstractReader implements Composable, CacheableProcessingComponent { - - private ComponentManager manager; +final public class ImageReader extends ResourceReader { - public void compose(ComponentManager manager) { - this.manager = manager; - } - - private Source inputSource; - private Response response; - private Request request; private int width; private int height; - private int expireTime; - private String format; - public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) - throws ProcessingException, SAXException, IOException { + public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException { super.setup(resolver, objectModel, src, par); - this.request = ObjectModelHelper.getRequest(objectModel); - this.response = ObjectModelHelper.getResponse(objectModel); - this.width = par.getParameterAsInteger("width", 0); - this.height = par.getParameterAsInteger("height", 0); - this.expireTime = par.getParameterAsInteger("expire-time", -1); - getLogger().debug("Image data: [" + this.width + "x" + this.height + "] Expire Time: " + expireTime); - - try { - this.inputSource = this.resolver.resolveURI(super.source); - } catch (SourceException se) { - throw new ProcessingException("Could not retrieve source '"+super.source+"'", se); - } - } - - /** - * Generate the unique key. - * This key must be unique inside the space of this component. - * This method must be invoked before the generateValidity() method. - * - * @return The generated key or <code>null</code> if the component - * is currently not cacheable. - */ - public Serializable generateKey() { - return this.inputSource.getSystemId() + ":" + width + height; - } - /** - * Generate the validity object. - * Before this method can be invoked the generateKey() method - * must be invoked. - * - * @return The generated validity object or <code>null</code> if the - * component is currently not cacheable. - */ - public SourceValidity generateValidity() { - return this.inputSource.getValidity(); - } + width = par.getParameterAsInteger("width", 0); + height = par.getParameterAsInteger("height", 0); - /** - * @return the time the read source was last modified or 0 if it is not - * possible to detect - */ - public long getLastModified() { - return this.inputSource.getLastModified(); } - /** - * Generates the requested resource. - */ - public void generate() throws IOException, ProcessingException { - if (expireTime > 0) { - response.setDateHeader("Expires", new Date().getTime() + expireTime); - } - - try { - InputStream in = this.inputSource.getInputStream(); - - if ((width == 0) && (height == 0)) { - read(in, out); - } else { - convert(in, width, height, out); - } - } catch (SourceException se) { - throw new ProcessingException("Could not read source '"+super.source+"'", se); - } - } - - private void read(InputStream in, OutputStream out) { - try { - response.setHeader("Accept-Ranges", "bytes"); - byte[] buffer = new byte[8192]; - int length = -1; - while ((length = in.read(buffer)) > -1) { - out.write(buffer, 0, length); - } - in.close(); - out.flush(); - } catch (IOException ioe) { - getLogger().debug("Received an IOException, assuming client severed connection on purpose"); - } - } - /** * Returns the affine transform that implements the scaling. * The behavior is the following: if both the new width and height values @@ -242,54 +139,68 @@ return new AffineTransform(wm, 0.0d, 0.0d, hm, 0.0d, 0.0d); } - /** - * NOTE (SM): - * Due to Bug Id 4502892 (which is found in *all* JVM implementations from - * 1.2.x and 1.3.x on all OS!), we must buffer the JPEG generation to avoid - * that connection resetting by the peer (user pressing the stop button, - * for example) crashes the entire JVM (yes, dude, the bug is *that* nasty - * since it happens in JPEG routines which are native!) - * I'm perfectly aware of the huge memory problems that this causes (almost - * doubling memory consuption for each image and making the GC work twice - * as hard) but it's *far* better than restarting the JVM every 2 minutes - * (since this is the average experience for image-intensive web application - * such as an image gallery). - * Please, go to the <a href="http://developer.java.sun.com/developer/bugParade/bugs/4502892.html">Sun Developers Connection</a> - * and vote this BUG as the one you would like fixed sooner rather than - * later and all this hack will automagically go away. - * Many deep thanks to Michael Hartle <[EMAIL PROTECTED]> for tracking - * this down and suggesting the workaround. - * - * UPDATE (SM): - * This appears to be fixed on JDK 1.4 - */ - private void convert(InputStream in, double w, double h, OutputStream out) throws IOException, ProcessingException { - try { - JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(in); - Raster original = decoder.decodeAsRaster(); - JPEGDecodeParam decodeParam = decoder.getJPEGDecodeParam(); - double ow = (double) decodeParam.getWidth(); - double oh = (double) decodeParam.getHeight(); - AffineTransformOp filter = new AffineTransformOp(getTransform(ow, oh, w, h), AffineTransformOp.TYPE_BILINEAR); - WritableRaster scaled = filter.createCompatibleDestRaster(original); - filter.filter(original, scaled); - // JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); - ByteArrayOutputStream bstream = new ByteArrayOutputStream(); - JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(bstream); - encoder.encode(scaled); - in.close(); - out.write(bstream.toByteArray()); - out.flush(); - } catch (ImageFormatException e) { - throw new ProcessingException("Error reading the image. Note that only JPEG images are currently supported."); + protected void processStream() throws IOException, ProcessingException { + if (width > 0 || height > 0) { + if (getLogger().isDebugEnabled()) { + getLogger().debug("image " + ((width==0)?"?":Integer.toString(width)) + "x" + ((height==0)?"?":Integer.toString(height)) +" expires: " + expires); + } + + // since we create the image on the fly + response.setHeader("Accept-Ranges", "none"); + + /** + * NOTE (SM): + * Due to Bug Id 4502892 (which is found in *all* JVM implementations from + * 1.2.x and 1.3.x on all OS!), we must buffer the JPEG generation to avoid + * that connection resetting by the peer (user pressing the stop button, + * for example) crashes the entire JVM (yes, dude, the bug is *that* nasty + * since it happens in JPEG routines which are native!) + * I'm perfectly aware of the huge memory problems that this causes (almost + * doubling memory consuption for each image and making the GC work twice + * as hard) but it's *far* better than restarting the JVM every 2 minutes + * (since this is the average experience for image-intensive web application + * such as an image gallery). + * Please, go to the <a href="http://developer.java.sun.com/developer/bugParade/bugs/4502892.html">Sun Developers Connection</a> + * and vote this BUG as the one you would like fixed sooner rather than + * later and all this hack will automagically go away. + * Many deep thanks to Michael Hartle <[EMAIL PROTECTED]> for tracking + * this down and suggesting the workaround. + * + * UPDATE (SM): + * This appears to be fixed on JDK 1.4 + */ + + try { + JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(inputStream); + Raster original = decoder.decodeAsRaster(); + JPEGDecodeParam decodeParam = decoder.getJPEGDecodeParam(); + double ow = (double) decodeParam.getWidth(); + double oh = (double) decodeParam.getHeight(); + AffineTransformOp filter = new AffineTransformOp(getTransform(ow, oh, width, height), AffineTransformOp.TYPE_BILINEAR); + WritableRaster scaled = filter.createCompatibleDestRaster(original); + filter.filter(original, scaled); + + // JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); + + ByteArrayOutputStream bstream = new ByteArrayOutputStream(); + JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(bstream); + encoder.encode(scaled); + out.write(bstream.toByteArray()); + + out.flush(); + } catch (ImageFormatException e) { + throw new ProcessingException("Error reading the image. Note that only JPEG images are currently supported."); + } + + inputStream.close(); + } + else { + // only read the resource - no modifications requested + if (getLogger().isDebugEnabled()) { + getLogger().debug("passing original resource"); + } + super.processStream(); } } - /** - * Returns the mime-type of the resource in process. - */ - public String getMimeType() { - Context ctx = ObjectModelHelper.getContext(objectModel); - return (ctx != null) ? ctx.getMimeType(this.source) : null; - } }
---------------------------------------------------------------------- In case of troubles, e-mail: [EMAIL PROTECTED] To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]