From a1f9e979b92f3a566e6a5b9a5686dec5f3f5fa91 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 23 Dec 2013 11:11:48 +0100 Subject: [PATCH] Servlet changes for 3.0. --- .../twelvemonkeys/servlet/GenericFilter.java | 35 +- .../twelvemonkeys/servlet/GenericServlet.java | 3 +- .../twelvemonkeys/servlet/HttpServlet.java | 3 +- .../ServletResponseStreamDelegate.java | 31 +- .../image/ImageServletResponseImpl.java | 34 +- .../ImageServletResponseImplTestCase.java | 330 ++++++++++++++---- .../com/twelvemonkeys/servlet/image/star.png | Bin 0 -> 5953 bytes 7 files changed, 322 insertions(+), 114 deletions(-) create mode 100644 servlet/src/test/resources/com/twelvemonkeys/servlet/image/star.png diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java index 2c279c85..6397e182 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java @@ -50,10 +50,10 @@ import java.util.Enumeration; *

* To write a generic filter, you need only override the abstract * {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. @@ -67,6 +67,7 @@ import java.util.Enumeration; * @see FilterConfig */ public abstract class GenericFilter implements Filter, FilterConfig, Serializable { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil /** * The filter config. @@ -76,32 +77,29 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl /** * Makes sure the filter runs once per request *

- * see #isRunOnce - * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_VALUE * @see #oncePerRequest - * see #ATTRIB_RUN_ONCE_VALUE */ private final static String ATTRIB_RUN_ONCE_EXT = ".REQUEST_HANDLED"; /** * Makes sure the filter runs once per request. * Must be configured through init method, as the filter name is not - * available before we have a FitlerConfig object. + * available before we have a {@code FilterConfig} object. *

- * see #isRunOnce - * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_VALUE * @see #oncePerRequest - * see #ATTRIB_RUN_ONCE_VALUE */ private String attribRunOnce = null; /** * Makes sure the filter runs once per request *

- * see #isRunOnce - * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_EXT * @see #oncePerRequest - * see #ATTRIB_RUN_ONCE_EXT */ private static final Object ATTRIB_RUN_ONCE_VALUE = new Object(); @@ -213,16 +211,16 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * and returns false. * A return value of false, indicates that the filter has not yet run. * A return value of true, indicates that the filter has run for this - * request, and processing should not contine. + * request, and processing should not continue. *

* Note that the method will mark the request as filtered on first * invocation. *

- * see #ATTRIB_RUN_ONCE_EXT - * see #ATTRIB_RUN_ONCE_VALUE + * @see #ATTRIB_RUN_ONCE_EXT + * @see #ATTRIB_RUN_ONCE_VALUE * * @param pRequest the servlet request - * @return {@code true} if the request is allready filtered, otherwise + * @return {@code true} if the request is already filtered, otherwise * {@code false}. */ private boolean isRunOnce(final ServletRequest pRequest) { @@ -233,6 +231,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl // Set attribute and return false (continue) pRequest.setAttribute(attribRunOnce, ATTRIB_RUN_ONCE_VALUE); + return false; } @@ -286,7 +285,6 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @see ServletContext */ public ServletContext getServletContext() { - // TODO: Create a servlet context wrapper that lets you log to a log4j appender? return filterConfig.getServletContext(); } @@ -347,6 +345,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * * @deprecated For compatibility only, use {@link #init init} instead. */ + @SuppressWarnings("UnusedDeclaration") public void setFilterConfig(final FilterConfig pFilterConfig) { try { init(pFilterConfig); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java index 0653e115..7f2198ca 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java @@ -39,7 +39,7 @@ import java.lang.reflect.InvocationTargetException; *

* {@code GenericServlet} has an auto-init system, that automatically invokes * the method matching the signature {@code void setX(<Type>)}, - * for every init-parameter {@code x}. Both camelCase and lisp-style paramter + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter * naming is supported, lisp-style names will be converted to camelCase. * Parameter values are automatically converted from string representation to * most basic types, if necessary. @@ -50,6 +50,7 @@ import java.lang.reflect.InvocationTargetException; * @version $Id: GenericServlet.java#1 $ */ public abstract class GenericServlet extends javax.servlet.GenericServlet { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil /** * Called by the web container to indicate to a servlet that it is being diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java index ea5d0754..cff681bf 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java @@ -39,7 +39,7 @@ import java.lang.reflect.InvocationTargetException; *

* {@code HttpServlet} has an auto-init system, that automatically invokes * the method matching the signature {@code void setX(<Type>)}, - * for every init-parameter {@code x}. Both camelCase and lisp-style paramter + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter * naming is supported, lisp-style names will be converted to camelCase. * Parameter values are automatically converted from string representation to * most basic types, if necessary. @@ -50,6 +50,7 @@ import java.lang.reflect.InvocationTargetException; * @version $Id: HttpServlet.java#1 $ */ public abstract class HttpServlet extends javax.servlet.http.HttpServlet { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil /** * Called by the web container to indicate to a servlet that it is being diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java index 3032010a..bf645e53 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java @@ -28,17 +28,20 @@ package com.twelvemonkeys.servlet; -import com.twelvemonkeys.lang.Validate; - import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.OutputStream; + +import static com.twelvemonkeys.lang.Validate.notNull; /** * A delegate for handling stream support in wrapped servlet responses. + *

+ * Client code should delegate {@code getOutputStream}, {@code getWriter}, + * {@code flushBuffer} and {@code resetBuffer} methods from the servlet response. * * @author Harald Kuhr * @author last modified by $Author: haku $ @@ -48,35 +51,33 @@ public class ServletResponseStreamDelegate { private Object out = null; protected final ServletResponse response; - public ServletResponseStreamDelegate(ServletResponse pResponse) { - response = Validate.notNull(pResponse, "response"); + public ServletResponseStreamDelegate(final ServletResponse pResponse) { + response = notNull(pResponse, "response"); } - // NOTE: Intentionally NOT threadsafe, as one request/response should be - // handled by one thread ONLY. + // NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY. public final ServletOutputStream getOutputStream() throws IOException { if (out == null) { OutputStream out = createOutputStream(); this.out = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out); } else if (out instanceof PrintWriter) { - throw new IllegalStateException("getWriter() allready called."); + throw new IllegalStateException("getWriter() already called."); } return (ServletOutputStream) out; } - // NOTE: Intentionally NOT threadsafe, as one request/response should be - // handled by one thread ONLY. + // NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY. public final PrintWriter getWriter() throws IOException { if (out == null) { - // NOTE: getCharacterEncoding may should not return null + // NOTE: getCharacterEncoding may/should not return null OutputStream out = createOutputStream(); String charEncoding = response.getCharacterEncoding(); this.out = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out)); } else if (out instanceof ServletOutputStream) { - throw new IllegalStateException("getOutputStream() allready called."); + throw new IllegalStateException("getOutputStream() already called."); } return (PrintWriter) out; @@ -84,8 +85,9 @@ public class ServletResponseStreamDelegate { /** * Returns the {@code OutputStream}. - * Override this method to provide a decoreated outputstream. - * This method is guaranteed to be invoked only once for a request/response. + * Subclasses should override this method to provide a decorated output stream. + * This method is guaranteed to be invoked only once for a request/response + * (unless {@code resetBuffer} is invoked). *

* This implementation simply returns the output stream from the wrapped * response. @@ -107,7 +109,6 @@ public class ServletResponseStreamDelegate { } public void resetBuffer() { - // TODO: Is this okay? Probably not... :-) out = null; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java index fe22c930..6190420d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java @@ -63,17 +63,15 @@ import java.util.Iterator; * The response also automatically handles writing the image back to the underlying response stream * in the preferred format, when the response is flushed. *

- * * @author Harald Kuhr * @version $Id: ImageServletResponseImpl.java#10 $ - * */ // TODO: Refactor out HTTP specifics (if possible). // TODO: Is it a good ide to throw IIOException? // TODO: This implementation has a problem if two filters does scaling, as the second will overwrite the SIZE attribute // TODO: Allow different scaling algorithm based on input image (use case: IndexColorModel does not scale well using default, smooth may be slow for large images) +// TODO: Support pluggable pre- and post-processing steps class ImageServletResponseImpl extends HttpServletResponseWrapper implements ImageServletResponse { - private ServletRequest originalRequest; private final ServletContext context; private final ServletResponseStreamDelegate streamDelegate; @@ -223,6 +221,9 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima // The default JPEG quality is not good enough, so always adjust compression/quality if ((requestQuality != null || "jpeg".equalsIgnoreCase(getFormatNameSafe(writer))) && param.canWriteCompressed()) { + // TODO: See http://blog.apokalyptik.com/2009/09/16/quality-time-with-your-jpegs/ for better adjusting the (default) JPEG quality + // OR: Use the metadata of the original image + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // WORKAROUND: Known bug in GIFImageWriter in certain JDK versions, compression type is not set by default @@ -234,10 +235,13 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } if ("gif".equalsIgnoreCase(getFormatNameSafe(writer)) && !(image.getColorModel() instanceof IndexColorModel) - && image.getColorModel().getTransparency() != Transparency.OPAQUE) { + /*&& image.getColorModel().getTransparency() != Transparency.OPAQUE*/) { // WORKAROUND: Bug in GIFImageWriter may throw NPE if transparent pixels // See: http://bugs.sun.com/view_bug.do?bug_id=6287936 - image = ImageUtil.createIndexed(ImageUtil.toBuffered(image), 256, null, ImageUtil.TRANSPARENCY_BITMASK | ImageUtil.DITHER_DIFFUSION_ALTSCANS); + image = ImageUtil.createIndexed( + ImageUtil.toBuffered(image), 256, null, + (image.getColorModel().getTransparency() == Transparency.OPAQUE ? ImageUtil.TRANSPARENCY_OPAQUE : ImageUtil.TRANSPARENCY_BITMASK) | ImageUtil.DITHER_DIFFUSION_ALTSCANS + ); } ////////////////// ImageOutputStream stream = ImageIO.createImageOutputStream(out); @@ -425,18 +429,29 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima if (image != null && size != null && (image.getWidth() != size.width || image.getHeight() != size.height)) { int resampleAlgorithm = getResampleAlgorithmFromRequest(); + // TODO: One possibility is to NOT handle index color here, and only handle it later, IF NEEDED (read: GIF, + // possibly also for PNG) when we know the output format (flush method). + // This will make the filter faster (and better quality, possibly at the expense of more bytes being sent + // over the wire) in the general case. Who uses GIF nowadays anyway? + // Also, this means we could either keep the original IndexColorModel in the filter, or go through the + // expensive operation of re-calculating the optimal palette for the new image (the latter might improve quality). + // NOTE: Only use createScaled if IndexColorModel, as it's more expensive due to color conversion - if (image.getColorModel() instanceof IndexColorModel) { - return ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); +/* if (image.getColorModel() instanceof IndexColorModel) { +// return ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); + BufferedImage resampled = ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); + return ImageUtil.createIndexed(resampled, (IndexColorModel) image.getColorModel(), null, ImageUtil.DITHER_NONE | ImageUtil.TRANSPARENCY_BITMASK); +// return ImageUtil.createIndexed(resampled, 256, null, ImageUtil.COLOR_SELECTION_QUALITY | ImageUtil.DITHER_NONE | ImageUtil.TRANSPARENCY_BITMASK); } else { + */ return ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); - } +// } } return image; } - private int getResampleAlgorithmFromRequest() { + int getResampleAlgorithmFromRequest() { Object algorithm = originalRequest.getAttribute(ATTRIB_IMAGE_RESAMPLE_ALGORITHM); if (algorithm instanceof Integer && ((Integer) algorithm == Image.SCALE_SMOOTH || (Integer) algorithm == Image.SCALE_FAST || (Integer) algorithm == Image.SCALE_DEFAULT)) { return (Integer) algorithm; @@ -445,6 +460,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima if (algorithm != null) { context.log("WARN: Illegal image resampling algorithm: " + algorithm); } + return BufferedImage.SCALE_DEFAULT; } } diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java index 5210151d..c42f316d 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java @@ -2,10 +2,13 @@ package com.twelvemonkeys.servlet.image; import com.twelvemonkeys.image.BufferedImageIcon; import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.servlet.OutputStreamAdapter; import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import javax.imageio.ImageIO; import javax.servlet.ServletContext; @@ -15,8 +18,8 @@ import javax.servlet.http.HttpServletResponse; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; @@ -30,7 +33,7 @@ import static org.mockito.Mockito.*; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java#6 $ + * @version $Id: twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java#6 $ */ public class ImageServletResponseImplTestCase { private static final String CONTENT_TYPE_BMP = "image/bmp"; @@ -42,9 +45,13 @@ public class ImageServletResponseImplTestCase { private static final String IMAGE_NAME_PNG = "12monkeys-splash.png"; private static final String IMAGE_NAME_GIF = "tux.gif"; + private static final String IMAGE_NAME_PNG_INDEXED = "star.png"; private static final Dimension IMAGE_DIMENSION_PNG = new Dimension(300, 410); private static final Dimension IMAGE_DIMENSION_GIF = new Dimension(250, 250); + private static final Dimension IMAGE_DIMENSION_PNG_INDEXED = new Dimension(199, 192); + + private static final int STREAM_DEFAULT_SIZE = 2000; private HttpServletRequest request; private ServletContext context; @@ -58,12 +65,19 @@ public class ImageServletResponseImplTestCase { context = mock(ServletContext.class); when(context.getResource("/" + IMAGE_NAME_PNG)).thenReturn(getClass().getResource(IMAGE_NAME_PNG)); when(context.getResource("/" + IMAGE_NAME_GIF)).thenReturn(getClass().getResource(IMAGE_NAME_GIF)); + when(context.getResource("/" + IMAGE_NAME_PNG_INDEXED)).thenReturn(getClass().getResource(IMAGE_NAME_PNG_INDEXED)); when(context.getMimeType("file.bmp")).thenReturn(CONTENT_TYPE_BMP); when(context.getMimeType("file.foo")).thenReturn(CONTENT_TYPE_FOO); when(context.getMimeType("file.gif")).thenReturn(CONTENT_TYPE_GIF); when(context.getMimeType("file.jpeg")).thenReturn(CONTENT_TYPE_JPEG); when(context.getMimeType("file.png")).thenReturn(CONTENT_TYPE_PNG); when(context.getMimeType("file.txt")).thenReturn(CONTENT_TYPE_TEXT); + + MockLogger mockLogger = new MockLogger(); + doAnswer(mockLogger).when(context).log(anyString()); + doAnswer(mockLogger).when(context).log(anyString(), any(Throwable.class)); + //noinspection deprecation + doAnswer(mockLogger).when(context).log(any(Exception.class), anyString()); } private void fakeResponse(HttpServletRequest pRequest, ImageServletResponseImpl pImageResponse) throws IOException { @@ -98,7 +112,7 @@ public class ImageServletResponseImplTestCase { @Test public void testBasicResponse() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -118,7 +132,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -133,7 +147,7 @@ public class ImageServletResponseImplTestCase { @Test public void testNoOpResponse() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -157,7 +171,7 @@ public class ImageServletResponseImplTestCase { // Transcode original PNG to JPEG with no other changes @Test public void testTranscodeResponsePNGToJPEG() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -173,6 +187,12 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); + // Assert JPEG + ByteArrayInputStream input = out.createInputStream(); + assertEquals(0xFF, input.read()); + assertEquals(0xD8, input.read()); + assertEquals(0xFF, input.read()); + // Test that image data is still readable /* File tempFile = File.createTempFile("imageservlet-test-", ".jpeg"); @@ -182,7 +202,7 @@ public class ImageServletResponseImplTestCase { System.err.println("open " + tempFile); */ - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); @@ -208,43 +228,53 @@ public class ImageServletResponseImplTestCase { // (even if there's only one possible compression mode/type combo; MODE_EXPLICIT/"LZW") @Test public void testTranscodeResponsePNGToGIFWithQuality() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); - HttpServletResponse response = mock(HttpServletResponse.class); - when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - when(request.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY)).thenReturn(.5f); // Force quality setting in param + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + when(request.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY)).thenReturn(.5f); // Force quality setting in param - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); - fakeResponse(request, imageResponse); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); - // Force transcode to GIF - imageResponse.setOutputContentType("image/gif"); + // Force transcode to GIF + imageResponse.setOutputContentType("image/gif"); - // Flush image to wrapped response - imageResponse.flush(); + // Flush image to wrapped response + imageResponse.flush(); - assertTrue("Content has no data", out.size() > 0); + assertTrue("Content has no data", out.size() > 0); - // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); - assertNotNull(outImage); - assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); - assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); + // Assert GIF + ByteArrayInputStream stream = out.createInputStream(); + assertEquals('G', stream.read()); + assertEquals('I', stream.read()); + assertEquals('F', stream.read()); + assertEquals('8', stream.read()); + assertEquals('9', stream.read()); + assertEquals('a', stream.read()); - BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(out.createInputStream()); + assertNotNull(outImage); + assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); + assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); - // Should keep transparency, but is now binary - assertSimilarImageTransparent(image, outImage, 120f); + BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); - verify(response).setContentType(CONTENT_TYPE_GIF); - verify(response).getOutputStream(); - } + // Should keep transparency, but is now binary +// showIt(image, outImage, null); + assertSimilarImageTransparent(image, outImage, 50f); + + verify(response).setContentType(CONTENT_TYPE_GIF); + verify(response).getOutputStream(); + } // WORKAROUND: Bug in GIFImageWriter may throw NPE if transparent pixels // See: http://bugs.sun.com/view_bug.do?bug_id=6287936 @Test public void testTranscodeResponsePNGToGIF() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -260,7 +290,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); @@ -268,7 +298,8 @@ public class ImageServletResponseImplTestCase { BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); // Should keep transparency, but is now binary - assertSimilarImageTransparent(image, outImage, 120f); +// showIt(image, outImage, null); + assertSimilarImageTransparent(image, outImage, 50f); verify(response).setContentType(CONTENT_TYPE_GIF); verify(response).getOutputStream(); @@ -297,13 +328,13 @@ public class ImageServletResponseImplTestCase { } @Test - public void testTranscodeResponseIndexedCM() throws IOException { + public void testTranscodeResponseIndexColorModelGIFToJPEG() throws IOException { // Custom setup HttpServletRequest request = mock(HttpServletRequest.class); when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_GIF); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -318,8 +349,14 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); + // Assert JPEG + ByteArrayInputStream stream = out.createInputStream(); + assertEquals(0xFF, stream.read()); + assertEquals(0xD8, stream.read()); + assertEquals(0xFF, stream.read()); + // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_GIF.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_GIF.height, outImage.getHeight()); @@ -329,6 +366,59 @@ public class ImageServletResponseImplTestCase { assertSimilarImage(image, outImage, 96f); } + @Test + // TODO: Insert bug id/reference here for regression tracking + public void testIndexedColorModelResizePNG() throws IOException { + // Results differ with algorithm, so we test each algorithm by itself + int[] algorithms = new int[] {Image.SCALE_DEFAULT, Image.SCALE_FAST, Image.SCALE_SMOOTH, Image.SCALE_REPLICATE, Image.SCALE_AREA_AVERAGING, 77}; + + for (int algorithm : algorithms) { + Dimension size = new Dimension(100, 100); + + // Custom setup + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).thenReturn(false); + when(request.getAttribute(ImageServletResponse.ATTRIB_IMAGE_RESAMPLE_ALGORITHM)).thenReturn(algorithm); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG_INDEXED); + + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); + + imageResponse.getImage(); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Assert format is still PNG + ByteArrayInputStream inputStream = out.createInputStream(); + assertEquals(0x89, inputStream.read()); + assertEquals('P', inputStream.read()); + assertEquals('N', inputStream.read()); + assertEquals('G', inputStream.read()); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(out.createInputStream()); + assertNotNull(outImage); + assertEquals(size.width, outImage.getWidth()); + assertEquals(size.height, outImage.getHeight()); + + BufferedImage read = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG_INDEXED)); + BufferedImage image = ImageUtil.createResampled(read, size.width, size.height, imageResponse.getResampleAlgorithmFromRequest()); + +// showIt(image, outImage, null); + + assertSimilarImageTransparent(image, outImage, 10f); + } + } + private static BufferedImage flatten(final BufferedImage pImage, final Color pBackgroundColor) { BufferedImage image = ImageUtil.toBuffered(pImage, BufferedImage.TYPE_INT_ARGB); @@ -369,32 +459,64 @@ public class ImageServletResponseImplTestCase { } private void assertSimilarImageTransparent(final BufferedImage pExpected, final BufferedImage pActual, final float pArtifactThreshold) { + IndexColorModel icm = pActual.getColorModel() instanceof IndexColorModel ? (IndexColorModel) pActual.getColorModel() : null; + Object pixel = null; + for (int y = 0; y < pExpected.getHeight(); y++) { for (int x = 0; x < pExpected.getWidth(); x++) { int expected = pExpected.getRGB(x, y); int actual = pActual.getRGB(x, y); - int alpha = (expected >> 24) & 0xff; + if (icm != null) { + // Look up, using ICM - boolean transparent = alpha < 0x40; + int alpha = (expected >> 24) & 0xff; + boolean transparent = alpha < 0x40; - // Multiply out alpha for each component - int expectedR = (int) ((((expected >> 16) & 0xff) * alpha) / 255f); - int expectedG = (int) ((((expected >> 8 ) & 0xff) * alpha) / 255f); - int expectedB = (int) ((( expected & 0xff) * alpha) / 255f); + if (transparent) { + int expectedLookedUp = icm.getRGB(icm.getTransparentPixel()); + assertRGBEquals(x, y, expectedLookedUp & 0xff000000, actual & 0xff000000, 0); + } + else { + pixel = icm.getDataElements(expected, pixel); + int expectedLookedUp = icm.getRGB(pixel); + assertRGBEquals(x, y, expectedLookedUp & 0xffffff, actual & 0xffffff, pArtifactThreshold); + } + } + else { + // Multiply out alpha for each component if pre-multiplied +// int expectedR = (int) ((((expected >> 16) & 0xff) * alpha) / 255f); +// int expectedG = (int) ((((expected >> 8) & 0xff) * alpha) / 255f); +// int expectedB = (int) (((expected & 0xff) * alpha) / 255f); - - assertEquals("a(" + x + "," + y + ")", transparent ? 0 : 0xff, (actual >> 24) & 0xff); - assertEquals("R(" + x + "," + y + ")", expectedR, (actual >> 16) & 0xff, pArtifactThreshold); - assertEquals("G(" + x + "," + y + ")", expectedG, (actual >> 8) & 0xff, pArtifactThreshold); - assertEquals("B(" + x + "," + y + ")", expectedB, actual & 0xff, pArtifactThreshold); + assertRGBEquals(x, y, expected, actual, pArtifactThreshold); + } } } } + private void assertRGBEquals(int x, int y, int expected, int actual, float pArtifactThreshold) { + int expectedA = (expected >> 24) & 0xff; + int expectedR = (expected >> 16) & 0xff; + int expectedG = (expected >> 8) & 0xff; + int expectedB = expected & 0xff; + + try { + assertEquals("Alpha", expectedA, (actual >> 24) & 0xff, pArtifactThreshold); + assertEquals("RGB", 0, (Math.abs(expectedR - ((actual >> 16) & 0xff)) + + Math.abs(expectedG - ((actual >> 8) & 0xff)) + + Math.abs(expectedB - ((actual) & 0xff))) / 3.0, pArtifactThreshold); + } + catch (AssertionError e) { + AssertionError assertionError = new AssertionError(String.format("@[%d,%d] expected: 0x%08x but was: 0x%08x", x, y, expected, actual)); + assertionError.initCause(e); + throw assertionError; + } + } + @Test public void testReplaceResponse() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -418,7 +540,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -527,7 +649,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -546,7 +668,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -564,7 +686,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -584,7 +706,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -605,7 +727,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -645,7 +767,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -666,7 +788,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -729,7 +851,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -748,7 +870,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -777,7 +899,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -796,7 +918,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -815,7 +937,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -836,7 +958,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -865,7 +987,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -886,7 +1008,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -905,7 +1027,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -927,7 +1049,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -964,7 +1086,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -986,7 +1108,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -1027,7 +1149,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -1049,7 +1171,7 @@ public class ImageServletResponseImplTestCase { when(request.getContextPath()).thenReturn("/ape"); when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(STREAM_DEFAULT_SIZE); HttpServletResponse response = mock(HttpServletResponse.class); when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); @@ -1070,7 +1192,7 @@ public class ImageServletResponseImplTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + BufferedImage outImage = ImageIO.read(out.createInputStream()); assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); @@ -1401,6 +1523,9 @@ public class ImageServletResponseImplTestCase { // TODO: Test getSize()... private static class BlackLabel extends JLabel { + private final Paint checkeredBG; + private boolean opaque = true; + public BlackLabel(final String text, final BufferedImage outImage) { super(text, new BufferedImageIcon(outImage), JLabel.CENTER); setOpaque(true); @@ -1409,6 +1534,71 @@ public class ImageServletResponseImplTestCase { setVerticalAlignment(JLabel.CENTER); setVerticalTextPosition(JLabel.BOTTOM); setHorizontalTextPosition(JLabel.CENTER); + + checkeredBG = createTexture(); + } + + @Override + public boolean isOpaque() { + return opaque && super.isOpaque(); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics; + + int iconHeight = getIcon() == null ? 0 : getIcon().getIconHeight() + getIconTextGap(); + + // Paint checkered bg behind icon + g.setPaint(checkeredBG); + g.fillRect(0, 0, getWidth(), getHeight()); + + // Paint black bg behind text + g.setColor(getBackground()); + g.fillRect(0, iconHeight, getWidth(), getHeight() - iconHeight); + + try { + opaque = false; + super.paintComponent(g); + } + finally { + opaque = true; + } + } + + private static Paint createTexture() { + GraphicsConfiguration graphicsConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + BufferedImage pattern = graphicsConfiguration.createCompatibleImage(20, 20); + Graphics2D g = pattern.createGraphics(); + try { + g.setColor(Color.LIGHT_GRAY); + g.fillRect(0, 0, pattern.getWidth(), pattern.getHeight()); + g.setColor(Color.GRAY); + g.fillRect(0, 0, pattern.getWidth() / 2, pattern.getHeight() / 2); + g.fillRect(pattern.getWidth() / 2, pattern.getHeight() / 2, pattern.getWidth() / 2, pattern.getHeight() / 2); + } + finally { + g.dispose(); + } + + return new TexturePaint(pattern, new Rectangle(pattern.getWidth(), pattern.getHeight())); + } + } + + private static class MockLogger implements Answer { + public Void answer(InvocationOnMock invocation) throws Throwable { + // either log(String), log(String, Throwable) or log(Exception, String) + Object[] arguments = invocation.getArguments(); + + String msg = (String) (arguments[0] instanceof String ? arguments[0] : arguments[1]); + Throwable t = (Throwable) (arguments[0] instanceof Exception ? arguments[0] : arguments.length > 1 ? arguments[1] : null); + + System.out.println("mock-context: " + msg); + if (t != null) { + t.printStackTrace(System.out); + } + + return null; } } } diff --git a/servlet/src/test/resources/com/twelvemonkeys/servlet/image/star.png b/servlet/src/test/resources/com/twelvemonkeys/servlet/image/star.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0730e90e9614e9683b6d0ff294de238277c23b GIT binary patch literal 5953 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