diff --git a/common/common-image/pom.xml b/common/common-image/pom.xml index dc1a9fba..df8b1320 100644 --- a/common/common-image/pom.xml +++ b/common/common-image/pom.xml @@ -1,35 +1,37 @@ - - + + 4.0.0 - - com.twelvemonkeys.common - common - 3.0-SNAPSHOT - - common-image + + com.twelvemonkeys.common + common + 3.0-SNAPSHOT + + common-image jar - TwelveMonkeys :: Common :: Image - - The TwelveMonkeys Common Image support - - + TwelveMonkeys :: Common :: Image + + The TwelveMonkeys Common Image support + - - ${project.groupId} - common-lang - - - ${project.groupId} - common-io - - - jmagick + + ${project.groupId} + common-lang + + + + ${project.groupId} + common-io + + + + jmagick jmagick - 6.2.4 - true - - + 6.2.4 + true + provided + + diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java b/common/common-image/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java index 7b8a3b46..ae3b8e9e 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java @@ -41,21 +41,24 @@ import java.util.ArrayList; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java#1 $ */ public abstract class AbstractImageSource implements ImageProducer { - private List mConsumers = new ArrayList(); - protected int mWidth; - protected int mHeight; - protected int mXOff; - protected int mYOff; + private List consumers = new ArrayList(); + protected int width; + protected int height; + protected int xOff; + protected int yOff; // ImageProducer interface - public void addConsumer(ImageConsumer pConsumer) { - if (mConsumers.contains(pConsumer)) { + public void addConsumer(final ImageConsumer pConsumer) { + if (consumers.contains(pConsumer)) { return; } - mConsumers.add(pConsumer); + + consumers.add(pConsumer); + try { initConsumer(pConsumer); sendPixels(pConsumer); + if (isConsumer(pConsumer)) { pConsumer.imageComplete(ImageConsumer.STATICIMAGEDONE); @@ -68,34 +71,35 @@ public abstract class AbstractImageSource implements ImageProducer { } catch (Exception e) { e.printStackTrace(); + if (isConsumer(pConsumer)) { pConsumer.imageComplete(ImageConsumer.IMAGEERROR); } } } - public void removeConsumer(ImageConsumer pConsumer) { - mConsumers.remove(pConsumer); + public void removeConsumer(final ImageConsumer pConsumer) { + consumers.remove(pConsumer); } /** - * This implementation silently ignores this instruction. If pixeldata is + * This implementation silently ignores this instruction. If pixel data is * not in TDLR order by default, subclasses must override this method. * * @param pConsumer the consumer that requested the resend * * @see ImageProducer#requestTopDownLeftRightResend(java.awt.image.ImageConsumer) */ - public void requestTopDownLeftRightResend(ImageConsumer pConsumer) { + public void requestTopDownLeftRightResend(final ImageConsumer pConsumer) { // ignore } - public void startProduction(ImageConsumer pConsumer) { + public void startProduction(final ImageConsumer pConsumer) { addConsumer(pConsumer); } - public boolean isConsumer(ImageConsumer pConsumer) { - return mConsumers.contains(pConsumer); + public boolean isConsumer(final ImageConsumer pConsumer) { + return consumers.contains(pConsumer); } protected abstract void initConsumer(ImageConsumer pConsumer); diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java index 1ece4bd4..73e52172 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java @@ -47,33 +47,34 @@ import java.io.IOException; */ public class AreaAverageOp implements BufferedImageOp, RasterOp { - final private int mWidth; - final private int mHeight; + final private int width; + final private int height; - private Rectangle mSourceRegion; + private Rectangle sourceRegion; public AreaAverageOp(final int pWidth, final int pHeight) { - mWidth = pWidth; - mHeight = pHeight; + width = pWidth; + height = pHeight; } public Rectangle getSourceRegion() { - if (mSourceRegion == null) { + if (sourceRegion == null) { return null; } - return new Rectangle(mSourceRegion); + + return new Rectangle(sourceRegion); } public void setSourceRegion(final Rectangle pSourceRegion) { if (pSourceRegion == null) { - mSourceRegion = null; + sourceRegion = null; } else { - if (mSourceRegion == null) { - mSourceRegion = new Rectangle(pSourceRegion); + if (sourceRegion == null) { + sourceRegion = new Rectangle(pSourceRegion); } else { - mSourceRegion.setBounds(pSourceRegion); + sourceRegion.setBounds(pSourceRegion); } } } @@ -93,7 +94,7 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { long start = System.currentTimeMillis(); // Straight-forward version - //Image scaled = src.getScaledInstance(mWidth, mHeight, Image.SCALE_AREA_AVERAGING); + //Image scaled = src.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING); //ImageUtil.drawOnto(result, scaled); //result = new BufferedImageFactory(scaled).getBufferedImage(); @@ -104,7 +105,7 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { AffineTransform xform = null; int w = src.getWidth(); int h = src.getHeight(); - while (w / 2 > mWidth && h / 2 > mHeight) { + while (w / 2 > width && h / 2 > height) { w /= 2; h /= 2; @@ -129,7 +130,7 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { src = temp.getSubimage(0, 0, w, h); } - resample(src, result, AffineTransform.getScaleInstance(mWidth / (double) w, mHeight / (double) h)); + resample(src, result, AffineTransform.getScaleInstance(width / (double) w, height / (double) h)); */ // The real version @@ -160,11 +161,11 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { private WritableRaster filterImpl(Raster src, WritableRaster dest) { //System.out.println("src: " + src); //System.out.println("dest: " + dest); - if (mSourceRegion != null) { - int cx = mSourceRegion.x; - int cy = mSourceRegion.y; - int cw = mSourceRegion.width; - int ch = mSourceRegion.height; + if (sourceRegion != null) { + int cx = sourceRegion.x; + int cy = sourceRegion.y; + int cw = sourceRegion.width; + int ch = sourceRegion.height; boolean same = src == dest; dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null); @@ -179,11 +180,11 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { // TODO: This don't work too well.. // The thing is that the step length and the scan length will vary, for // non-even (1/2, 1/4, 1/8 etc) resampling - int widthSteps = (width + mWidth - 1) / mWidth; - int heightSteps = (height + mHeight - 1) / mHeight; + int widthSteps = (width + this.width - 1) / this.width; + int heightSteps = (height + this.height - 1) / this.height; - final boolean oddX = width % mWidth != 0; - final boolean oddY = height % mHeight != 0; + final boolean oddX = width % this.width != 0; + final boolean oddY = height % this.height != 0; final int dataElements = src.getNumDataElements(); final int bands = src.getNumBands(); @@ -210,16 +211,16 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { } } - for (int y = 0; y < mHeight; y++) { - if (!oddY || y < mHeight) { + for (int y = 0; y < this.height; y++) { + if (!oddY || y < this.height) { scanH = heightSteps; } else { scanH = height - (y * heightSteps); } - for (int x = 0; x < mWidth; x++) { - if (!oddX || x < mWidth) { + for (int x = 0; x < this.width; x++) { + if (!oddX || x < this.width) { scanW = widthSteps; } else { @@ -243,8 +244,8 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { // //System.err.println("width: " + width); //System.err.println("height: " + height); - //System.err.println("mWidth: " + mWidth); - //System.err.println("mHeight: " + mHeight); + //System.err.println("width: " + width); + //System.err.println("height: " + height); // //e.printStackTrace(); continue; @@ -382,20 +383,20 @@ public class AreaAverageOp implements BufferedImageOp, RasterOp { public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { ColorModel cm = destCM != null ? destCM : src.getColorModel(); return new BufferedImage(cm, - ImageUtil.createCompatibleWritableRaster(src, cm, mWidth, mHeight), + ImageUtil.createCompatibleWritableRaster(src, cm, width, height), cm.isAlphaPremultiplied(), null); } public WritableRaster createCompatibleDestRaster(Raster src) { - return src.createCompatibleWritableRaster(mWidth, mHeight); + return src.createCompatibleWritableRaster(width, height); } public Rectangle2D getBounds2D(Raster src) { - return new Rectangle(mWidth, mHeight); + return new Rectangle(width, height); } public Rectangle2D getBounds2D(BufferedImage src) { - return new Rectangle(mWidth, mHeight); + return new Rectangle(width, height); } public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java index 97664a38..da7e8a48 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java @@ -67,7 +67,7 @@ public class BrightnessContrastFilter extends RGBImageFilter { } // Use a precalculated lookup table for performace - private int[] mLUT = null; + private final int[] LUT; /** * Creates a BrightnessContrastFilter with default values @@ -105,7 +105,7 @@ public class BrightnessContrastFilter extends RGBImageFilter { * {@code -1.0,..,0.0,..,1.0}. */ public BrightnessContrastFilter(float pBrightness, float pContrast) { - mLUT = createLUT(pBrightness, pContrast); + LUT = createLUT(pBrightness, pContrast); } private static int[] createLUT(float pBrightness, float pContrast) { @@ -157,9 +157,9 @@ public class BrightnessContrastFilter extends RGBImageFilter { int b = pARGB & 0xFF; // Scale to new contrast - r = mLUT[r]; - g = mLUT[g]; - b = mLUT[b]; + r = LUT[r]; + g = LUT[g]; + b = LUT[b]; // Return ARGB pixel, leave transparency as is return (pARGB & 0xFF000000) | (r << 16) | (g << 8) | b; diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java index 06154aef..69de39b3 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java @@ -1,543 +1,541 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.image; - -import java.awt.*; -import java.awt.image.*; -import java.util.*; -import java.util.List; -import java.lang.reflect.Array; - -/** - * A faster, lighter and easier way to convert an {@code Image} to a - * {@code BufferedImage} than using a {@code PixelGrabber}. - * Clients may provide progress listeners to monitor conversion progress. - *

- * Supports source image subsampling and source region extraction. - * Supports source images with 16 bit {@link ColorModel} and - * {@link DataBuffer#TYPE_USHORT} transfer type, without converting to - * 32 bit/TYPE_INT. - *

- * NOTE: Does not support images with more than one {@code ColorModel} or - * different types of pixel data. This is not very common. - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java#1 $ - */ -public final class BufferedImageFactory { - private List mListeners; - private int mPercentageDone; - - private ImageProducer mProducer; - private boolean mError; - private boolean mFetching; - private boolean mReadColorModelOnly; - - private int mX = 0; - private int mY = 0; - private int mWidth = -1; - private int mHeight = -1; - - private int mXSub = 1; - private int mYSub = 1; - - private int mOffset; - private int mScanSize; - - private ColorModel mSourceColorModel; - private Hashtable mSourceProperties; // ImageConsumer API dictates Hashtable - - private Object mSourcePixels; - - private BufferedImage mBuffered; - private ColorModel mColorModel; - - // NOTE: Just to not expose the inheritance - private final Consumer mConsumer = new Consumer(); - - /** - * Creates a {@code BufferedImageFactory}. - * @param pSource the source image - */ - public BufferedImageFactory(Image pSource) { - this(pSource.getSource()); - } - - /** - * Creates a {@code BufferedImageFactory}. - * @param pSource the source image producer - */ - public BufferedImageFactory(ImageProducer pSource) { - mProducer = pSource; - } - - /** - * Returns the {@code BufferedImage} extracted from the given - * {@code ImageSource}. Multiple requests will return the same image. - * - * @return the {@code BufferedImage} - * - * @throws ImageConversionException if the given {@code ImageSource} cannot - * be converted for some reason. - */ - public BufferedImage getBufferedImage() throws ImageConversionException { - doFetch(false); - return mBuffered; - } - - /** - * Returns the {@code ColorModel} extracted from the - * given {@code ImageSource}. Multiple requests will return the same model. - * - * @return the {@code ColorModel} - * - * @throws ImageConversionException if the given {@code ImageSource} cannot - * be converted for some reason. - */ - public ColorModel getColorModel() throws ImageConversionException { - doFetch(true); - return mBuffered != null ? mBuffered.getColorModel() : mColorModel; - } - - /** - * Frees resources used by this {@code BufferedImageFactory}. - */ - public void dispose() { - freeResources(); - mBuffered = null; - mColorModel = null; - } - - /** - * Aborts the image prodcution. - */ - public void abort() { - mConsumer.imageComplete(ImageConsumer.IMAGEABORTED); - } - - /** - * Sets the source region (AOI) for the new image. - * - * @param pRect the source region - */ - public void setSourceRegion(Rectangle pRect) { - // Refetch everything, if region changed - if (mX != pRect.x || mY != pRect.y || mWidth != pRect.width || mHeight != pRect.height) { - dispose(); - } - - mX = pRect.x; - mY = pRect.y; - mWidth = pRect.width; - mHeight = pRect.height; - } - - /** - * Sets the source subsampling for the new image. - * - * @param pXSub horisontal subsampling factor - * @param pYSub vertical subsampling factor - */ - public void setSourceSubsampling(int pXSub, int pYSub) { - // Refetch everything, if subsampling changed - if (mXSub != pXSub || mYSub != pYSub) { - dispose(); - } - - if (pXSub > 1) { - mXSub = pXSub; - } - if (pYSub > 1) { - mYSub = pYSub; - } - } - - private synchronized void doFetch(boolean pColorModelOnly) throws ImageConversionException { - if (!mFetching && (!pColorModelOnly && mBuffered == null || mBuffered == null && mSourceColorModel == null)) { - // NOTE: Subsampling is only applied if extracting full image - if (!pColorModelOnly && (mXSub > 1 || mYSub > 1)) { - // If only sampling a region, the region must be scaled too - if (mWidth > 0 && mHeight > 0) { - mWidth = (mWidth + mXSub - 1) / mXSub; - mHeight = (mHeight + mYSub - 1) / mYSub; - - mX = (mX + mXSub - 1) / mXSub; - mY = (mY + mYSub - 1) / mYSub; - } - - mProducer = new FilteredImageSource(mProducer, new SubsamplingFilter(mXSub, mYSub)); - } - - // Start fetching - mFetching = true; - mReadColorModelOnly = pColorModelOnly; - mProducer.startProduction(mConsumer); // Note: If single-thread (synchronous), this call will block - - - // Wait until the producer wakes us up, by calling imageComplete - while (mFetching) { - try { - wait(); - } - catch (InterruptedException e) { - throw new ImageConversionException("Image conversion aborted: " + e.getMessage(), e); - } - } - - if (mError) { - throw new ImageConversionException("Image conversion failed: ImageConsumer.IMAGEERROR."); - } - - if (pColorModelOnly) { - createColorModel(); - } - else { - createBuffered(); - } - } - } - - private void createColorModel() { - mColorModel = mSourceColorModel; - - // Clean up, in case any objects are copied/cloned, so we can free resources - freeResources(); - } - - private void createBuffered() { - if (mWidth > 0 && mHeight > 0) { - if (mSourceColorModel != null && mSourcePixels != null) { - // TODO: Fix pixel size / color model problem - WritableRaster raster = ImageUtil.createRaster(mWidth, mHeight, mSourcePixels, mSourceColorModel); - mBuffered = new BufferedImage(mSourceColorModel, raster, mSourceColorModel.isAlphaPremultiplied(), mSourceProperties); - } - else { - mBuffered = ImageUtil.createClear(mWidth, mHeight, null); - } - } - - // Clean up, in case any objects are copied/cloned, so we can free resources - freeResources(); - } - - private void freeResources() { - mSourceColorModel = null; - mSourcePixels = null; - mSourceProperties = null; - } - - private void processProgress(int mScanline) { - if (mListeners != null) { - int percent = 100 * mScanline / mHeight; - - //System.out.println("Progress: " + percent + "%"); - - if (percent > mPercentageDone) { - mPercentageDone = percent; - - // TODO: Fix concurrent modification if a listener removes itself... - for (ProgressListener listener : mListeners) { - listener.progress(this, percent); - } - } - } - } - - /** - * Adds a progress listener to this factory. - * - * @param pListener the progress listener - */ - public void addProgressListener(ProgressListener pListener) { - if (mListeners == null) { - mListeners = new ArrayList(); - } - mListeners.add(pListener); - } - - /** - * Removes a progress listener from this factory. - * - * @param pListener the progress listener - */ - public void removeProgressListener(ProgressListener pListener) { - if (mListeners == null) { - return; - } - mListeners.remove(pListener); - } - - /** - * Removes all progress listeners from this factory. - */ - public void removeAllProgressListeners() { - if (mListeners != null) { - mListeners.clear(); - } - } - - /** - * Converts an array of {@code int} pixles to an array of {@code short} - * pixels. The conversion is done, by masking out the - * higher 16 bits of the {@code int}. - * - * For eny given {@code int}, the {@code short} value is computed as - * follows: - *

{@code - * short value = (short) (intValue & 0x0000ffff); - * }
- * - * @param pPixels the pixel data to convert - * @return an array of {@code short}s, same lenght as {@code pPixels} - */ - private static short[] toShortPixels(int[] pPixels) { - short[] pixels = new short[pPixels.length]; - for (int i = 0; i < pixels.length; i++) { - pixels[i] = (short) (pPixels[i] & 0xffff); - } - return pixels; - } - - /** - * This interface allows clients of a {@code BufferedImageFactory} to - * receive notifications of decoding progress. - * - * @see BufferedImageFactory#addProgressListener - * @see BufferedImageFactory#removeProgressListener - */ - public static interface ProgressListener extends EventListener { - - /** - * Reports progress to this listener. - * Invoked by the {@code BufferedImageFactory} to report progress in - * the image decoding. - * - * @param pFactory the factory reporting the progress - * @param pPercentage the perccentage of progress - */ - void progress(BufferedImageFactory pFactory, float pPercentage); - } - - private class Consumer implements ImageConsumer { - /** - * Implementation of all setPixels methods. - * Note that this implementation assumes that all invocations for one - * image uses the same color model, and that the pixel data has the - * same type. - * - * @param pX x coordinate of pixel data region - * @param pY y coordinate of pixel data region - * @param pWidth width of pixel data region - * @param pHeight height of pixel data region - * @param pModel the color model of the pixel data - * @param pPixels the pixel data array - * @param pOffset the offset into the pixel data array - * @param pScanSize the scan size of the pixel data array - */ - private void setPixelsImpl(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, Object pPixels, int pOffset, int pScanSize) { - setColorModelOnce(pModel); - - if (pPixels == null) { - return; - } - - //System.out.println("Setting " + pPixels.getClass().getComponentType() + " pixels: " + Array.getLength(pPixels)); - - // Allocate array if neccessary - if (mSourcePixels == null) { - /* - System.out.println("ColorModel: " + pModel); - System.out.println("Scansize: " + pScanSize + " TrasferType: " + ImageUtil.getTransferType(pModel)); - System.out.println("Creating " + pPixels.getClass().getComponentType() + " array of length " + (mWidth * mHeight)); - */ - // Allocate a suitable source pixel array - // TODO: Should take pixel "width" into consideration, for byte packed rasters?! - // OR... Is anything but single-pixel models really supported by the API? - mSourcePixels = Array.newInstance(pPixels.getClass().getComponentType(), mWidth * mHeight); - mScanSize = mWidth; - mOffset = 0; - } - else if (mSourcePixels.getClass() != pPixels.getClass()) { - throw new IllegalStateException("Only one pixel type allowed"); - } - - // AOI stuff - if (pY < mY) { - int diff = mY - pY; - if (diff >= pHeight) { - return; - } - pOffset += pScanSize * diff; - pY += diff; - pHeight -= diff; - } - if (pY + pHeight > mY + mHeight) { - pHeight = (mY + mHeight) - pY; - if (pHeight <= 0) { - return; - } - } - - if (pX < mX) { - int diff = mX - pX; - if (diff >= pWidth) { - return; - } - pOffset += diff; - pX += diff; - pWidth -= diff; - } - if (pX + pWidth > mX + mWidth) { - pWidth = (mX + mWidth) - pX; - if (pWidth <= 0) { - return; - } - } - - int dstOffset = mOffset + (pY - mY) * mScanSize + (pX - mX); - - // Do the pixel copying - for (int i = pHeight; i > 0; i--) { - System.arraycopy(pPixels, pOffset, mSourcePixels, dstOffset, pWidth); - pOffset += pScanSize; - dstOffset += mScanSize; - } - - processProgress(pY + pHeight); - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, short[] pPixels, int pOffset, int pScanSize) { - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); - } - - private void setColorModelOnce(ColorModel pModel) { - // NOTE: There seems to be a "bug" in AreaAveragingScaleFilter, as it - // first passes the original colormodel through in setColorModel, then - // later replaces it with the default RGB in the first setPixels call - // (this is probably allowed according to the spec, but it's a waste of - // time and space). - if (mSourceColorModel != pModel) { - if (/*mSourceColorModel == null ||*/ mSourcePixels == null) { - mSourceColorModel = pModel; - } - else { - throw new IllegalStateException("Change of ColorModel after pixel delivery not supported"); - } - } - - // If color model is all we ask for, stop now - if (mReadColorModelOnly) { - mConsumer.imageComplete(ImageConsumer.IMAGEABORTED); - } - } - - /** {@code ImageConsumer} implementation, do not invoke */ - public void imageComplete(int pStatus) { - mFetching = false; - - if (mProducer != null) { - mProducer.removeConsumer(this); - } - - switch (pStatus) { - case IMAGEERROR: - new Error().printStackTrace(); - mError = true; - break; - } - - synchronized (BufferedImageFactory.this) { - BufferedImageFactory.this.notifyAll(); - } - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setColorModel(ColorModel pModel) { - //System.out.println("SetColorModel: " + pModel); - setColorModelOnce(pModel); - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setDimensions(int pWidth, int pHeight) { - //System.out.println("Setting dimensions: " + pWidth + ", " + pHeight); - if (mWidth < 0) { - mWidth = pWidth - mX; - } - if (mHeight < 0) { - mHeight = pHeight - mY; - } - - // Hmm.. Special case, but is it a good idea? - if (mWidth <= 0 || mHeight <= 0) { - imageComplete(STATICIMAGEDONE); - } - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setHints(int pHintflags) { - // ignore - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) { - /*if (pModel.getPixelSize() < 8) { - // Byte packed - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toBytePackedPixels(pPixels, pModel.getPixelSize()), pOffset, pScanSize); - } - /* - else if (pModel.getPixelSize() > 8) { - // Byte interleaved - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toByteInterleavedPixels(pPixels), pOffset, pScanSize); - } - */ - //else { - // Default, pixelSize == 8, one byte pr pixel - setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); - //} - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) { - if (ImageUtil.getTransferType(pModel) == DataBuffer.TYPE_USHORT) { - // NOTE: Workaround for limitation in ImageConsumer API - // Convert int[] to short[], to be compatible with the ColorModel - setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, toShortPixels(pPixels), pOffset, pScanSize); - } - else { - setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, pPixels, pOffset, pScanSize); - } - } - - /** {@code ImageConsumer} implementation, do not invoke directly */ - public void setProperties(Hashtable pProperties) { - mSourceProperties = pProperties; - } - } +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import com.twelvemonkeys.lang.Validate; + +import java.awt.*; +import java.awt.image.*; +import java.lang.reflect.Array; +import java.util.EventListener; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A faster, lighter and easier way to convert an {@code Image} to a + * {@code BufferedImage} than using a {@code PixelGrabber}. + * Clients may provide progress listeners to monitor conversion progress. + *

+ * Supports source image subsampling and source region extraction. + * Supports source images with 16 bit {@link ColorModel} and + * {@link DataBuffer#TYPE_USHORT} transfer type, without converting to + * 32 bit/TYPE_INT. + *

+ * NOTE: Does not support images with more than one {@code ColorModel} or + * different types of pixel data. This is not very common. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java#1 $ + */ +public final class BufferedImageFactory { + private List listeners; + private int percentageDone; + + private ImageProducer producer; + private ImageConversionException consumerException; + private volatile boolean fetching; + private boolean readColorModelOnly; + + private int x = 0; + private int y = 0; + private int width = -1; + private int height = -1; + + private int xSub = 1; + private int ySub = 1; + + private int offset; + private int scanSize; + + private ColorModel sourceColorModel; + private Hashtable sourceProperties; // ImageConsumer API dictates Hashtable + + private Object sourcePixels; + + private BufferedImage buffered; + private ColorModel colorModel; + + // NOTE: Just to not expose the inheritance + private final Consumer consumer = new Consumer(); + + /** + * Creates a {@code BufferedImageFactory}. + * @param pSource the source image + * @throws IllegalArgumentException if {@code pSource == null} + */ + public BufferedImageFactory(final Image pSource) { + this(pSource != null ? pSource.getSource() : null); + } + + /** + * Creates a {@code BufferedImageFactory}. + * @param pSource the source image producer + * @throws IllegalArgumentException if {@code pSource == null} + */ + public BufferedImageFactory(final ImageProducer pSource) { + Validate.notNull(pSource, "source"); + producer = pSource; + } + + /** + * Returns the {@code BufferedImage} extracted from the given + * {@code ImageSource}. Multiple requests will return the same image. + * + * @return the {@code BufferedImage} + * + * @throws ImageConversionException if the given {@code ImageSource} cannot + * be converted for some reason. + */ + public BufferedImage getBufferedImage() throws ImageConversionException { + doFetch(false); + return buffered; + } + + /** + * Returns the {@code ColorModel} extracted from the + * given {@code ImageSource}. Multiple requests will return the same model. + * + * @return the {@code ColorModel} + * + * @throws ImageConversionException if the given {@code ImageSource} cannot + * be converted for some reason. + */ + public ColorModel getColorModel() throws ImageConversionException { + doFetch(true); + return buffered != null ? buffered.getColorModel() : colorModel; + } + + /** + * Frees resources used by this {@code BufferedImageFactory}. + */ + public void dispose() { + freeResources(); + buffered = null; + colorModel = null; + } + + /** + * Aborts the image production. + */ + public void abort() { + consumer.imageComplete(ImageConsumer.IMAGEABORTED); + } + + /** + * Sets the source region (AOI) for the new image. + * + * @param pRegion the source region + */ + public void setSourceRegion(final Rectangle pRegion) { + // Re-fetch everything, if region changed + if (x != pRegion.x || y != pRegion.y || width != pRegion.width || height != pRegion.height) { + dispose(); + } + + x = pRegion.x; + y = pRegion.y; + width = pRegion.width; + height = pRegion.height; + } + + /** + * Sets the source subsampling for the new image. + * + * @param pXSub horizontal subsampling factor + * @param pYSub vertical subsampling factor + */ + public void setSourceSubsampling(int pXSub, int pYSub) { + // Re-fetch everything, if subsampling changed + if (xSub != pXSub || ySub != pYSub) { + dispose(); + } + + if (pXSub > 1) { + xSub = pXSub; + } + if (pYSub > 1) { + ySub = pYSub; + } + } + + private synchronized void doFetch(boolean pColorModelOnly) throws ImageConversionException { + if (!fetching && (!pColorModelOnly && buffered == null || buffered == null && sourceColorModel == null)) { + // NOTE: Subsampling is only applied if extracting full image + if (!pColorModelOnly && (xSub > 1 || ySub > 1)) { + // If only sampling a region, the region must be scaled too + if (width > 0 && height > 0) { + width = (width + xSub - 1) / xSub; + height = (height + ySub - 1) / ySub; + + x = (x + xSub - 1) / xSub; + y = (y + ySub - 1) / ySub; + } + + producer = new FilteredImageSource(producer, new SubsamplingFilter(xSub, ySub)); + } + + // Start fetching + fetching = true; + readColorModelOnly = pColorModelOnly; + + producer.startProduction(consumer); // Note: If single-thread (synchronous), this call will block + + // Wait until the producer wakes us up, by calling imageComplete + while (fetching) { + try { + wait(200l); + } + catch (InterruptedException e) { + throw new ImageConversionException("Image conversion aborted: " + e.getMessage(), e); + } + } + + if (consumerException != null) { + throw new ImageConversionException("Image conversion failed: " + consumerException.getMessage(), consumerException); + } + + if (pColorModelOnly) { + createColorModel(); + } + else { + createBuffered(); + } + } + } + + private void createColorModel() { + colorModel = sourceColorModel; + + // Clean up, in case any objects are copied/cloned, so we can free resources + freeResources(); + } + + private void createBuffered() { + if (width > 0 && height > 0) { + if (sourceColorModel != null && sourcePixels != null) { + // TODO: Fix pixel size / color model problem + WritableRaster raster = ImageUtil.createRaster(width, height, sourcePixels, sourceColorModel); + buffered = new BufferedImage(sourceColorModel, raster, sourceColorModel.isAlphaPremultiplied(), sourceProperties); + } + else { + buffered = ImageUtil.createClear(width, height, null); + } + } + + // Clean up, in case any objects are copied/cloned, so we can free resources + freeResources(); + } + + private void freeResources() { + sourceColorModel = null; + sourcePixels = null; + sourceProperties = null; + } + + private void processProgress(int mScanline) { + if (listeners != null) { + int percent = 100 * mScanline / height; + + //System.out.println("Progress: " + percent + "%"); + + if (percent > percentageDone) { + percentageDone = percent; + + for (ProgressListener listener : listeners) { + listener.progress(this, percent); + } + } + } + } + + /** + * Adds a progress listener to this factory. + * + * @param pListener the progress listener + */ + public void addProgressListener(ProgressListener pListener) { + if (pListener == null) { + return; + } + + if (listeners == null) { + listeners = new CopyOnWriteArrayList(); + } + + listeners.add(pListener); + } + + /** + * Removes a progress listener from this factory. + * + * @param pListener the progress listener + */ + public void removeProgressListener(ProgressListener pListener) { + if (pListener == null) { + return; + } + + if (listeners == null) { + return; + } + + listeners.remove(pListener); + } + + /** + * Removes all progress listeners from this factory. + */ + public void removeAllProgressListeners() { + if (listeners != null) { + listeners.clear(); + } + } + + /** + * Converts an array of {@code int} pixels to an array of {@code short} + * pixels. The conversion is done, by masking out the + * higher 16 bits of the {@code int}. + * + * For eny given {@code int}, the {@code short} value is computed as + * follows: + *

{@code + * short value = (short) (intValue & 0x0000ffff); + * }
+ * + * @param pPixels the pixel data to convert + * @return an array of {@code short}s, same lenght as {@code pPixels} + */ + private static short[] toShortPixels(int[] pPixels) { + short[] pixels = new short[pPixels.length]; + for (int i = 0; i < pixels.length; i++) { + pixels[i] = (short) (pPixels[i] & 0xffff); + } + return pixels; + } + + /** + * This interface allows clients of a {@code BufferedImageFactory} to + * receive notifications of decoding progress. + * + * @see BufferedImageFactory#addProgressListener + * @see BufferedImageFactory#removeProgressListener + */ + public static interface ProgressListener extends EventListener { + + /** + * Reports progress to this listener. + * Invoked by the {@code BufferedImageFactory} to report progress in + * the image decoding. + * + * @param pFactory the factory reporting the progress + * @param pPercentage the percentage of progress + */ + void progress(BufferedImageFactory pFactory, float pPercentage); + } + + private class Consumer implements ImageConsumer { + /** + * Implementation of all setPixels methods. + * Note that this implementation assumes that all invocations for one + * image uses the same color model, and that the pixel data has the + * same type. + * + * @param pX x coordinate of pixel data region + * @param pY y coordinate of pixel data region + * @param pWidth width of pixel data region + * @param pHeight height of pixel data region + * @param pModel the color model of the pixel data + * @param pPixels the pixel data array + * @param pOffset the offset into the pixel data array + * @param pScanSize the scan size of the pixel data array + */ + @SuppressWarnings({"SuspiciousSystemArraycopy"}) + private void setPixelsImpl(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, Object pPixels, int pOffset, int pScanSize) { + setColorModelOnce(pModel); + + if (pPixels == null) { + return; + } + + // Allocate array if necessary + if (sourcePixels == null) { + // Allocate a suitable source pixel array + // TODO: Should take pixel "width" into consideration, for byte packed rasters?! + // OR... Is anything but single-pixel models really supported by the API? + sourcePixels = Array.newInstance(pPixels.getClass().getComponentType(), width * height); + scanSize = width; + offset = 0; + } + else if (sourcePixels.getClass() != pPixels.getClass()) { + throw new IllegalStateException("Only one pixel type allowed"); + } + + // AOI stuff + if (pY < y) { + int diff = y - pY; + if (diff >= pHeight) { + return; + } + pOffset += pScanSize * diff; + pY += diff; + pHeight -= diff; + } + if (pY + pHeight > y + height) { + pHeight = (y + height) - pY; + if (pHeight <= 0) { + return; + } + } + + if (pX < x) { + int diff = x - pX; + if (diff >= pWidth) { + return; + } + pOffset += diff; + pX += diff; + pWidth -= diff; + } + if (pX + pWidth > x + width) { + pWidth = (x + width) - pX; + if (pWidth <= 0) { + return; + } + } + + int dstOffset = offset + (pY - y) * scanSize + (pX - x); + + // Do the pixel copying + for (int i = pHeight; i > 0; i--) { + System.arraycopy(pPixels, pOffset, sourcePixels, dstOffset, pWidth); + pOffset += pScanSize; + dstOffset += scanSize; + } + + processProgress(pY + pHeight); + } + + public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, short[] pPixels, int pOffset, int pScanSize) { + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); + } + + private void setColorModelOnce(final ColorModel pModel) { + // NOTE: There seems to be a "bug" in AreaAveragingScaleFilter, as it + // first passes the original color model through in setColorModel, then + // later replaces it with the default RGB in the first setPixels call + // (this is probably allowed according to the spec, but it's a waste of time and space). + if (sourceColorModel != pModel) { + if (/*sourceColorModel == null ||*/ sourcePixels == null) { + sourceColorModel = pModel; + } + else { + throw new IllegalStateException("Change of ColorModel after pixel delivery not supported"); + } + } + + // If color model is all we ask for, stop now + if (readColorModelOnly) { + consumer.imageComplete(ImageConsumer.IMAGEABORTED); + } + } + + public void imageComplete(int pStatus) { + fetching = false; + + if (producer != null) { + producer.removeConsumer(this); + } + + switch (pStatus) { + case ImageConsumer.IMAGEERROR: + consumerException = new ImageConversionException("ImageConsumer.IMAGEERROR"); + break; + } + + synchronized (BufferedImageFactory.this) { + BufferedImageFactory.this.notifyAll(); + } + } + + public void setColorModel(ColorModel pModel) { + setColorModelOnce(pModel); + } + + public void setDimensions(int pWidth, int pHeight) { + if (width < 0) { + width = pWidth - x; + } + if (height < 0) { + height = pHeight - y; + } + + // Hmm.. Special case, but is it a good idea? + if (width <= 0 || height <= 0) { + imageComplete(ImageConsumer.STATICIMAGEDONE); + } + } + + public void setHints(int pHintflags) { + // ignore + } + + public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) { + /*if (pModel.getPixelSize() < 8) { + // Byte packed + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toBytePackedPixels(pPixels, pModel.getPixelSize()), pOffset, pScanSize); + } + /* + else if (pModel.getPixelSize() > 8) { + // Byte interleaved + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toByteInterleavedPixels(pPixels), pOffset, pScanSize); + } + */ + //else { + // Default, pixelSize == 8, one byte pr pixel + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); + //} + } + + public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) { + if (ImageUtil.getTransferType(pModel) == DataBuffer.TYPE_USHORT) { + // NOTE: Workaround for limitation in ImageConsumer API + // Convert int[] to short[], to be compatible with the ColorModel + setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, toShortPixels(pPixels), pOffset, pScanSize); + } + else { + setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, pPixels, pOffset, pScanSize); + } + } + + public void setProperties(Hashtable pProperties) { + sourceProperties = pProperties; + } + } } \ No newline at end of file diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java index d254cdff..d4339b14 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.image; +import com.twelvemonkeys.lang.Validate; + import javax.swing.Icon; import java.awt.image.BufferedImage; import java.awt.*; @@ -41,51 +43,44 @@ import java.awt.geom.AffineTransform; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java#2 $ */ public class BufferedImageIcon implements Icon { - private final BufferedImage mImage; - private int mWidth; - private int mHeight; - private final boolean mFast; + private final BufferedImage image; + private int width; + private int height; + private final boolean fast; public BufferedImageIcon(BufferedImage pImage) { - this(pImage, pImage.getWidth(), pImage.getHeight()); + this(pImage, pImage != null ? pImage.getWidth() : 0, pImage != null ? pImage.getHeight() : 0); } public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) { - if (pImage == null) { - throw new IllegalArgumentException("image == null"); - } - if (pWidth <= 0 || pHeight <= 0) { - throw new IllegalArgumentException("Icon size must be positive"); - } + image = Validate.notNull(pImage, "image"); + width = Validate.isTrue(pWidth > 0, pWidth, "width must be positive: %d"); + height = Validate.isTrue(pHeight > 0, pHeight, "height must be positive: %d"); - mImage = pImage; - mWidth = pWidth; - mHeight = pHeight; - - mFast = pImage.getWidth() == mWidth && pImage.getHeight() == mHeight; + fast = image.getWidth() == width && image.getHeight() == height; } public int getIconHeight() { - return mHeight; + return height; } public int getIconWidth() { - return mWidth; + return width; } public void paintIcon(Component c, Graphics g, int x, int y) { - if (mFast || !(g instanceof Graphics2D)) { + if (fast || !(g instanceof Graphics2D)) { //System.out.println("Scaling fast"); - g.drawImage(mImage, x, y, mWidth, mHeight, null); + g.drawImage(image, x, y, width, height, null); } else { //System.out.println("Scaling using interpolation"); Graphics2D g2 = (Graphics2D) g; AffineTransform xform = AffineTransform.getTranslateInstance(x, y); - xform.scale(mWidth / (double) mImage.getWidth(), mHeight / (double) mImage.getHeight()); + xform.scale(width / (double) image.getWidth(), height / (double) image.getHeight()); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(mImage, xform, null); + g2.drawImage(image, xform, null); } } } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java index 59285204..8a6ac22c 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java @@ -73,14 +73,15 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { */ public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP - private final Kernel mKernel; - private final int mEdgeCondition; + private final Kernel kernel; + private final int edgeCondition; - private final ConvolveOp mConvolve; + private final ConvolveOp convolve; public ConvolveWithEdgeOp(final Kernel pKernel, final int pEdgeCondition, final RenderingHints pHints) { // Create convolution operation int edge; + switch (pEdgeCondition) { case EDGE_REFLECT: case EDGE_WRAP: @@ -90,9 +91,10 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { edge = pEdgeCondition; break; } - mKernel = pKernel; - mEdgeCondition = pEdgeCondition; - mConvolve = new ConvolveOp(pKernel, edge, pHints); + + kernel = pKernel; + edgeCondition = pEdgeCondition; + convolve = new ConvolveOp(pKernel, edge, pHints); } public ConvolveWithEdgeOp(final Kernel pKernel) { @@ -107,8 +109,8 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { throw new IllegalArgumentException("source image cannot be the same as the destination image"); } - int borderX = mKernel.getWidth() / 2; - int borderY = mKernel.getHeight() / 2; + int borderX = kernel.getWidth() / 2; + int borderY = kernel.getHeight() / 2; BufferedImage original = addBorder(pSource, borderX, borderY); @@ -126,7 +128,7 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { } // Do the filtering (if destination is null, a new image will be created) - destination = mConvolve.filter(original, destination); + destination = convolve.filter(original, destination); if (pSource != original) { // Remove the border @@ -137,7 +139,7 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { } private BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY) { - if ((mEdgeCondition & 2) == 0) { + if ((edgeCondition & 2) == 0) { return pOriginal; } @@ -158,7 +160,7 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { g.drawImage(pOriginal, pBorderX, pBorderY, null); // TODO: I guess we need the top/left etc, if the corner pixels are covered by the kernel - switch (mEdgeCondition) { + switch (edgeCondition) { case EDGE_REFLECT: // Top/left (empty) g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center @@ -186,7 +188,7 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right break; default: - throw new IllegalArgumentException("Illegal edge operation " + mEdgeCondition); + throw new IllegalArgumentException("Illegal edge operation " + edgeCondition); } } @@ -206,39 +208,39 @@ public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { * @see #EDGE_WRAP */ public int getEdgeCondition() { - return mEdgeCondition; + return edgeCondition; } public WritableRaster filter(final Raster pSource, final WritableRaster pDestination) { - return mConvolve.filter(pSource, pDestination); + return convolve.filter(pSource, pDestination); } public BufferedImage createCompatibleDestImage(final BufferedImage pSource, final ColorModel pDesinationColorModel) { - return mConvolve.createCompatibleDestImage(pSource, pDesinationColorModel); + return convolve.createCompatibleDestImage(pSource, pDesinationColorModel); } public WritableRaster createCompatibleDestRaster(final Raster pSource) { - return mConvolve.createCompatibleDestRaster(pSource); + return convolve.createCompatibleDestRaster(pSource); } public Rectangle2D getBounds2D(final BufferedImage pSource) { - return mConvolve.getBounds2D(pSource); + return convolve.getBounds2D(pSource); } public Rectangle2D getBounds2D(final Raster pSource) { - return mConvolve.getBounds2D(pSource); + return convolve.getBounds2D(pSource); } public Point2D getPoint2D(final Point2D pSourcePoint, final Point2D pDestinationPoint) { - return mConvolve.getPoint2D(pSourcePoint, pDestinationPoint); + return convolve.getPoint2D(pSourcePoint, pDestinationPoint); } public RenderingHints getRenderingHints() { - return mConvolve.getRenderingHints(); + return convolve.getRenderingHints(); } public Kernel getKernel() { - return mConvolve.getKernel(); + return convolve.getKernel(); } } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/CopyDither.java b/common/common-image/src/main/java/com/twelvemonkeys/image/CopyDither.java index 0e4b3e6e..041bb8a6 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/CopyDither.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/CopyDither.java @@ -51,7 +51,7 @@ import java.awt.image.WritableRaster; */ public class CopyDither implements BufferedImageOp, RasterOp { - protected IndexColorModel mIndexColorModel = null; + protected IndexColorModel indexColorModel = null; /** * Creates a {@code CopyDither}, using the given @@ -61,7 +61,7 @@ public class CopyDither implements BufferedImageOp, RasterOp { */ public CopyDither(IndexColorModel pICM) { // Store colormodel - mIndexColorModel = pICM; + indexColorModel = pICM; } /** @@ -83,17 +83,12 @@ public class CopyDither implements BufferedImageOp, RasterOp { * @throws ImageFilterException if {@code pDestCM} is not {@code null} or * an instance of {@code IndexColorModel}. */ - public final BufferedImage createCompatibleDestImage(BufferedImage pSource, - ColorModel pDestCM) { + public final BufferedImage createCompatibleDestImage(BufferedImage pSource, ColorModel pDestCM) { if (pDestCM == null) { - return new BufferedImage(pSource.getWidth(), pSource.getHeight(), - BufferedImage.TYPE_BYTE_INDEXED, - mIndexColorModel); + return new BufferedImage(pSource.getWidth(), pSource.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, indexColorModel); } else if (pDestCM instanceof IndexColorModel) { - return new BufferedImage(pSource.getWidth(), pSource.getHeight(), - BufferedImage.TYPE_BYTE_INDEXED, - (IndexColorModel) pDestCM); + return new BufferedImage(pSource.getWidth(), pSource.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) pDestCM); } else { throw new ImageFilterException("Only IndexColorModel allowed."); @@ -112,13 +107,7 @@ public class CopyDither implements BufferedImageOp, RasterOp { return createCompatibleDestRaster(pSrc, getICM(pSrc)); } - public final WritableRaster createCompatibleDestRaster(Raster pSrc, - IndexColorModel pIndexColorModel) { - /* - return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(), - BufferedImage.TYPE_BYTE_INDEXED, - pIndexColorModel).getRaster(); - */ + public final WritableRaster createCompatibleDestRaster(Raster pSrc, IndexColorModel pIndexColorModel) { return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight()); } @@ -207,8 +196,7 @@ public class CopyDither implements BufferedImageOp, RasterOp { * @return the destination image, or a new image, if {@code pDest} was * {@code null}. */ - public final BufferedImage filter(BufferedImage pSource, - BufferedImage pDest) { + public final BufferedImage filter(BufferedImage pSource, BufferedImage pDest) { // Create destination image, if none provided if (pDest == null) { pDest = createCompatibleDestImage(pSource, getICM(pSource)); @@ -238,16 +226,17 @@ public class CopyDither implements BufferedImageOp, RasterOp { } private IndexColorModel getICM(BufferedImage pSource) { - return (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY)); + return (indexColorModel != null ? indexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY)); } + private IndexColorModel getICM(Raster pSource) { - return (mIndexColorModel != null ? mIndexColorModel : createIndexColorModel(pSource)); + return (indexColorModel != null ? indexColorModel : createIndexColorModel(pSource)); } private IndexColorModel createIndexColorModel(Raster pSource) { - BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(), - BufferedImage.TYPE_INT_ARGB); + BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(), BufferedImage.TYPE_INT_ARGB); image.setData(pSource); + return IndexImage.getIndexColorModel(image, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY); } @@ -261,8 +250,7 @@ public class CopyDither implements BufferedImageOp, RasterOp { * @return the destination raster, or a new raster, if {@code pDest} was * {@code null}. */ - public final WritableRaster filter(final Raster pSource, WritableRaster pDest, - IndexColorModel pColorModel) { + public final WritableRaster filter(final Raster pSource, WritableRaster pDest, IndexColorModel pColorModel) { int width = pSource.getWidth(); int height = pSource.getHeight(); @@ -292,6 +280,7 @@ public class CopyDither implements BufferedImageOp, RasterOp { pDest.setDataElements(x, y, pixel); } } + return pDest; } } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java index 5d9c4e31..363b9542 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java @@ -36,34 +36,36 @@ import java.util.Random; * @author Harald Kuhr * @author last modified by $Author: haku $ * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java#1 $ + * @version $Id: DiffusionDither.java#1 $ * */ public class DiffusionDither implements BufferedImageOp, RasterOp { - protected IndexColorModel mIndexColorModel = null; - private boolean mAlternateScans = true; private static final int FS_SCALE = 1 << 8; private static final Random RANDOM = new Random(); + protected final IndexColorModel indexColorModel; + private boolean alternateScans = true; + /** * Creates a {@code DiffusionDither}, using the given * {@code IndexColorModel} for dithering into. * * @param pICM an IndexColorModel. */ - public DiffusionDither(IndexColorModel pICM) { - // Store colormodel - mIndexColorModel = pICM; + public DiffusionDither(final IndexColorModel pICM) { + // Store color model + indexColorModel = pICM; } /** * Creates a {@code DiffusionDither}, with no fixed - * {@code IndexColorModel}. The colormodel will be generated for each - * filtering, unless the dest image allready has an + * {@code IndexColorModel}. The color model will be generated for each + * filtering, unless the destination image already has an * {@code IndexColorModel}. */ public DiffusionDither() { + this(null); } /** @@ -74,7 +76,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { * @param pUse {@code true} if scan mode should be alternating left/right */ public void setAlternateScans(boolean pUse) { - mAlternateScans = pUse; + alternateScans = pUse; } /** @@ -86,8 +88,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { * @throws ImageFilterException if {@code pDestCM} is not {@code null} or * an instance of {@code IndexColorModel}. */ - public final BufferedImage createCompatibleDestImage(BufferedImage pSource, - ColorModel pDestCM) { + public final BufferedImage createCompatibleDestImage(BufferedImage pSource, ColorModel pDestCM) { if (pDestCM == null) { return new BufferedImage(pSource.getWidth(), pSource.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, @@ -107,7 +108,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { * Creates a compatible {@code Raster} to dither into. * Only {@code IndexColorModel} allowed. * - * @param pSrc + * @param pSrc the source raster * * @return a {@code WritableRaster} */ @@ -115,14 +116,16 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { return createCompatibleDestRaster(pSrc, getICM(pSrc)); } - public final WritableRaster createCompatibleDestRaster(Raster pSrc, - IndexColorModel pIndexColorModel) { + /** + * Creates a compatible {@code Raster} to dither into. + * + * @param pSrc the source raster. + * @param pIndexColorModel the index color model used to create a {@code Raster}. + * + * @return a {@code WritableRaster} + */ + public final WritableRaster createCompatibleDestRaster(Raster pSrc, IndexColorModel pIndexColorModel) { return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight()); - /* - return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(), - BufferedImage.TYPE_BYTE_INDEXED, - pIndexColorModel).getRaster(); - */ } @@ -221,8 +224,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { * @return the destination image, or a new image, if {@code pDest} was * {@code null}. */ - public final BufferedImage filter(BufferedImage pSource, - BufferedImage pDest) { + public final BufferedImage filter(BufferedImage pSource, BufferedImage pDest) { // Create destination image, if none provided if (pDest == null) { pDest = createCompatibleDestImage(pSource, getICM(pSource)); @@ -252,10 +254,10 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { } private IndexColorModel getICM(BufferedImage pSource) { - return (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK)); + return (indexColorModel != null ? indexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK)); } private IndexColorModel getICM(Raster pSource) { - return (mIndexColorModel != null ? mIndexColorModel : createIndexColorModel(pSource)); + return (indexColorModel != null ? indexColorModel : createIndexColorModel(pSource)); } private IndexColorModel createIndexColorModel(Raster pSource) { @@ -265,8 +267,6 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { return IndexImage.getIndexColorModel(image, 256, IndexImage.TRANSPARENCY_BITMASK); } - - /** * Performs a single-input/single-output dither operation, applying basic * Floyd-Steinberg error-diffusion to the image. @@ -278,8 +278,7 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { * @return the destination raster, or a new raster, if {@code pDest} was * {@code null}. */ - public final WritableRaster filter(final Raster pSource, WritableRaster pDest, - IndexColorModel pColorModel) { + public final WritableRaster filter(final Raster pSource, WritableRaster pDest, IndexColorModel pColorModel) { int width = pSource.getWidth(); int height = pSource.getHeight(); @@ -456,10 +455,11 @@ public class DiffusionDither implements BufferedImageOp, RasterOp { mNextErr = temperr; // Toggle direction - if (mAlternateScans) { + if (alternateScans) { forward = !forward; } } + return pDest; } } \ No newline at end of file diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/GrayFilter.java b/common/common-image/src/main/java/com/twelvemonkeys/image/GrayFilter.java index dd4a8ef2..03e2dee1 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/GrayFilter.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/GrayFilter.java @@ -48,8 +48,8 @@ public class GrayFilter extends RGBImageFilter { canFilterIndexColorModel = true; } - private int mLow = 0; - private float mRange = 1.0f; + private int low = 0; + private float range = 1.0f; /** * Constructs a GrayFilter using ITU color-conversion. @@ -82,8 +82,8 @@ public class GrayFilter extends RGBImageFilter { pHigh = 1f; } - mLow = (int) (pLow * 255f); - mRange = pHigh - pLow; + low = (int) (pLow * 255f); + range = pHigh - pLow; } @@ -118,9 +118,9 @@ public class GrayFilter extends RGBImageFilter { //int gray = (int) ((float) (r + g + b) / 3.0f); - if (mRange != 1.0f) { + if (range != 1.0f) { // Apply range - gray = mLow + (int) (gray * mRange); + gray = low + (int) (gray * range); } // Return ARGB pixel diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageFilterException.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageFilterException.java index d91bf053..d05cb5c4 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageFilterException.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageFilterException.java @@ -32,42 +32,20 @@ package com.twelvemonkeys.image; * This class wraps IllegalArgumentException as thrown by the * BufferedImageOp interface for more fine-grained control. * - * @author Harald Kuhr + * @author Harald Kuhr * * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java#1 $ */ public class ImageFilterException extends IllegalArgumentException { - private Throwable mCause = null; - - public ImageFilterException(String pStr) { - super(pStr); + public ImageFilterException(String message) { + super(message); } - public ImageFilterException(Throwable pT) { - initCause(pT); + public ImageFilterException(Throwable cause) { + super(cause); } - public ImageFilterException(String pStr, Throwable pT) { - super(pStr); - initCause(pT); - } - - public Throwable initCause(Throwable pThrowable) { - if (mCause != null) { - // May only be called once - throw new IllegalStateException(); - } - else if (pThrowable == this) { - throw new IllegalArgumentException(); - } - - mCause = pThrowable; - - // Hmmm... - return this; - } - - public Throwable getCause() { - return mCause; + public ImageFilterException(String message, Throwable cause) { + super(message, cause); } } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java index d7cba48a..9f08f0fd 100644 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java @@ -32,19 +32,17 @@ import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.*; - import java.util.Hashtable; /** * This class contains methods for basic image manipulation and conversion. * - * @todo Split palette generation out, into ColorModel classes. - * * @author Harald Kuhr * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $ */ public final class ImageUtil { + // TODO: Split palette generation out, into ColorModel classes (?) public final static int ROTATE_90_CCW = -90; public final static int ROTATE_90_CW = 90; @@ -59,12 +57,14 @@ public final class ImageUtil { * @see #EDGE_REFLECT */ public static final int EDGE_ZERO_FILL = ConvolveOp.EDGE_ZERO_FILL; + /** * Alias for {@link ConvolveOp#EDGE_NO_OP}. * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) * @see #EDGE_REFLECT */ public static final int EDGE_NO_OP = ConvolveOp.EDGE_NO_OP; + /** * Adds a border to the image while convolving. The border will reflect the * edges of the original image. This is usually a good default. @@ -74,6 +74,7 @@ public final class ImageUtil { * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) */ public static final int EDGE_REFLECT = 2; // as JAI BORDER_REFLECT + /** * Adds a border to the image while convolving. The border will wrap the * edges of the original image. This is usually the best choice for tiles. @@ -229,7 +230,7 @@ public final class ImageUtil { * The new image will have the same {@code ColorModel}, * {@code Raster} and properties as the original image, if possible. *

- * If the image is allready a {@code BufferedImage}, it is simply returned + * If the image is already a {@code BufferedImage}, it is simply returned * and no conversion takes place. * * @param pOriginal the image to convert. @@ -237,7 +238,7 @@ public final class ImageUtil { * @return a {@code BufferedImage} */ public static BufferedImage toBuffered(RenderedImage pOriginal) { - // Don't convert if it allready is a BufferedImage + // Don't convert if it already is a BufferedImage if (pOriginal instanceof BufferedImage) { return (BufferedImage) pOriginal; } @@ -283,7 +284,7 @@ public final class ImageUtil { * Converts the {@code RenderedImage} to a {@code BufferedImage} of the * given type. *

- * If the image is allready a {@code BufferedImage} of the given type, it + * If the image is already a {@code BufferedImage} of the given type, it * is simply returned and no conversion takes place. * * @param pOriginal the image to convert. @@ -297,7 +298,7 @@ public final class ImageUtil { * @see java.awt.image.BufferedImage#getType() */ public static BufferedImage toBuffered(RenderedImage pOriginal, int pType) { - // Don't convert if it allready is BufferedImage and correct type + // Don't convert if it already is BufferedImage and correct type if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType) { return (BufferedImage) pOriginal; } @@ -329,7 +330,7 @@ public final class ImageUtil { * given type. The new image will have the same {@code ColorModel}, * {@code Raster} and properties as the original image, if possible. *

- * If the image is allready a {@code BufferedImage} of the given type, it + * If the image is already a {@code BufferedImage} of the given type, it * is simply returned and no conversion takes place. *

* This method simply invokes @@ -354,7 +355,7 @@ public final class ImageUtil { * The new image will have the same {@code ColorModel}, {@code Raster} and * properties as the original image, if possible. *

- * If the image is allready a {@code BufferedImage}, it is simply returned + * If the image is already a {@code BufferedImage}, it is simply returned * and no conversion takes place. * * @param pOriginal the image to convert. @@ -365,7 +366,7 @@ public final class ImageUtil { * @throws ImageConversionException if the image cannot be converted */ public static BufferedImage toBuffered(Image pOriginal) { - // Don't convert if it allready is BufferedImage + // Don't convert if it already is BufferedImage if (pOriginal instanceof BufferedImage) { return (BufferedImage) pOriginal; } @@ -536,32 +537,45 @@ public final class ImageUtil { * * @param pOriginal the orignal image * @param pModel the original color model - * @param mWidth the requested width of the raster - * @param mHeight the requested height of the raster + * @param width the requested width of the raster + * @param height the requested height of the raster * * @return a new WritableRaster */ - static WritableRaster createCompatibleWritableRaster(BufferedImage pOriginal, ColorModel pModel, int mWidth, int mHeight) { + static WritableRaster createCompatibleWritableRaster(BufferedImage pOriginal, ColorModel pModel, int width, int height) { if (pModel == null || equals(pOriginal.getColorModel(), pModel)) { + int[] bOffs; switch (pOriginal.getType()) { case BufferedImage.TYPE_3BYTE_BGR: - int[] bOffs = {2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return + bOffs = new int[]{2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, - mWidth, mHeight, - mWidth * 3, 3, + width, height, + width * 3, 3, bOffs, null); case BufferedImage.TYPE_4BYTE_ABGR: case BufferedImage.TYPE_4BYTE_ABGR_PRE: bOffs = new int[] {3, 2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, - mWidth, mHeight, - mWidth * 4, 4, + width, height, + width * 4, 4, bOffs, null); + case BufferedImage.TYPE_CUSTOM: + // Peek into the sample model to see if we have a sample model that will be incompatible with the default case + SampleModel sm = pOriginal.getRaster().getSampleModel(); + if (sm instanceof ComponentSampleModel) { + bOffs = ((ComponentSampleModel) sm).getBandOffsets(); + return Raster.createInterleavedRaster(sm.getDataType(), + width, height, + width * bOffs.length, bOffs.length, + bOffs, null); + } + // Else fall through default: - return pOriginal.getColorModel().createCompatibleWritableRaster(mWidth, mHeight); + return pOriginal.getColorModel().createCompatibleWritableRaster(width, height); } } - return pModel.createCompatibleWritableRaster(mWidth, mHeight); + + return pModel.createCompatibleWritableRaster(width, height); } /** @@ -569,7 +583,7 @@ public final class ImageUtil { * The new image will have the same {@code ColorModel}, {@code Raster} and * properties as the original image, if possible. *

- * If the image is allready a {@code BufferedImage} of the given type, it + * If the image is already a {@code BufferedImage} of the given type, it * is simply returned and no conversion takes place. * * @param pOriginal the image to convert. @@ -597,7 +611,7 @@ public final class ImageUtil { * the color model */ private static BufferedImage toBuffered(Image pOriginal, int pType, IndexColorModel pICM) { - // Don't convert if it allready is BufferedImage and correct type + // Don't convert if it already is BufferedImage and correct type if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType && (pICM == null || equals(((BufferedImage) pOriginal).getColorModel(), pICM))) { @@ -784,7 +798,7 @@ public final class ImageUtil { * Creates a scaled instance of the given {@code Image}, and converts it to * a {@code BufferedImage} if needed. * If the original image is a {@code BufferedImage} the result will have - * same type and colormodel. Note that this implies overhead, and is + * same type and color model. Note that this implies overhead, and is * probably not useful for anything but {@code IndexColorModel} images. * * @param pImage the {@code Image} to scale @@ -820,7 +834,7 @@ public final class ImageUtil { BufferedImage scaled = createResampled(pImage, pWidth, pHeight, pHints); - // Convert if colormodels or type differ, to behave as documented + // Convert if color models or type differ, to behave as documented if (type != scaled.getType() && type != BI_TYPE_ANY || !equals(scaled.getColorModel(), cm)) { //System.out.print("Converting TYPE " + scaled.getType() + " -> " + type + "... "); //long start = System.currentTimeMillis(); @@ -965,9 +979,6 @@ public final class ImageUtil { } private static int convertAWTHints(int pHints) { - // TODO: These conversions are broken! - // box == area average - // point == replicate (or..?) switch (pHints) { case Image.SCALE_FAST: case Image.SCALE_REPLICATE: diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java b/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java index 950b195a..ef97bc5c 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/IndexImage.java @@ -89,6 +89,8 @@ import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -127,7 +129,7 @@ import java.util.List; * @author Thomas DeWeese * @author Jun Inamori * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java#1 $ + * @version $Id: IndexImage.java#1 $ * @see DiffusionDither */ class IndexImage { @@ -237,19 +239,22 @@ class IndexImage { if (this.val != val) { return false; } + count++; + return true; } } /** - * Used to define a cube of the colorspace. The cube can be split - * approximagely in half to generate two cubes. + * Used to define a cube of the color space. The cube can be split + * approximately in half to generate two cubes. */ private static class Cube { - int[] min = {0, 0, 0}, max = {255, 255, 255}; + int[] min = {0, 0, 0}; + int[] max = {255, 255, 255}; boolean done = false; - List[] colors = null; + List[] colors = null; int count = 0; static final int RED = 0; static final int GRN = 1; @@ -261,7 +266,7 @@ class IndexImage { * @param colors contains the 3D color histogram to be subdivided * @param count the total number of pixels in the 3D histogram. */ - public Cube(List[] colors, int count) { + public Cube(List[] colors, int count) { this.colors = colors; this.count = count; } @@ -312,20 +317,27 @@ class IndexImage { c0 = RED; c1 = GRN; } + Cube ret; ret = splitChannel(splitChannel, c0, c1); + if (ret != null) { return ret; } + ret = splitChannel(c0, splitChannel, c1); + if (ret != null) { return ret; } + ret = splitChannel(c1, splitChannel, c0); + if (ret != null) { return ret; } + done = true; return null; @@ -381,16 +393,13 @@ class IndexImage { for (int k = minIdx[c1]; k <= maxIdx[c1]; k++) { int idx = idx2 | (k << c1Sh4); - List v = colors[idx]; + List v = colors[idx]; if (v == null) { continue; } - Iterator itr = v.iterator(); - Counter c; - - while (itr.hasNext()) { - c = (Counter) itr.next(); + + for (Counter c : v) { val = c.val; vals[0] = (val & 0xFF0000) >> 16; vals[1] = (val & 0xFF00) >> 8; @@ -425,7 +434,6 @@ class IndexImage { int c = counts[i]; if (c == 0) { - // No counts below this so move up bottom of cube. if ((tcount == 0) && (i < max[splitChannel])) { this.min[splitChannel] = i + 1; @@ -438,10 +446,8 @@ class IndexImage { continue; } if ((half - tcount) <= ((tcount + c) - half)) { - // Then lastAdd is a better top idx for this then i. if (lastAdd == -1) { - // No lower place to break. if (c == this.count) { @@ -465,13 +471,11 @@ class IndexImage { else { if (i == this.max[splitChannel]) { if (c == this.count) { - // would move min up but that should // have happened already. return null;// no split to make. } else { - // Would like to break between i and i+1 // but no i+1 so use lastAdd and i; splitLo = lastAdd; @@ -503,6 +507,7 @@ class IndexImage { ret.max[c0] = this.max[c0]; ret.min[c1] = this.min[c1]; ret.max[c1] = this.max[c1]; + return ret; } @@ -515,6 +520,7 @@ class IndexImage { if (this.count == 0) { return 0; } + float red = 0, grn = 0, blu = 0; int minR = min[0], minG = min[1], minB = min[2]; int maxR = max[0], maxG = max[1], maxB = max[2]; @@ -531,20 +537,18 @@ class IndexImage { for (int k = minIdx[2]; k <= maxIdx[2]; k++) { int idx = idx2 | k; - List v = colors[idx]; + List v = colors[idx]; if (v == null) { continue; } - Iterator itr = v.iterator(); - Counter c; - while (itr.hasNext()) { - c = (Counter) itr.next(); + for (Counter c : v) { val = c.val; ired = (val & 0xFF0000) >> 16; igrn = (val & 0x00FF00) >> 8; iblu = (val & 0x0000FF); + if (((ired >= minR) && (ired <= maxR)) && ((igrn >= minG) && (igrn <= maxG)) && ((iblu >= minB) && (iblu <= maxB))) { weight = (c.count / (float) this.count); red += ((float) ired) * weight; @@ -579,10 +583,7 @@ class IndexImage { * This version will be removed in a later version of the API. */ public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, boolean pFast) { - - return getIndexColorModel(pImage, pNumberOfColors, pFast - ? COLOR_SELECTION_FAST - : COLOR_SELECTION_QUALITY); + return getIndexColorModel(pImage, pNumberOfColors, pFast ? COLOR_SELECTION_FAST : COLOR_SELECTION_QUALITY); } /** @@ -636,17 +637,12 @@ class IndexImage { // We now have at least a buffered image, create model from it if (icm == null) { icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints); - - //System.out.println("IndexColorModel created from colors."); - } + } else if (!(icm instanceof InverseColorMapIndexColorModel)) { // If possible, use faster code - //System.out.println("Wrappimg IndexColorModel in InverseColorMapIndexColorModel"); icm = new InverseColorMapIndexColorModel(icm); } - //else { - //System.out.println("Allredy InverseColorMapIndexColorModel"); - //} + return icm; } @@ -674,7 +670,8 @@ class IndexImage { int height = pImage.getHeight(); // Using 4 bits from R, G & B. - List[] colors = new List[1 << 12];// [4096] + @SuppressWarnings("unchecked") + List[] colors = new List[1 << 12];// [4096] // Speedup, doesn't decrease image quality much int step = 1; @@ -739,13 +736,16 @@ class IndexImage { while (numberOfCubes < pNumberOfColors) { while (cubes[fCube].isDone()) { fCube++; + if (fCube == numberOfCubes) { break; } } + if (fCube == numberOfCubes) { break; } + Cube cube = cubes[fCube]; Cube newCube = cube.split(); @@ -756,6 +756,7 @@ class IndexImage { cube = newCube; newCube = tmp; } + int j = fCube; int count = cube.count; @@ -765,17 +766,19 @@ class IndexImage { } cubes[j++] = cubes[i]; } + cubes[j++] = cube; count = newCube.count; + while (j < numberOfCubes) { if (cubes[j].count < count) { break; } j++; } - for (int i = numberOfCubes; i > j; i--) { - cubes[i] = cubes[i - 1]; - } + + System.arraycopy(cubes, j, cubes, j + 1, numberOfCubes - j); + cubes[j/*++*/] = newCube; numberOfCubes++; } @@ -803,15 +806,13 @@ class IndexImage { // - transparency added to all totally black colors? int numOfBits = 8; - // -- haraldK, 20021024, as suggested by Thomas E Deweese + // -- haraldK, 20021024, as suggested by Thomas E. Deweese // plus adding a transparent pixel IndexColorModel icm; if (useTransparency) { - //icm = new IndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); } else { - //icm = new IndexColorModel(numOfBits, r.length, r, g, b); icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b); } return icm; @@ -925,7 +926,7 @@ class IndexImage { * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, Color pMatte, int pHints) { - // NOTE: We need to apply matte before creating colormodel, otherwise we + // NOTE: We need to apply matte before creating color model, otherwise we // won't have colors for potential faded transitions IndexColorModel icm; @@ -985,15 +986,16 @@ class IndexImage { final int width = pImage.getWidth(); final int height = pImage.getHeight(); - // Support transparancy? + // Support transparency? boolean transparency = isTransparent(pHints) && (pImage.getColorModel().getTransparency() != Transparency.OPAQUE) && (pColors.getTransparency() != Transparency.OPAQUE); // Create image with solid background BufferedImage solid = pImage; - if (pMatte != null) {// transparency doesn't really matter + if (pMatte != null) { // transparency doesn't really matter solid = createSolid(pImage, pMatte); } + BufferedImage indexed; // Support TYPE_BYTE_BINARY, but only for 2 bit images, as the default @@ -1044,12 +1046,12 @@ class IndexImage { finally { g2d.dispose(); } + break; } // Transparency support, this approach seems lame, but it's the only // solution I've found until now (that actually works). - // Got anything to do with isPremultiplied? Hmm... if (transparency) { // Re-apply the alpha-channel of the original image applyAlpha(indexed, pImage); @@ -1183,14 +1185,14 @@ class IndexImage { * @param pAlpha the image containing the alpha */ private static void applyAlpha(BufferedImage pImage, BufferedImage pAlpha) { - // Apply alpha as transparancy, using threshold of 25% + // Apply alpha as transparency, using threshold of 25% for (int y = 0; y < pAlpha.getHeight(); y++) { for (int x = 0; x < pAlpha.getWidth(); x++) { // Get alpha component of pixel, if less than 25% opaque // (0x40 = 64 => 25% of 256), the pixel will be transparent if (((pAlpha.getRGB(x, y) >> 24) & 0xFF) < 0x40) { - pImage.setRGB(x, y, 0x00FFFFFF);// 100% transparent + pImage.setRGB(x, y, 0x00FFFFFF); // 100% transparent } } } @@ -1200,7 +1202,6 @@ class IndexImage { * This class is also a command-line utility. */ public static void main(String pArgs[]) { - // Defaults int argIdx = 0; int speedTest = -1; @@ -1237,14 +1238,13 @@ class IndexImage { speedTest = 10; } } - else - if ((pArgs[argIdx].charAt(1) == 'w') || pArgs[argIdx].equals("--overwrite")) { + else if ((pArgs[argIdx].charAt(1) == 'w') || pArgs[argIdx].equals("--overwrite")) { overWrite = true; argIdx++; } - else - if ((pArgs[argIdx].charAt(1) == 'c') || pArgs[argIdx].equals("--colors")) { + else if ((pArgs[argIdx].charAt(1) == 'c') || pArgs[argIdx].equals("--colors")) { argIdx++; + try { numColors = Integer.parseInt(pArgs[argIdx++]); } @@ -1253,34 +1253,28 @@ class IndexImage { break; } } - else - if ((pArgs[argIdx].charAt(1) == 'g') || pArgs[argIdx].equals("--grayscale")) { + else if ((pArgs[argIdx].charAt(1) == 'g') || pArgs[argIdx].equals("--grayscale")) { argIdx++; gray = true; } - else - if ((pArgs[argIdx].charAt(1) == 'm') || pArgs[argIdx].equals("--monochrome")) { + else if ((pArgs[argIdx].charAt(1) == 'm') || pArgs[argIdx].equals("--monochrome")) { argIdx++; numColors = 2; monochrome = true; } - else - if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--dither")) { + else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--dither")) { argIdx++; dither = pArgs[argIdx++]; } - else - if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--palette")) { + else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--palette")) { argIdx++; paletteFileName = pArgs[argIdx++]; } - else - if ((pArgs[argIdx].charAt(1) == 'q') || pArgs[argIdx].equals("--quality")) { + else if ((pArgs[argIdx].charAt(1) == 'q') || pArgs[argIdx].equals("--quality")) { argIdx++; quality = pArgs[argIdx++]; } - else - if ((pArgs[argIdx].charAt(1) == 'b') || pArgs[argIdx].equals("--bgcolor")) { + else if ((pArgs[argIdx].charAt(1) == 'b') || pArgs[argIdx].equals("--bgcolor")) { argIdx++; try { background = StringUtil.toColor(pArgs[argIdx++]); @@ -1290,18 +1284,15 @@ class IndexImage { break; } } - else - if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--transparency")) { + else if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--transparency")) { argIdx++; transparency = true; } - else - if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--outputformat")) { + else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--outputformat")) { argIdx++; format = StringUtil.toLowerCase(pArgs[argIdx++]); } - else - if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) { + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) { argIdx++; // Setting errArgs to true, to print usage @@ -1321,6 +1312,7 @@ class IndexImage { ? ", " : "\n")); } + System.err.print("Output format names: "); String[] writers = ImageIO.getWriterFormatNames(); @@ -1333,7 +1325,7 @@ class IndexImage { } // Read in image - java.io.File in = new java.io.File(pArgs[argIdx++]); + File in = new File(pArgs[argIdx++]); if (!in.exists()) { System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); @@ -1341,10 +1333,10 @@ class IndexImage { } // Read palette if needed - java.io.File paletteFile = null; + File paletteFile = null; if (paletteFileName != null) { - paletteFile = new java.io.File(paletteFileName); + paletteFile = new File(paletteFileName); if (!paletteFile.exists()) { System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); System.exit(5); @@ -1352,10 +1344,10 @@ class IndexImage { } // Make sure we can write - java.io.File out; + File out; if (argIdx < pArgs.length) { - out = new java.io.File(pArgs[argIdx/*++*/]); + out = new File(pArgs[argIdx/*++*/]); // Get format from file extension if (format == null) { @@ -1363,7 +1355,6 @@ class IndexImage { } } else { - // Create new file in current dir, same name + format extension String baseName = FileUtil.getBasename(in); @@ -1371,8 +1362,9 @@ class IndexImage { if (format == null) { format = "png"; } - out = new java.io.File(baseName + '.' + format); + out = new File(baseName + '.' + format); } + if (!overWrite && out.exists()) { System.err.println("The file \"" + out.getAbsolutePath() + "\" allready exists!"); System.exit(5); @@ -1396,7 +1388,7 @@ class IndexImage { } } } - catch (java.io.IOException ioe) { + catch (IOException ioe) { ioe.printStackTrace(System.err); System.exit(5); } @@ -1441,16 +1433,14 @@ class IndexImage { /////////////////////////////// // Index long start = 0; - long end; if (speedTest > 0) { - // SPEED TESTING System.out.println("Measuring speed!"); start = System.currentTimeMillis(); - // END SPEED TESTING } + BufferedImage indexed; IndexColorModel colors; @@ -1459,7 +1449,6 @@ class IndexImage { colors = MonochromeColorModel.getInstance(); } else if (gray) { - //indexed = ImageUtil.toBuffered(ImageUtil.grayscale(image), BufferedImage.TYPE_BYTE_GRAY); image = ImageUtil.toBuffered(ImageUtil.grayscale(image)); indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints); @@ -1470,7 +1459,6 @@ class IndexImage { } } else if (paletteImg != null) { - // Get palette from image indexed = getIndexedImage(ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB), colors = getIndexColorModel(paletteImg, numColors, hints), background, hints); @@ -1479,12 +1467,10 @@ class IndexImage { image = ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB); indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints); } + if (speedTest > 0) { - // SPEED TESTING - end = System.currentTimeMillis(); - System.out.println("Color selection + dither: " + (end - start) + " ms"); - + System.out.println("Color selection + dither: " + (System.currentTimeMillis() - start) + " ms"); // END SPEED TESTING } @@ -1494,11 +1480,11 @@ class IndexImage { System.err.println("No writer for format: \"" + format + "\"!"); } } - catch (java.io.IOException ioe) { + catch (IOException ioe) { ioe.printStackTrace(System.err); } - if (speedTest > 0) { + if (speedTest > 0) { // SPEED TESTING System.out.println("Measuring speed!"); @@ -1513,17 +1499,16 @@ class IndexImage { for (int i = 0; i < speedTest; i++) { start = System.currentTimeMillis(); getIndexedImage(image, colors, background, hints); - end = System.currentTimeMillis(); - time += (end - start); + time += (System.currentTimeMillis() - start); System.out.print('.'); if ((i + 1) % 10 == 0) { System.out.println("\nAverage (after " + (i + 1) + " iterations): " + (time / (i + 1)) + "ms"); } } + System.out.println("\nDither only:"); System.out.println("Total time (" + speedTest + " invocations): " + time + "ms"); System.out.println("Average: " + time / speedTest + "ms"); - // END SPEED TESTING } } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMap.java b/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMap.java index 5b334244..838275b6 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMap.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMap.java @@ -72,12 +72,12 @@ class InverseColorMap { */ final static int MAXQUANTVAL = 1 << 5; - byte[] mRGBMapByte; - int[] mRGBMapInt; - int mNumColors; - int mMaxColor; - byte[] mInverseRGB; // inverse rgb color map - int mTransparentIndex = -1; + byte[] rgbMapByte; + int[] rgbMapInt; + int numColors; + int maxColor; + byte[] inverseRGB; // inverse rgb color map + int transparentIndex = -1; /** * @param pRGBColorMap the rgb color map to create inverse color map for. @@ -99,11 +99,11 @@ class InverseColorMap { * @param pTransparent the index of the transparent pixel in the map */ InverseColorMap(byte[] pRGBColorMap, int pTransparent) { - mRGBMapByte = pRGBColorMap; - mNumColors = mRGBMapByte.length / 4; - mTransparentIndex = pTransparent; + rgbMapByte = pRGBColorMap; + numColors = rgbMapByte.length / 4; + transparentIndex = pTransparent; - mInverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; + inverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]); } @@ -112,11 +112,11 @@ class InverseColorMap { * @param pTransparent the index of the transparent pixel in the map */ InverseColorMap(int[] pRGBColorMap, int pTransparent) { - mRGBMapInt = pRGBColorMap; - mNumColors = mRGBMapInt.length; - mTransparentIndex = pTransparent; + rgbMapInt = pRGBColorMap; + numColors = rgbMapInt.length; + transparentIndex = pTransparent; - mInverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; + inverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]); } @@ -130,8 +130,8 @@ class InverseColorMap { final int xsqr = 1 << (TRUNCBITS * 2); // 64 - twice the smallest step size vale of quantized colors final int xsqr2 = xsqr + xsqr; - for (int i = 0; i < mNumColors; ++i) { - if (i == mTransparentIndex) { + for (int i = 0; i < numColors; ++i) { + if (i == transparentIndex) { // Skip the transparent pixel continue; } @@ -141,15 +141,15 @@ class InverseColorMap { int blue, b, bdist, binc, bxx; // HaraldK 20040801: Added support for int[] - if (mRGBMapByte != null) { - red = mRGBMapByte[i * 4] & 0xFF; - green = mRGBMapByte[i * 4 + 1] & 0xFF; - blue = mRGBMapByte[i * 4 + 2] & 0xFF; + if (rgbMapByte != null) { + red = rgbMapByte[i * 4] & 0xFF; + green = rgbMapByte[i * 4 + 1] & 0xFF; + blue = rgbMapByte[i * 4 + 2] & 0xFF; } - else if (mRGBMapInt != null) { - red = (mRGBMapInt[i] >> 16) & 0xFF; - green = (mRGBMapInt[i] >> 8) & 0xFF; - blue = mRGBMapInt[i] & 0xFF; + else if (rgbMapInt != null) { + red = (rgbMapInt[i] >> 16) & 0xFF; + green = (rgbMapInt[i] >> 8) & 0xFF; + blue = rgbMapInt[i] & 0xFF; } else { throw new IllegalStateException("colormap == null"); @@ -170,7 +170,7 @@ class InverseColorMap { for (b = 0, bdist = gdist, bxx = binc; b < MAXQUANTVAL; bdist += bxx, ++b, ++rgbI, bxx += xsqr2) { if (i == 0 || pTemp[rgbI] > bdist) { pTemp[rgbI] = bdist; - mInverseRGB[rgbI] = (byte) i; + inverseRGB[rgbI] = (byte) i; } } } @@ -187,7 +187,7 @@ class InverseColorMap { * created inverse color map. */ public final int getIndexNearest(int pColor) { - return mInverseRGB[((pColor >> (3 * TRUNCBITS)) & QUANTMASK_RED) + + return inverseRGB[((pColor >> (3 * TRUNCBITS)) & QUANTMASK_RED) + ((pColor >> (2 * TRUNCBITS)) & QUANTMASK_GREEN) + ((pColor >> (/* 1 * */ TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF; } @@ -203,7 +203,7 @@ class InverseColorMap { */ public final int getIndexNearest(int pRed, int pGreen, int pBlue) { // NOTE: the third line in expression for blue is shifting DOWN not UP. - return mInverseRGB[((pRed << (2 * QUANTBITS - TRUNCBITS)) & QUANTMASK_RED) + + return inverseRGB[((pRed << (2 * QUANTBITS - TRUNCBITS)) & QUANTMASK_RED) + ((pGreen << (/* 1 * */ QUANTBITS - TRUNCBITS)) & QUANTMASK_GREEN) + ((pBlue >> (TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF; } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java b/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java index 41fae813..8bba6e5e 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java @@ -46,13 +46,13 @@ import java.awt.image.IndexColorModel; */ public class InverseColorMapIndexColorModel extends IndexColorModel { - protected int mRGBs[]; - protected int mMapSize; + protected int rgbs[]; + protected int mapSize; - protected InverseColorMap mInverseMap = null; + protected InverseColorMap inverseMap = null; private final static int ALPHA_THRESHOLD = 0x80; - private int mWhiteIndex = -1; + private int whiteIndex = -1; private final static int WHITE = 0x00FFFFFF; private final static int RGB_MASK = 0x00FFFFFF; @@ -74,11 +74,11 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { ImageUtil.getTransferType(pColorModel), pColorModel.getValidPixels()); - mRGBs = pRGBs; - mMapSize = mRGBs.length; + rgbs = pRGBs; + mapSize = rgbs.length; - mInverseMap = new InverseColorMap(mRGBs); - mWhiteIndex = getWhiteIndex(); + inverseMap = new InverseColorMap(rgbs); + whiteIndex = getWhiteIndex(); } /** @@ -91,6 +91,7 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { private static int[] getRGBs(IndexColorModel pColorModel) { int[] rgb = new int[pColorModel.getMapSize()]; pColorModel.getRGBs(rgb); + return rgb; } @@ -111,15 +112,13 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { * * @see IndexColorModel#IndexColorModel(int, int, int[], int, boolean, int, int) */ - public InverseColorMapIndexColorModel(int pNumBits, int pSize, int[] pRGBs, - int pStart, boolean pAlpha, int pTransparentIndex, - int pTransferType) { + public InverseColorMapIndexColorModel(int pNumBits, int pSize, int[] pRGBs, int pStart, boolean pAlpha, int pTransparentIndex, int pTransferType) { super(pNumBits, pSize, pRGBs, pStart, pAlpha, pTransparentIndex, pTransferType); - mRGBs = getRGBs(this); - mMapSize = mRGBs.length; + rgbs = getRGBs(this); + mapSize = rgbs.length; - mInverseMap = new InverseColorMap(mRGBs, pTransparentIndex); - mWhiteIndex = getWhiteIndex(); + inverseMap = new InverseColorMap(rgbs, pTransparentIndex); + whiteIndex = getWhiteIndex(); } /** @@ -138,15 +137,13 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { * * @see IndexColorModel#IndexColorModel(int, int, byte[], byte[], byte[], int) */ - public InverseColorMapIndexColorModel(int pNumBits, int pSize, - byte[] pReds, byte[] pGreens, byte[] pBlues, - int pTransparentIndex) { + public InverseColorMapIndexColorModel(int pNumBits, int pSize, byte[] pReds, byte[] pGreens, byte[] pBlues, int pTransparentIndex) { super(pNumBits, pSize, pReds, pGreens, pBlues, pTransparentIndex); - mRGBs = getRGBs(this); - mMapSize = mRGBs.length; + rgbs = getRGBs(this); + mapSize = rgbs.length; - mInverseMap = new InverseColorMap(mRGBs, pTransparentIndex); - mWhiteIndex = getWhiteIndex(); + inverseMap = new InverseColorMap(rgbs, pTransparentIndex); + whiteIndex = getWhiteIndex(); } /** @@ -164,19 +161,18 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { * * @see IndexColorModel#IndexColorModel(int, int, byte[], byte[], byte[]) */ - public InverseColorMapIndexColorModel(int pNumBits, int pSize, - byte[] pReds, byte[] pGreens, byte[] pBlues) { + public InverseColorMapIndexColorModel(int pNumBits, int pSize, byte[] pReds, byte[] pGreens, byte[] pBlues) { super(pNumBits, pSize, pReds, pGreens, pBlues); - mRGBs = getRGBs(this); - mMapSize = mRGBs.length; + rgbs = getRGBs(this); + mapSize = rgbs.length; - mInverseMap = new InverseColorMap(mRGBs); - mWhiteIndex = getWhiteIndex(); + inverseMap = new InverseColorMap(rgbs); + whiteIndex = getWhiteIndex(); } private int getWhiteIndex() { - for (int i = 0; i < mRGBs.length; i++) { - int color = mRGBs[i]; + for (int i = 0; i < rgbs.length; i++) { + int color = rgbs[i]; if ((color & RGB_MASK) == WHITE) { return i; } @@ -244,7 +240,6 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { * */ public Object getDataElements(int rgb, Object pixel) { - int alpha = (rgb>>>24); int pix; @@ -253,11 +248,11 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { } else { int color = rgb & RGB_MASK; - if (color == WHITE && mWhiteIndex != -1) { - pix = mWhiteIndex; + if (color == WHITE && whiteIndex != -1) { + pix = whiteIndex; } else { - pix = mInverseMap.getIndexNearest(color); + pix = inverseMap.getIndexNearest(color); } } @@ -297,8 +292,7 @@ public class InverseColorMapIndexColorModel extends IndexColorModel { shortObj[0] = (short) pix; break; default: - throw new UnsupportedOperationException("This method has not been " + - "implemented for transferType " + transferType); + throw new UnsupportedOperationException("This method has not been implemented for transferType " + transferType); } return pixel; } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java b/common/common-image/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java index 9f8b8647..681f2854 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java @@ -54,11 +54,11 @@ final class MagickAccelerator { private static final int RESAMPLE_OP = 0; - private static Class[] sNativeOp = new Class[1]; + private static Class[] nativeOp = new Class[1]; static { try { - sNativeOp[RESAMPLE_OP] = Class.forName("com.twelvemonkeys.image.ResampleOp"); + nativeOp[RESAMPLE_OP] = Class.forName("com.twelvemonkeys.image.ResampleOp"); } catch (ClassNotFoundException e) { System.err.println("Could not find class: " + e); @@ -94,8 +94,8 @@ final class MagickAccelerator { } private static int getNativeOpIndex(Class pOpClass) { - for (int i = 0; i < sNativeOp.length; i++) { - if (pOpClass == sNativeOp[i]) { + for (int i = 0; i < nativeOp.length; i++) { + if (pOpClass == nativeOp[i]) { return i; } } @@ -112,7 +112,7 @@ final class MagickAccelerator { switch (getNativeOpIndex(pOperation.getClass())) { case RESAMPLE_OP: ResampleOp resample = (ResampleOp) pOperation; - result = resampleMagick(pInput, resample.mWidth, resample.mHeight, resample.mFilterType); + result = resampleMagick(pInput, resample.width, resample.height, resample.filterType); // NOTE: If output parameter is non-null, we have to return that // image, instead of result diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java b/common/common-image/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java index 9f4894f9..5e92dd86 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java @@ -33,7 +33,7 @@ import java.awt.image.*; /** * Monochrome B/W color model. * - * @author Harald Kuhr + * @author Harald Kuhr */ public class MonochromeColorModel extends IndexColorModel { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/PixelizeOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/PixelizeOp.java index c73efd84..f71dc330 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/PixelizeOp.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/PixelizeOp.java @@ -48,37 +48,37 @@ public class PixelizeOp implements BufferedImageOp, RasterOp { // TODO: support more raster types/color models // TODO: This is actually an implementation of Area Averaging, without the scale... Let's extract it... - final private int mPixelSizeX; - final private int mPixelSizeY; + final private int pixelSizeX; + final private int pixelSizeY; - private Rectangle mSourceRegion; + private Rectangle sourceRegion; public PixelizeOp(final int pPixelSize) { this(pPixelSize, pPixelSize); } public PixelizeOp(final int pPixelSizeX, final int pPixelSizeY) { - mPixelSizeX = pPixelSizeX; - mPixelSizeY = pPixelSizeY; + pixelSizeX = pPixelSizeX; + pixelSizeY = pPixelSizeY; } public Rectangle getSourceRegion() { - if (mSourceRegion == null) { + if (sourceRegion == null) { return null; } - return new Rectangle(mSourceRegion); + return new Rectangle(sourceRegion); } public void setSourceRegion(final Rectangle pSourceRegion) { if (pSourceRegion == null) { - mSourceRegion = null; + sourceRegion = null; } else { - if (mSourceRegion == null) { - mSourceRegion = new Rectangle(pSourceRegion); + if (sourceRegion == null) { + sourceRegion = new Rectangle(pSourceRegion); } else { - mSourceRegion.setBounds(pSourceRegion); + sourceRegion.setBounds(pSourceRegion); } } } @@ -107,11 +107,11 @@ public class PixelizeOp implements BufferedImageOp, RasterOp { private WritableRaster filterImpl(Raster src, WritableRaster dest) { //System.out.println("src: " + src); //System.out.println("dest: " + dest); - if (mSourceRegion != null) { - int cx = mSourceRegion.x; - int cy = mSourceRegion.y; - int cw = mSourceRegion.width; - int ch = mSourceRegion.height; + if (sourceRegion != null) { + int cx = sourceRegion.x; + int cy = sourceRegion.y; + int cw = sourceRegion.width; + int ch = sourceRegion.height; boolean same = src == dest; dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null); @@ -122,8 +122,8 @@ public class PixelizeOp implements BufferedImageOp, RasterOp { final int width = src.getWidth(); final int height = src.getHeight(); - int w = (width + mPixelSizeX - 1) / mPixelSizeX; - int h = (height + mPixelSizeY - 1) / mPixelSizeY; + int w = (width + pixelSizeX - 1) / pixelSizeX; + int h = (height + pixelSizeY - 1) / pixelSizeY; final boolean oddX = width % w != 0; final boolean oddY = height % h != 0; @@ -156,23 +156,23 @@ public class PixelizeOp implements BufferedImageOp, RasterOp { for (int y = 0; y < h; y++) { if (!oddY || y + 1 < h) { - scanH = mPixelSizeY; + scanH = pixelSizeY; } else { - scanH = height - (y * mPixelSizeY); + scanH = height - (y * pixelSizeY); } for (int x = 0; x < w; x++) { if (!oddX || x + 1 < w) { - scanW = mPixelSizeX; + scanW = pixelSizeX; } else { - scanW = width - (x * mPixelSizeX); + scanW = width - (x * pixelSizeX); } final int pixelCount = scanW * scanH; final int pixelLength = pixelCount * dataElements; - data = src.getDataElements(x * mPixelSizeX, y * mPixelSizeY, scanW, scanH, data); + data = src.getDataElements(x * pixelSizeX, y * pixelSizeY, scanW, scanH, data); // NOTE: These are not neccessarily ARGB.. double valueA = 0.0; @@ -277,7 +277,7 @@ public class PixelizeOp implements BufferedImageOp, RasterOp { } - dest.setDataElements(x * mPixelSizeX, y * mPixelSizeY, scanW, scanH, data); + dest.setDataElements(x * pixelSizeX, y * pixelSizeY, scanW, scanH, data); } } /*/ diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java index 205f0ca4..90f04cef 100644 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java @@ -60,7 +60,6 @@ import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.*; - /** * Resamples (scales) a {@code BufferedImage} to a new width and height, using * high performance and high quality algorithms. @@ -138,7 +137,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { // MagickAccelerator to work consistently (see magick.FilterType). /** - * Undefined interpolation, filter method will use default filter + * Undefined interpolation, filter method will use default filter. */ public final static int FILTER_UNDEFINED = 0; /** @@ -194,11 +193,11 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { /** * Mitchell interpolation. High quality. */ - public final static int FILTER_MITCHELL = 12;// IM default scale with palette or alpha, or scale up + public final static int FILTER_MITCHELL = 12; // IM default scale with palette or alpha, or scale up /** * Lanczos interpolation. High quality. */ - public final static int FILTER_LANCZOS = 13;// IM default + public final static int FILTER_LANCZOS = 13; // IM default /** * Blackman-Bessel interpolation. High quality. */ @@ -291,10 +290,10 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { // Member variables // Package access, to allow access from MagickAccelerator - int mWidth; - int mHeight; + int width; + int height; - int mFilterType; + int filterType; private static final boolean TRANSFORM_OP_BICUBIC_SUPPORT = SystemUtil.isFieldAvailable(AffineTransformOp.class.getName(), "TYPE_BICUBIC"); /** @@ -302,14 +301,13 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { */ // TODO: Move to abstract class AbstractBufferedImageOp? static class Key extends RenderingHints.Key { - static int sIndex = 10000; - private final String mName; + private final String name; public Key(final String pName) { super(sIndex++); - mName = pName; + name = pName; } public boolean isCompatibleValue(Object pValue) { @@ -317,7 +315,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } public String toString() { - return mName; + return name; } } @@ -326,27 +324,27 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { */ // TODO: Extract abstract Value class, and move to AbstractBufferedImageOp static final class Value { - final private RenderingHints.Key mKey; - final private String mName; - final private int mType; + final private RenderingHints.Key key; + final private String name; + final private int type; public Value(final RenderingHints.Key pKey, final String pName, final int pType) { - mKey = pKey; - mName = pName; + key = pKey; + name = pName; validateFilterType(pType); - mType = pType;// TODO: test for duplicates + type = pType;// TODO: test for duplicates } public boolean isCompatibleKey(Key pKey) { - return pKey == mKey; + return pKey == key; } public int getFilterType() { - return mType; + return type; } public String toString() { - return mName; + return name; } } @@ -354,11 +352,11 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { * Creates a {@code ResampleOp} that will resample input images to the * given width and height, using the default interpolation filter. * - * @param pWidth width of the resampled image - * @param pHeight height of the resampled image + * @param width width of the re-sampled image + * @param height height of the re-sampled image */ - public ResampleOp(int pWidth, int pHeight) { - this(pWidth, pHeight, FILTER_UNDEFINED); + public ResampleOp(int width, int height) { + this(width, height, FILTER_UNDEFINED); } /** @@ -394,38 +392,38 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { * * Other hints have no effect on this filter. * - * @param pWidth width of the resampled image - * @param pHeight height of the resampled image - * @param pHints rendering hints, affecting interpolation algorithm + * @param width width of the re-sampled image + * @param height height of the re-sampled image + * @param hints rendering hints, affecting interpolation algorithm * @see #KEY_RESAMPLE_INTERPOLATION * @see RenderingHints#KEY_INTERPOLATION * @see RenderingHints#KEY_RENDERING * @see RenderingHints#KEY_COLOR_RENDERING */ - public ResampleOp(int pWidth, int pHeight, RenderingHints pHints) { - this(pWidth, pHeight, getFilterType(pHints)); + public ResampleOp(int width, int height, RenderingHints hints) { + this(width, height, getFilterType(hints)); } /** * Creates a {@code ResampleOp} that will resample input images to the * given width and height, using the given interpolation filter. * - * @param pWidth width of the resampled image - * @param pHeight height of the resampled image - * @param pFilterType interpolation filter algorithm + * @param width width of the re-sampled image + * @param height height of the re-sampled image + * @param filterType interpolation filter algorithm * @see filter type constants */ - public ResampleOp(int pWidth, int pHeight, int pFilterType) { - if (pWidth <= 0 || pHeight <= 0) { + public ResampleOp(int width, int height, int filterType) { + if (width <= 0 || height <= 0) { // NOTE: w/h == 0 makes the Magick DLL crash and the JVM dies.. :-P throw new IllegalArgumentException("width and height must be positive"); } - mWidth = pWidth; - mHeight = pHeight; + this.width = width; + this.height = height; - validateFilterType(pFilterType); - mFilterType = pFilterType; + validateFilterType(filterType); + this.filterType = filterType; } private static void validateFilterType(int pFilterType) { @@ -471,25 +469,21 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } return value != null ? ((Value) value).getFilterType() : FILTER_UNDEFINED; } - else - if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(pHints.get(RenderingHints.KEY_INTERPOLATION)) + else if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(pHints.get(RenderingHints.KEY_INTERPOLATION)) || (!pHints.containsKey(RenderingHints.KEY_INTERPOLATION) && (RenderingHints.VALUE_RENDER_SPEED.equals(pHints.get(RenderingHints.KEY_RENDERING)) || RenderingHints.VALUE_COLOR_RENDER_SPEED.equals(pHints.get(RenderingHints.KEY_COLOR_RENDERING))))) { - // Nearest neighbour, or prioritze speed + // Nearest neighbour, or prioritize speed return FILTER_POINT; } - else - if (RenderingHints.VALUE_INTERPOLATION_BILINEAR.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))) { + else if (RenderingHints.VALUE_INTERPOLATION_BILINEAR.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))) { // Triangle equals bi-linear interpolation return FILTER_TRIANGLE; } - else - if (RenderingHints.VALUE_INTERPOLATION_BICUBIC.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))) { + else if (RenderingHints.VALUE_INTERPOLATION_BICUBIC.equals(pHints.get(RenderingHints.KEY_INTERPOLATION))) { return FILTER_QUADRATIC;// No idea if this is correct..? } - else - if (RenderingHints.VALUE_RENDER_QUALITY.equals(pHints.get(RenderingHints.KEY_RENDERING)) + else if (RenderingHints.VALUE_RENDER_QUALITY.equals(pHints.get(RenderingHints.KEY_RENDERING)) || RenderingHints.VALUE_COLOR_RENDER_QUALITY.equals(pHints.get(RenderingHints.KEY_COLOR_RENDERING))) { // Prioritize quality return FILTER_MITCHELL; @@ -500,83 +494,87 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } /** - * Resamples (scales) the image to the size, and using the algorithm + * Re-samples (scales) the image to the size, and using the algorithm * specified in the constructor. * - * @param pInput The {@code BufferedImage} to be filtered - * @param pOutput The {@code BufferedImage} in which to store the resampled + * @param input The {@code BufferedImage} to be filtered + * @param output The {@code BufferedImage} in which to store the resampled * image - * @return The resampled {@code BufferedImage}. - * @throws NullPointerException if {@code pInput} is {@code null} - * @throws IllegalArgumentException if {@code pInput == pOutput}. + * @return The re-sampled {@code BufferedImage}. + * @throws NullPointerException if {@code input} is {@code null} + * @throws IllegalArgumentException if {@code input == output}. * @see #ResampleOp(int,int,int) */ - public final BufferedImage filter(final BufferedImage pInput, final BufferedImage pOutput) { - if (pInput == null) { + public final BufferedImage filter(final BufferedImage input, final BufferedImage output) { + if (input == null) { throw new NullPointerException("Input == null"); } - if (pInput == pOutput) { + if (input == output) { throw new IllegalArgumentException("Output image cannot be the same as the input image"); } InterpolationFilter filter; - // Special case for POINT, TRIANGLE and QUADRATIC filter, as standard - // Java implementation is very fast (possibly H/W accellerated) - switch (mFilterType) { + // Java implementation is very fast (possibly H/W accelerated) + switch (filterType) { case FILTER_POINT: - return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); - case FILTER_TRIANGLE: - return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_BILINEAR); - case FILTER_QUADRATIC: - if (TRANSFORM_OP_BICUBIC_SUPPORT) { - return fastResample(pInput, pOutput, mWidth, mHeight, 3); // AffineTransformOp.TYPE_BICUBIC + if (input.getType() != BufferedImage.TYPE_CUSTOM) { + return fastResample(input, output, width, height, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); } - // Fall through + // Else fall through + case FILTER_TRIANGLE: + if (input.getType() != BufferedImage.TYPE_CUSTOM) { + return fastResample(input, output, width, height, AffineTransformOp.TYPE_BILINEAR); + } + // Else fall through + case FILTER_QUADRATIC: + if (input.getType() != BufferedImage.TYPE_CUSTOM && TRANSFORM_OP_BICUBIC_SUPPORT) { + return fastResample(input, output, width, height, 3); // AffineTransformOp.TYPE_BICUBIC + } + // Else fall through default: - filter = createFilter(mFilterType); + filter = createFilter(filterType); // NOTE: Workaround for filter throwing exceptions when input or output is less than support... - if (Math.min(pInput.getWidth(), pInput.getHeight()) <= filter.support() || Math.min(mWidth, mHeight) <= filter.support()) { - return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_BILINEAR); + if (Math.min(input.getWidth(), input.getHeight()) <= filter.support() || Math.min(width, height) <= filter.support()) { + return fastResample(input, output, width, height, AffineTransformOp.TYPE_BILINEAR); } // Fall through } - // Try to use native ImageMagick code - BufferedImage result = MagickAccelerator.filter(this, pInput, pOutput); + BufferedImage result = MagickAccelerator.filter(this, input, output); if (result != null) { return result; } // Otherwise, continue in pure Java mode - // TODO: What if pOutput != null and wrong size? Create new? Render on only a part? Document? + // TODO: What if output != null and wrong size? Create new? Render on only a part? Document? // If filter type != POINT or BOX an input has IndexColorModel, convert - // to true color, with alpha reflecting that of the original colormodel. - BufferedImage input; + // to true color, with alpha reflecting that of the original color model. + BufferedImage temp; ColorModel cm; - if (mFilterType != FILTER_BOX && (cm = pInput.getColorModel()) instanceof IndexColorModel) { - // TODO: OPTIMIZE: If colormodel has only b/w or gray, we could skip color info - input = ImageUtil.toBuffered(pInput, cm.hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); + if (filterType != FILTER_POINT && filterType != FILTER_BOX && (cm = input.getColorModel()) instanceof IndexColorModel) { + // TODO: OPTIMIZE: If color model has only b/w or gray, we could skip color info + temp = ImageUtil.toBuffered(input, cm.hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); } else { - input = pInput; + temp = input; } // Create or convert output to a suitable image // TODO: OPTIMIZE: Don't really need to convert all types to same as input - result = pOutput != null ? /*pOutput*/ ImageUtil.toBuffered(pOutput, input.getType()) : createCompatibleDestImage(input, null); + result = output != null && temp.getType() != BufferedImage.TYPE_CUSTOM ? /*output*/ ImageUtil.toBuffered(output, temp.getType()) : createCompatibleDestImage(temp, null); - resample(input, result, filter); + resample(temp, result, filter); - // If pOutput != null and needed to be converted, draw it back - if (pOutput != null && pOutput != result) { - //pOutput.setData(output.getRaster()); - ImageUtil.drawOnto(pOutput, result); - result = pOutput; + // If output != null and needed to be converted, draw it back + if (output != null && output != result) { + //output.setData(output.getRaster()); + ImageUtil.drawOnto(output, result); + result = output; } return result; @@ -672,8 +670,8 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } */ - private static BufferedImage fastResample(final BufferedImage pInput, final BufferedImage pOutput, final int pWidth, final int pHeight, final int pType) { - BufferedImage temp = pInput; + private static BufferedImage fastResample(final BufferedImage input, final BufferedImage output, final int width, final int height, final int type) { + BufferedImage temp = input; double xScale; double yScale; @@ -681,20 +679,20 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { AffineTransform transform; AffineTransformOp scale; - if (pType > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { + if (type > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { // Initially scale so all remaining operations will halve the image - if (pWidth < pInput.getWidth() || pHeight < pInput.getHeight()) { - int w = pWidth; - int h = pHeight; - while (w < pInput.getWidth() / 2) { + if (width < input.getWidth() || height < input.getHeight()) { + int w = width; + int h = height; + while (w < input.getWidth() / 2) { w *= 2; } - while (h < pInput.getHeight() / 2) { + while (h < input.getHeight() / 2) { h *= 2; } - xScale = w / (double) pInput.getWidth(); - yScale = h / (double) pInput.getHeight(); + xScale = w / (double) input.getWidth(); + yScale = h / (double) input.getHeight(); //System.out.println("First scale by x=" + xScale + ", y=" + yScale); @@ -704,12 +702,12 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } } - scale = null;// NOTE: This resets! + scale = null; // NOTE: This resets! - xScale = pWidth / (double) temp.getWidth(); - yScale = pHeight / (double) temp.getHeight(); + xScale = width / (double) temp.getWidth(); + yScale = height / (double) temp.getHeight(); - if (pType > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { + if (type > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { // TODO: Test skipping first scale (above), and instead scale once // more here, and a little less than .5 each time... // That would probably make the scaling smoother... @@ -740,17 +738,15 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } temp = scale.filter(temp, null); - } } //System.out.println("Rest to scale by x=" + xScale + ", y=" + yScale); transform = AffineTransform.getScaleInstance(xScale, yScale); - scale = new AffineTransformOp(transform, pType); - - return scale.filter(temp, pOutput); + scale = new AffineTransformOp(transform, type); + return scale.filter(temp, output); } /** @@ -760,7 +756,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { * @see filter type constants */ public int getFilterType() { - return mFilterType; + return filterType; } private static InterpolationFilter createFilter(int pFilterType) { @@ -770,7 +766,8 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } switch (pFilterType) { - //case FILTER_POINT: // Should never happen + case FILTER_POINT: + return new PointFilter(); case FILTER_BOX: return new BoxFilter(); case FILTER_TRIANGLE: @@ -815,14 +812,13 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { // If indexcolormodel, we probably don't want to use that... // NOTE: Either BOTH or NONE of the images must have ALPHA - return new BufferedImage(cm, ImageUtil.createCompatibleWritableRaster(pInput, cm, mWidth, mHeight), + return new BufferedImage(cm, ImageUtil.createCompatibleWritableRaster(pInput, cm, width, height), cm.isAlphaPremultiplied(), null); - } public RenderingHints getRenderingHints() { Object value; - switch (mFilterType) { + switch (filterType) { case FILTER_UNDEFINED: return null; case FILTER_POINT: @@ -871,14 +867,14 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { value = VALUE_INTERPOLATION_BLACKMAN_SINC; break; default: - throw new IllegalStateException("Unknown filter type: " + mFilterType); + throw new IllegalStateException("Unknown filter type: " + filterType); } return new RenderingHints(KEY_RESAMPLE_INTERPOLATION, value); } public Rectangle2D getBounds2D(BufferedImage src) { - return new Rectangle(mWidth, mHeight); + return new Rectangle(width, height); } public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { @@ -1439,10 +1435,8 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { resample() Resizes bitmaps while resampling them. - Returns -1 if error, 0 if success. */ private BufferedImage resample(BufferedImage pSource, BufferedImage pDest, InterpolationFilter pFilter) { - // TODO: Don't work... Could fix by creating a temporary image in filter method final int dstWidth = pDest.getWidth(); final int dstHeight = pDest.getHeight(); @@ -1451,7 +1445,8 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { /* create intermediate column to hold horizontal dst column zoom */ final ColorModel cm = pSource.getColorModel(); - final WritableRaster work = cm.createCompatibleWritableRaster(1, srcHeight); +// final WritableRaster work = cm.createCompatibleWritableRaster(1, srcHeight); + final WritableRaster work = ImageUtil.createCompatibleWritableRaster(pSource, cm, 1, srcHeight); double xscale = (double) dstWidth / (double) srcWidth; double yscale = (double) dstHeight / (double) srcHeight; @@ -1566,7 +1561,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { final WritableRaster out = pDest.getRaster(); // TODO: This is not optimal for non-byte-packed rasters... - // (What? Maybe I implemented the fix, but forgot to remove the qTODO?) + // (What? Maybe I implemented the fix, but forgot to remove the TODO?) final int numChannels = raster.getNumBands(); final int[] channelMax = new int[numChannels]; for (int k = 0; k < numChannels; k++) { @@ -1575,7 +1570,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { for (int xx = 0; xx < dstWidth; xx++) { ContributorList contribX = calcXContrib(xscale, fwidth, srcWidth, pFilter, xx); - /* Apply horz filter to make dst column in tmp. */ + /* Apply horiz filter to make dst column in tmp. */ for (int k = 0; k < srcHeight; k++) { for (int channel = 0; channel < numChannels; channel++) { diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java b/common/common-image/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java index 26aecd6a..839a5b5d 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java @@ -42,8 +42,8 @@ import java.awt.image.ReplicateScaleFilter; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java#1 $ */ public class SubsamplingFilter extends ReplicateScaleFilter { - private int mXSub; - private int mYSub; + private int xSub; + private int ySub; /** * Creates a {@code SubsamplingFilter}. @@ -62,16 +62,16 @@ public class SubsamplingFilter extends ReplicateScaleFilter { throw new IllegalArgumentException("Subsampling factors must be positive."); } - mXSub = pXSub; - mYSub = pYSub; + xSub = pXSub; + ySub = pYSub; } /** {@code ImageFilter} implementation, do not invoke. */ public void setDimensions(int pWidth, int pHeight) { - destWidth = (pWidth + mXSub - 1) / mXSub; - destHeight = (pHeight + mYSub - 1) / mYSub; + destWidth = (pWidth + xSub - 1) / xSub; + destHeight = (pHeight + ySub - 1) / ySub; - //System.out.println("Subsampling: " + mXSub + "," + mYSub + "-> " + destWidth + ", " + destHeight); + //System.out.println("Subsampling: " + xSub + "," + ySub + "-> " + destWidth + ", " + destHeight); super.setDimensions(pWidth, pHeight); } } diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/package-info.java b/common/common-image/src/main/java/com/twelvemonkeys/image/package-info.java index e344c3a8..9ee76555 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/package-info.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/package-info.java @@ -4,6 +4,6 @@ * See the class {@link com.twelvemonkeys.image.ImageUtil}. * * @version 1.0 - * @author Harald Kuhr + * @author Harald Kuhr */ package com.twelvemonkeys.image; \ No newline at end of file diff --git a/common/common-image/src/test/java/com/twelvemonkeys/image/BufferedImageFactoryTestCase.java b/common/common-image/src/test/java/com/twelvemonkeys/image/BufferedImageFactoryTestCase.java new file mode 100644 index 00000000..fa6cb48a --- /dev/null +++ b/common/common-image/src/test/java/com/twelvemonkeys/image/BufferedImageFactoryTestCase.java @@ -0,0 +1,366 @@ +package com.twelvemonkeys.image; + +import org.junit.Ignore; +import org.junit.Test; + +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ImageProducer; +import java.awt.image.IndexColorModel; +import java.net.URL; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +/** + * BufferedImageFactoryTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: BufferedImageFactoryTestCase.java,v 1.0 May 7, 2010 12:40:08 PM haraldk Exp$ + */ +public class BufferedImageFactoryTestCase { + @Test(expected = IllegalArgumentException.class) + public void testCreateNullImage() { + new BufferedImageFactory((Image) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNullProducer() { + new BufferedImageFactory((ImageProducer) null); + } + + // NPE in Toolkit, ok + @Test(expected = RuntimeException.class) + public void testGetBufferedImageErrorSourceByteArray() { + Image source = Toolkit.getDefaultToolkit().createImage((byte[]) null); + + new BufferedImageFactory(source); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBufferedImageErrorSourceImageProducer() { + Image source = Toolkit.getDefaultToolkit().createImage((ImageProducer) null); + + new BufferedImageFactory(source); + } + + // TODO: This is a quite serious bug, however, the bug is in the Toolkit, allowing such images in the first place... + // In any case, there's not much we can do, except until someone is bored and kills the app/thread... :-P + @Ignore("Bug in Toolkit") + @Test(timeout = 1000, expected = ImageConversionException.class) + public void testGetBufferedImageErrorSourceString() { + Image source = Toolkit.getDefaultToolkit().createImage((String) null); + + BufferedImageFactory factory = new BufferedImageFactory(source); + factory.getBufferedImage(); + } + + // This is a little random, and it would be nicer if we could throw an IllegalArgumentException on create. + // Unfortunately, the API doesn't allow this... + @Test(timeout = 1000, expected = ImageConversionException.class) + public void testGetBufferedImageErrorSourceURL() { + Image source = Toolkit.getDefaultToolkit().createImage(getClass().getResource("/META-INF/MANIFEST.MF")); + + BufferedImageFactory factory = new BufferedImageFactory(source); + factory.getBufferedImage(); + } + + @Test + public void testGetBufferedImageJPEG() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + BufferedImage image = factory.getBufferedImage(); + + assertEquals(187, image.getWidth()); + assertEquals(283, image.getHeight()); + } + + @Test + public void testGetColorModelJPEG() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + ColorModel colorModel = factory.getColorModel(); + + assertNotNull(colorModel); + assertEquals(3, colorModel.getNumColorComponents()); // getNumComponents may include alpha, we don't care + assertEquals(ColorSpace.getInstance(ColorSpace.CS_sRGB), colorModel.getColorSpace()); + + for (int i = 0; i < colorModel.getNumComponents(); i++) { + assertEquals(8, colorModel.getComponentSize(i)); + } + } + + @Test + public void testGetBufferedImageGIF() { + URL resource = getClass().getResource("/tux.gif"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + BufferedImage image = factory.getBufferedImage(); + + assertEquals(250, image.getWidth()); + assertEquals(250, image.getHeight()); + + assertEquals(Transparency.BITMASK, image.getTransparency()); + + // All corners of image should be fully transparent + assertEquals(0, image.getRGB(0, 0) >>> 24); + assertEquals(0, image.getRGB(249, 0) >>> 24); + assertEquals(0, image.getRGB(0, 249) >>> 24); + assertEquals(0, image.getRGB(249, 249) >>> 24); + } + + @Test + public void testGetColorModelGIF() { + URL resource = getClass().getResource("/tux.gif"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + ColorModel colorModel = factory.getColorModel(); + + assertNotNull(colorModel); + + assertEquals(3, colorModel.getNumColorComponents()); + assertEquals(ColorSpace.getInstance(ColorSpace.CS_sRGB), colorModel.getColorSpace()); + assertTrue(colorModel instanceof IndexColorModel); + + assertTrue(colorModel.hasAlpha()); + assertEquals(4, colorModel.getNumComponents()); + assertTrue(((IndexColorModel) colorModel).getTransparentPixel() >= 0); + assertEquals(Transparency.BITMASK, colorModel.getTransparency()); + + for (int i = 0; i < colorModel.getNumComponents(); i++) { + assertEquals(8, colorModel.getComponentSize(i)); + } + } + + @Test + public void testGetBufferedImageSubsampled() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + BufferedImage original = factory.getBufferedImage(); + + factory.setSourceSubsampling(2, 2); + BufferedImage image = factory.getBufferedImage(); // Accidentally also tests reuse... + + // Values rounded up + assertEquals(94, image.getWidth()); + assertEquals(142, image.getHeight()); + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertEquals("RGB[" + x + ", " + y + "]", original.getRGB(x * 2, y * 2), image.getRGB(x, y)); + } + } + } + + @Test + public void testGetBufferedImageSourceRegion() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + BufferedImage original = factory.getBufferedImage(); + + factory.setSourceRegion(new Rectangle(40, 40, 40, 40)); + BufferedImage image = factory.getBufferedImage(); // Accidentally also tests reuse... + + assertEquals(40, image.getWidth()); + assertEquals(40, image.getHeight()); + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertEquals("RGB[" + x + ", " + y + "]", original.getRGB(40 + x, 40 + y), image.getRGB(x, y)); + } + } + } + + @Test + public void testGetBufferedImageSubsampledSourceRegion() throws Exception{ + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + BufferedImage original = factory.getBufferedImage(); + + factory.setSourceRegion(new Rectangle(40, 40, 40, 40)); + factory.setSourceSubsampling(2, 2); + BufferedImage image = factory.getBufferedImage(); // Accidentally also tests reuse... + + assertEquals(20, image.getWidth()); + assertEquals(20, image.getHeight()); + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertEquals("RGB[" + x + ", " + y + "]", original.getRGB(40 + x * 2, 40 + y * 2), image.getRGB(x, y)); + } + } + } + + @Test + public void testAbort() throws Exception { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + final BufferedImageFactory factory = new BufferedImageFactory(source); + + // Listener should abort ASAP + factory.addProgressListener(new BufferedImageFactory.ProgressListener() { + public void progress(BufferedImageFactory pFactory, float pPercentage) { + if (pPercentage > 5) { + pFactory.abort(); + } + } + }); + + BufferedImage image = factory.getBufferedImage(); + + assertEquals(187, image.getWidth()); + assertEquals(283, image.getHeight()); + + // Upper right should be loaded + assertEquals((image.getRGB(186, 0) & 0xFF0000) >> 16 , 0x68, 10); + assertEquals((image.getRGB(186, 0) & 0xFF00) >> 8, 0x91, 10); + assertEquals(image.getRGB(186, 0) & 0xFF, 0xE0, 10); + + // Lower right should be blank + assertEquals(image.getRGB(186, 282) & 0xFFFFFF, 0); + } + + @Test + public void testListener() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + + VerifyingListener listener = new VerifyingListener(factory); + factory.addProgressListener(listener); + factory.getBufferedImage(); + + listener.verify(100f); + } + + @Test + public void testRemoveListener() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + + VerifyingListener listener = new VerifyingListener(factory); + factory.addProgressListener(listener); + factory.removeProgressListener(listener); + factory.getBufferedImage(); + + listener.verify(0); + } + + @Test + public void testRemoveNullListener() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + + VerifyingListener listener = new VerifyingListener(factory); + factory.addProgressListener(listener); + factory.removeProgressListener(null); + factory.getBufferedImage(); + + listener.verify(100); + } + + @Test + public void testRemoveNotAdddedListener() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + + VerifyingListener listener = new VerifyingListener(factory); + factory.addProgressListener(listener); + factory.removeProgressListener(new BufferedImageFactory.ProgressListener() { + public void progress(BufferedImageFactory pFactory, float pPercentage) { + } + }); + factory.getBufferedImage(); + + listener.verify(100); + } + + @Test + public void testRemoveAllListeners() { + URL resource = getClass().getResource("/sunflower.jpg"); + assertNotNull(resource); + Image source = Toolkit.getDefaultToolkit().createImage(resource); + assertNotNull(source); + + BufferedImageFactory factory = new BufferedImageFactory(source); + + VerifyingListener listener = new VerifyingListener(factory); + VerifyingListener listener2 = new VerifyingListener(factory); + factory.addProgressListener(listener); + factory.addProgressListener(listener); + factory.addProgressListener(listener2); + factory.removeAllProgressListeners(); + factory.getBufferedImage(); + + listener.verify(0); + listener2.verify(0); + } + + private static class VerifyingListener implements BufferedImageFactory.ProgressListener { + private final BufferedImageFactory factory; + private float progress; + + public VerifyingListener(BufferedImageFactory factory) { + this.factory = factory; + } + + public void progress(BufferedImageFactory pFactory, float pPercentage) { + assertEquals(factory, pFactory); + assertTrue(pPercentage >= progress && pPercentage <= 100f); + + progress = pPercentage; + } + + + public void verify(final float expectedProgress) { + assertEquals(expectedProgress, progress, .1f); // Sanity test that the listener was invoked + } + } +} diff --git a/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java b/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java index 57820f2b..fbd9a232 100755 --- a/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java +++ b/common/common-image/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java @@ -24,35 +24,35 @@ import java.lang.reflect.InvocationTargetException; public class ImageUtilTestCase extends TestCase { private final static String IMAGE_NAME = "/sunflower.jpg"; - private BufferedImage mOriginal; - private BufferedImage mImage; - private Image mScaled; + private BufferedImage original; + private BufferedImage image; + private Image scaled; public ImageUtilTestCase() throws Exception { - mImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); - mScaled = mImage.getScaledInstance(5, 5, Image.SCALE_FAST); + image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); + scaled = image.getScaledInstance(5, 5, Image.SCALE_FAST); // Read image from class path InputStream is = getClass().getResourceAsStream(IMAGE_NAME); - mOriginal = ImageIO.read(is); + original = ImageIO.read(is); - assertNotNull(mOriginal); + assertNotNull(original); } /* public void setUp() throws Exception { - mImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); - mScaled = mImage.getScaledInstance(5, 5, Image.SCALE_FAST); + image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); + scaled = image.getScaledInstance(5, 5, Image.SCALE_FAST); // Read image from class path InputStream is = ClassLoader.getSystemResourceAsStream(IMAGE_NAME); - mOriginal = ImageIO.read(is); + original = ImageIO.read(is); - assertNotNull(mOriginal); + assertNotNull(original); } protected void tearDown() throws Exception { - mOriginal = null; + original = null; } */ @@ -94,20 +94,20 @@ public class ImageUtilTestCase extends TestCase { // Should not be a buffered image assertFalse( "FOR SOME IMPLEMENTATIONS THIS MIGHT FAIL!\nIn that case, testToBufferedImage() will fail too.", - mScaled instanceof BufferedImage + scaled instanceof BufferedImage ); } public void testToBufferedImage() { - BufferedImage sameAsImage = ImageUtil.toBuffered((RenderedImage) mImage); - BufferedImage bufferedScaled = ImageUtil.toBuffered(mScaled); + BufferedImage sameAsImage = ImageUtil.toBuffered((RenderedImage) image); + BufferedImage bufferedScaled = ImageUtil.toBuffered(scaled); // Should be no need to convert - assertSame(mImage, sameAsImage); + assertSame(image, sameAsImage); // Should have same dimensions - assertEquals(mScaled.getWidth(null), bufferedScaled.getWidth()); - assertEquals(mScaled.getHeight(null), bufferedScaled.getHeight()); + assertEquals(scaled.getWidth(null), bufferedScaled.getWidth()); + assertEquals(scaled.getHeight(null), bufferedScaled.getHeight()); // Hmmm... assertTrue(new Integer(42).equals(bufferedScaled.getProperty("lucky-number")) @@ -116,28 +116,28 @@ public class ImageUtilTestCase extends TestCase { } public void testToBufferedImageType() { - // Assumes mImage is TYPE_INT_ARGB - BufferedImage converted = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_BYTE_INDEXED); - BufferedImage convertedToo = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_BYTE_BINARY); + // Assumes image is TYPE_INT_ARGB + BufferedImage converted = ImageUtil.toBuffered(image, BufferedImage.TYPE_BYTE_INDEXED); + BufferedImage convertedToo = ImageUtil.toBuffered(image, BufferedImage.TYPE_BYTE_BINARY); // Should not be the same - assertNotSame(mImage, converted); - assertNotSame(mImage, convertedToo); + assertNotSame(image, converted); + assertNotSame(image, convertedToo); // Correct type assertTrue(converted.getType() == BufferedImage.TYPE_BYTE_INDEXED); assertTrue(convertedToo.getType() == BufferedImage.TYPE_BYTE_BINARY); // Should have same dimensions - assertEquals(mImage.getWidth(), converted.getWidth()); - assertEquals(mImage.getHeight(), converted.getHeight()); + assertEquals(image.getWidth(), converted.getWidth()); + assertEquals(image.getHeight(), converted.getHeight()); - assertEquals(mImage.getWidth(), convertedToo.getWidth()); - assertEquals(mImage.getHeight(), convertedToo.getHeight()); + assertEquals(image.getWidth(), convertedToo.getWidth()); + assertEquals(image.getHeight(), convertedToo.getHeight()); } public void testBrightness() { - final BufferedImage original = mOriginal; + final BufferedImage original = this.original; assertNotNull(original); final BufferedImage notBrightened = ImageUtil.toBuffered(ImageUtil.brightness(original, 0f)); @@ -217,7 +217,7 @@ public class ImageUtilTestCase extends TestCase { public void testContrast() { - final BufferedImage original = mOriginal; + final BufferedImage original = this.original; assertNotNull(original); @@ -370,7 +370,7 @@ public class ImageUtilTestCase extends TestCase { } public void testSharpen() { - final BufferedImage original = mOriginal; + final BufferedImage original = this.original; assertNotNull(original); @@ -495,7 +495,7 @@ public class ImageUtilTestCase extends TestCase { } public void testBlur() { - final BufferedImage original = mOriginal; + final BufferedImage original = this.original; assertNotNull(original); @@ -563,7 +563,7 @@ public class ImageUtilTestCase extends TestCase { } public void testIndexImage() { - BufferedImage sunflower = mOriginal; + BufferedImage sunflower = original; assertNotNull(sunflower); diff --git a/common/common-image/src/test/resources/tux.gif b/common/common-image/src/test/resources/tux.gif new file mode 100644 index 00000000..378cd229 Binary files /dev/null and b/common/common-image/src/test/resources/tux.gif differ diff --git a/common/common-image/todo.txt b/common/common-image/todo.txt new file mode 100644 index 00000000..c8ed52d1 --- /dev/null +++ b/common/common-image/todo.txt @@ -0,0 +1,6 @@ +TODO: + Remove compile-time dependency on JMagick: + - Extract interface for MagickAccelerator + - Move implementation to separate module + - Instantiate impl via reflection +DONE: \ No newline at end of file diff --git a/common/common-io/pom.xml b/common/common-io/pom.xml index 1993037a..bef72a90 100644 --- a/common/common-io/pom.xml +++ b/common/common-io/pom.xml @@ -2,30 +2,31 @@ - 4.0.0 - - com.twelvemonkeys.common - common - 3.0-SNAPSHOT - - common-io - jar - TwelveMonkeys :: Common :: IO - - The TwelveMonkeys IO support - + 4.0.0 + + com.twelvemonkeys.common + common + 3.0-SNAPSHOT + + common-io + jar + TwelveMonkeys :: Common :: IO + + The TwelveMonkeys Common IO support + - - - ${project.groupId} - common-lang - - - ${project.groupId} - common-lang - tests - test - - + + + ${project.groupId} + common-lang + + + + ${project.groupId} + common-lang + tests + test + + diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java index ba23b5ea..69f07bba 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java @@ -14,28 +14,28 @@ import java.io.InputStream; */ abstract class AbstractCachedSeekableStream extends SeekableInputStream { /** The backing stream */ - protected final InputStream mStream; + protected final InputStream stream; - /** The stream positon in the backing stream (mStream) */ - protected long mStreamPosition; + /** The stream positon in the backing stream (stream) */ + protected long streamPosition; - private StreamCache mCache; + private StreamCache cache; protected AbstractCachedSeekableStream(final InputStream pStream, final StreamCache pCache) { Validate.notNull(pStream, "stream"); Validate.notNull(pCache, "cache"); - mStream = pStream; - mCache = pCache; + stream = pStream; + cache = pCache; } protected final StreamCache getCache() { - return mCache; + return cache; } @Override public int available() throws IOException { - long avail = mStreamPosition - mPosition + mStream.available(); + long avail = streamPosition - position + stream.available(); return avail > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) avail; } @@ -43,26 +43,26 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { checkOpen(); int read; - if (mPosition == mStreamPosition) { + if (position == streamPosition) { // TODO: Read more bytes here! // TODO: Use buffer if not in-memory cache? (See FileCacheSeekableStream overrides). // Read a byte from the stream - read = mStream.read(); + read = stream.read(); if (read >= 0) { - mStreamPosition++; - mCache.write(read); + streamPosition++; + cache.write(read); } } else { // ..or read byte from the cache syncPosition(); - read = mCache.read(); + read = cache.read(); } // TODO: This field is not REALLY considered accessible.. :-P if (read != -1) { - mPosition++; + position++; } return read; @@ -73,32 +73,32 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { checkOpen(); int length; - if (mPosition == mStreamPosition) { + if (position == streamPosition) { // Read bytes from the stream - length = mStream.read(pBytes, pOffset, pLength); + length = stream.read(pBytes, pOffset, pLength); if (length > 0) { - mStreamPosition += length; - mCache.write(pBytes, pOffset, length); + streamPosition += length; + cache.write(pBytes, pOffset, length); } } else { // ...or read bytes from the cache syncPosition(); - length = mCache.read(pBytes, pOffset, pLength); + length = cache.read(pBytes, pOffset, pLength); } // TODO: This field is not REALLY considered accessible.. :-P if (length > 0) { - mPosition += length; + position += length; } return length; } protected final void syncPosition() throws IOException { - if (mCache.getPosition() != mPosition) { - mCache.seek(mPosition); // Assure EOF is correctly thrown + if (cache.getPosition() != position) { + cache.seek(position); // Assure EOF is correctly thrown } } @@ -111,14 +111,14 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { public abstract boolean isCachedFile(); protected void seekImpl(long pPosition) throws IOException { - if (mStreamPosition < pPosition) { + if (streamPosition < pPosition) { // Make sure we append at end of cache - if (mCache.getPosition() != mStreamPosition) { - mCache.seek(mStreamPosition); + if (cache.getPosition() != streamPosition) { + cache.seek(streamPosition); } // Read diff from stream into cache - long left = pPosition - mStreamPosition; + long left = pPosition - streamPosition; // TODO: Use fixed buffer, instead of allocating here... int bufferLen = left > 1024 ? 1024 : (int) left; @@ -126,11 +126,11 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { while (left > 0) { int length = buffer.length < left ? buffer.length : (int) left; - int read = mStream.read(buffer, 0, length); + int read = stream.read(buffer, 0, length); if (read > 0) { - mCache.write(buffer, 0, read); - mStreamPosition += read; + cache.write(buffer, 0, read); + streamPosition += read; left -= read; } else if (read < 0) { @@ -138,27 +138,27 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { } } } - else if (mStreamPosition >= pPosition) { + else /*if (streamPosition >= pPosition) */ { // Seek backwards into the cache - mCache.seek(pPosition); + cache.seek(pPosition); } // System.out.println("pPosition: " + pPosition); -// System.out.println("mPosition: " + mPosition); -// System.out.println("mStreamPosition: " + mStreamPosition); -// System.out.println("mCache.mPosition: " + mCache.getPosition()); +// System.out.println("position: " + position); +// System.out.println("streamPosition: " + streamPosition); +// System.out.println("cache.position: " + cache.getPosition()); - // NOTE: If mPosition == pPosition then we're good to go + // NOTE: If position == pPosition then we're good to go } protected void flushBeforeImpl(long pPosition) { - mCache.flush(pPosition); + cache.flush(pPosition); } protected void closeImpl() throws IOException { - mCache.flush(mPosition); - mCache = null; - mStream.close(); + cache.flush(position); + cache = null; + stream.close(); } /** diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/CompoundReader.java b/common/common-io/src/main/java/com/twelvemonkeys/io/CompoundReader.java index 35114414..e41562f1 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/CompoundReader.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/CompoundReader.java @@ -46,15 +46,16 @@ import java.util.List; */ public class CompoundReader extends Reader { - private Reader mCurrent; - private List mReaders; - protected final Object mLock; + private Reader current; + private List readers; - protected final boolean mMarkSupported; + protected final Object finalLock; - private int mCurrentReader; - private int mMarkedReader; - private int mMark; + protected final boolean markSupported; + + private int currentReader; + private int markedReader; + private int mark; private int mNext; /** @@ -71,10 +72,10 @@ public class CompoundReader extends Reader { public CompoundReader(final Iterator pReaders) { super(Validate.notNull(pReaders, "readers")); - mLock = pReaders; // NOTE: It's ok to sync on pReaders, as the + finalLock = pReaders; // NOTE: It's ok to sync on pReaders, as the // reference can't change, only it's elements - mReaders = new ArrayList(); + readers = new ArrayList(); boolean markSupported = true; while (pReaders.hasNext()) { @@ -82,25 +83,25 @@ public class CompoundReader extends Reader { if (reader == null) { throw new NullPointerException("readers cannot contain null-elements"); } - mReaders.add(reader); + readers.add(reader); markSupported = markSupported && reader.markSupported(); } - mMarkSupported = markSupported; + this.markSupported = markSupported; - mCurrent = nextReader(); + current = nextReader(); } protected final Reader nextReader() { - if (mCurrentReader >= mReaders.size()) { - mCurrent = new EmptyReader(); + if (currentReader >= readers.size()) { + current = new EmptyReader(); } else { - mCurrent = mReaders.get(mCurrentReader++); + current = readers.get(currentReader++); } // NOTE: Reset mNext for every reader, and record marked reader in mark/reset methods! mNext = 0; - return mCurrent; + return current; } /** @@ -109,17 +110,18 @@ public class CompoundReader extends Reader { * @throws IOException if the stream is closed */ protected final void ensureOpen() throws IOException { - if (mReaders == null) { + if (readers == null) { throw new IOException("Stream closed"); } } public void close() throws IOException { // Close all readers - for (Reader reader : mReaders) { + for (Reader reader : readers) { reader.close(); } - mReaders = null; + + readers = null; } @Override @@ -130,46 +132,46 @@ public class CompoundReader extends Reader { // TODO: It would be nice if we could actually close some readers now - synchronized (mLock) { + synchronized (finalLock) { ensureOpen(); - mMark = mNext; - mMarkedReader = mCurrentReader; + mark = mNext; + markedReader = currentReader; - mCurrent.mark(pReadLimit); + current.mark(pReadLimit); } } @Override public void reset() throws IOException { - synchronized (mLock) { + synchronized (finalLock) { ensureOpen(); - if (mCurrentReader != mMarkedReader) { + if (currentReader != markedReader) { // Reset any reader before this - for (int i = mCurrentReader; i >= mMarkedReader; i--) { - mReaders.get(i).reset(); + for (int i = currentReader; i >= markedReader; i--) { + readers.get(i).reset(); } - mCurrentReader = mMarkedReader - 1; + currentReader = markedReader - 1; nextReader(); } - mCurrent.reset(); + current.reset(); - mNext = mMark; + mNext = mark; } } @Override public boolean markSupported() { - return mMarkSupported; + return markSupported; } @Override public int read() throws IOException { - synchronized (mLock) { - int read = mCurrent.read(); + synchronized (finalLock) { + int read = current.read(); - if (read < 0 && mCurrentReader < mReaders.size()) { + if (read < 0 && currentReader < readers.size()) { nextReader(); return read(); // In case of 0-length readers } @@ -181,10 +183,10 @@ public class CompoundReader extends Reader { } public int read(char pBuffer[], int pOffset, int pLength) throws IOException { - synchronized (mLock) { - int read = mCurrent.read(pBuffer, pOffset, pLength); + synchronized (finalLock) { + int read = current.read(pBuffer, pOffset, pLength); - if (read < 0 && mCurrentReader < mReaders.size()) { + if (read < 0 && currentReader < readers.size()) { nextReader(); return read(pBuffer, pOffset, pLength); // In case of 0-length readers } @@ -197,15 +199,15 @@ public class CompoundReader extends Reader { @Override public boolean ready() throws IOException { - return mCurrent.ready(); + return current.ready(); } @Override public long skip(long pChars) throws IOException { - synchronized (mLock) { - long skipped = mCurrent.skip(pChars); + synchronized (finalLock) { + long skipped = current.skip(pChars); - if (skipped == 0 && mCurrentReader < mReaders.size()) { + if (skipped == 0 && currentReader < readers.size()) { nextReader(); return skip(pChars); // In case of 0-length readers } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java index da7a2d1a..bf6c5b5f 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java @@ -39,11 +39,12 @@ import java.io.ByteArrayInputStream; *

* * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java#2 $ + * @version $Id: FastByteArrayOutputStream.java#2 $ */ +// TODO: Performance test of a stream impl that uses list of fixed size blocks, rather than contiguous block public final class FastByteArrayOutputStream extends ByteArrayOutputStream { - /** Max grow size (unless if writing more than this ammount of bytes) */ - protected int mMaxGrowSize = 1024 * 1024; // 1 MB + /** Max grow size (unless if writing more than this amount of bytes) */ + protected int maxGrowSize = 1024 * 1024; // 1 MB /** * Creates a {@code ByteArrayOutputStream} with the given initial buffer @@ -69,7 +70,7 @@ public final class FastByteArrayOutputStream extends ByteArrayOutputStream { } @Override - public synchronized void write(byte pBytes[], int pOffset, int pLength) { + public void write(byte pBytes[], int pOffset, int pLength) { if ((pOffset < 0) || (pOffset > pBytes.length) || (pLength < 0) || ((pOffset + pLength) > pBytes.length) || ((pOffset + pLength) < 0)) { throw new IndexOutOfBoundsException(); @@ -77,23 +78,24 @@ public final class FastByteArrayOutputStream extends ByteArrayOutputStream { else if (pLength == 0) { return; } - int newcount = count + pLength; - growIfNeeded(newcount); + + int newCount = count + pLength; + growIfNeeded(newCount); System.arraycopy(pBytes, pOffset, buf, count, pLength); - count = newcount; + count = newCount; } @Override - public synchronized void write(int pByte) { - int newcount = count + 1; - growIfNeeded(newcount); + public void write(int pByte) { + int newCount = count + 1; + growIfNeeded(newCount); buf[count] = (byte) pByte; - count = newcount; + count = newCount; } - private void growIfNeeded(int pNewcount) { - if (pNewcount > buf.length) { - int newSize = Math.max(Math.min(buf.length << 1, buf.length + mMaxGrowSize), pNewcount); + private void growIfNeeded(int pNewCount) { + if (pNewCount > buf.length) { + int newSize = Math.max(Math.min(buf.length << 1, buf.length + maxGrowSize), pNewCount); byte newBuf[] = new byte[newSize]; System.arraycopy(buf, 0, newBuf, 0, count); buf = newBuf; @@ -109,9 +111,10 @@ public final class FastByteArrayOutputStream extends ByteArrayOutputStream { // Non-synchronized version of toByteArray @Override public byte[] toByteArray() { - byte newbuf[] = new byte[count]; - System.arraycopy(buf, 0, newbuf, 0, count); - return newbuf; + byte newBuf[] = new byte[count]; + System.arraycopy(buf, 0, newBuf, 0, count); + + return newBuf; } /** @@ -121,7 +124,7 @@ public final class FastByteArrayOutputStream extends ByteArrayOutputStream { *

* Note that care needs to be taken to avoid writes to * this output stream after the input stream is created. - * Failing to do so, may result in unpredictable behviour. + * Failing to do so, may result in unpredictable behaviour. * * @return a new {@code ByteArrayInputStream}, reading from this stream's buffer. */ diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java index c064b394..e514c3c8 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java @@ -48,16 +48,7 @@ import java.io.*; */ public final class FileCacheSeekableStream extends AbstractCachedSeekableStream { -// private final InputStream mStream; -// private final RandomAccessFile mCache; - private byte[] mBuffer; - - /** The stream positon in the backing stream (mStream) */ -// private long mStreamPosition; - - // TODO: getStreamPosition() should always be the same as - // mCache.getFilePointer() - // otherwise there's some inconsistency here... Enforce this? + private byte[] buffer; /** * Creates a {@code FileCacheSeekableStream} reading from the given @@ -118,7 +109,7 @@ public final class FileCacheSeekableStream extends AbstractCachedSeekableStream super(pStream, new FileCache(pFile)); // TODO: Allow for custom buffer sizes? - mBuffer = new byte[1024]; + buffer = new byte[1024]; } public final boolean isCachedMemory() { @@ -132,39 +123,19 @@ public final class FileCacheSeekableStream extends AbstractCachedSeekableStream @Override protected void closeImpl() throws IOException { super.closeImpl(); - mBuffer = null; + buffer = null; } -/* - public final boolean isCached() { - return true; - } - - // InputStream overrides - @Override - public int available() throws IOException { - long avail = mStreamPosition - mPosition + mStream.available(); - return avail > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) avail; - } - - public void closeImpl() throws IOException { - mStream.close(); - mCache.close(); - - // TODO: Delete cache file here? - // ThreadPool.invokeLater(new DeleteFileAction(mCacheFile)); - } - */ @Override public int read() throws IOException { checkOpen(); int read; - if (mPosition == mStreamPosition) { + if (position == streamPosition) { // Read ahead into buffer, for performance - read = readAhead(mBuffer, 0, mBuffer.length); + read = readAhead(buffer, 0, buffer.length); if (read >= 0) { - read = mBuffer[0] & 0xff; + read = buffer[0] & 0xff; } //System.out.println("Read 1 byte from stream: " + Integer.toHexString(read & 0xff)); @@ -179,7 +150,7 @@ public final class FileCacheSeekableStream extends AbstractCachedSeekableStream // TODO: This field is not REALLY considered accessible.. :-P if (read != -1) { - mPosition++; + position++; } return read; } @@ -189,7 +160,7 @@ public final class FileCacheSeekableStream extends AbstractCachedSeekableStream checkOpen(); int length; - if (mPosition == mStreamPosition) { + if (position == streamPosition) { // Read bytes from the stream length = readAhead(pBytes, pOffset, pLength); @@ -198,83 +169,29 @@ public final class FileCacheSeekableStream extends AbstractCachedSeekableStream else { // ...or read bytes from the cache syncPosition(); - length = getCache().read(pBytes, pOffset, (int) Math.min(pLength, mStreamPosition - mPosition)); + length = getCache().read(pBytes, pOffset, (int) Math.min(pLength, streamPosition - position)); //System.out.println("Read " + length + " byte from cache"); } // TODO: This field is not REALLY considered accessible.. :-P if (length > 0) { - mPosition += length; + position += length; } return length; } private int readAhead(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { int length; - length = mStream.read(pBytes, pOffset, pLength); + length = stream.read(pBytes, pOffset, pLength); if (length > 0) { - mStreamPosition += length; + streamPosition += length; getCache().write(pBytes, pOffset, length); } return length; } - /* - private void syncPosition() throws IOException { - if (mCache.getFilePointer() != mPosition) { - mCache.seek(mPosition); // Assure EOF is correctly thrown - } - } - - // Seekable overrides - - protected void flushBeforeImpl(long pPosition) { - // TODO: Implement - // For now, it's probably okay to do nothing, this is just for - // performance (as long as people follow spec, not behaviour) - } - - protected void seekImpl(long pPosition) throws IOException { - if (mStreamPosition < pPosition) { - // Make sure we append at end of cache - if (mCache.getFilePointer() != mStreamPosition) { - mCache.seek(mStreamPosition); - } - - // Read diff from stream into cache - long left = pPosition - mStreamPosition; - int bufferLen = left > 1024 ? 1024 : (int) left; - byte[] buffer = new byte[bufferLen]; - - while (left > 0) { - int length = buffer.length < left ? buffer.length : (int) left; - int read = mStream.read(buffer, 0, length); - - if (read > 0) { - mCache.write(buffer, 0, read); - mStreamPosition += read; - left -= read; - } - else if (read < 0) { - break; - } - } - } - else if (mStreamPosition >= pPosition) { - // Seek backwards into the cache - mCache.seek(pPosition); - } - -// System.out.println("pPosition: " + pPosition); -// System.out.println("mStreamPosition: " + mStreamPosition); -// System.out.println("mCache.getFilePointer(): " + mCache.getFilePointer()); - - // NOTE: If mPosition == pPosition then we're good to go - } - */ - final static class FileCache extends StreamCache { private RandomAccessFile mCacheFile; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java index 87bcee47..e86ad5f0 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java @@ -87,7 +87,7 @@ public final class FileSeekableStream extends SeekableInputStream { @Override public int available() throws IOException { - long length = mRandomAccess.length() - mPosition; + long length = mRandomAccess.length() - position; return length > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) length; } @@ -100,7 +100,7 @@ public final class FileSeekableStream extends SeekableInputStream { int read = mRandomAccess.read(); if (read >= 0) { - mPosition++; + position++; } return read; } @@ -111,7 +111,7 @@ public final class FileSeekableStream extends SeekableInputStream { int read = mRandomAccess.read(pBytes, pOffset, pLength); if (read > 0) { - mPosition += read; + position += read; } return read; } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FileSystem.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FileSystem.java index 7670ac3e..9b18f789 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FileSystem.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FileSystem.java @@ -38,7 +38,7 @@ import java.io.InputStreamReader; *

* * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSystem.java#1 $ + * @version $Id: FileSystem.java#1 $ */ abstract class FileSystem { abstract long getFreeSpace(File pPath); @@ -57,21 +57,21 @@ abstract class FileSystem { //System.out.println("os = " + os); os = os.toLowerCase(); - if (os.indexOf("windows") != -1) { + if (os.contains("windows")) { return new Win32FileSystem(); } - else if (os.indexOf("linux") != -1 || - os.indexOf("sun os") != -1 || - os.indexOf("sunos") != -1 || - os.indexOf("solaris") != -1 || - os.indexOf("mpe/ix") != -1 || - os.indexOf("hp-ux") != -1 || - os.indexOf("aix") != -1 || - os.indexOf("freebsd") != -1 || - os.indexOf("irix") != -1 || - os.indexOf("digital unix") != -1 || - os.indexOf("unix") != -1 || - os.indexOf("mac os x") != -1) { + else if (os.contains("linux") || + os.contains("sun os") || + os.contains("sunos") || + os.contains("solaris") || + os.contains("mpe/ix") || + os.contains("hp-ux") || + os.contains("aix") || + os.contains("freebsd") || + os.contains("irix") || + os.contains("digital unix") || + os.contains("unix") || + os.contains("mac os x")) { return new UnixFileSystem(); } else { @@ -80,10 +80,10 @@ abstract class FileSystem { } private static class UnknownFileSystem extends FileSystem { - private final String mOSName; + private final String osName; UnknownFileSystem(String pOSName) { - mOSName = pOSName; + osName = pOSName; } long getFreeSpace(File pPath) { @@ -95,7 +95,7 @@ abstract class FileSystem { } String getName() { - return "Unknown (" + mOSName + ")"; + return "Unknown (" + osName + ")"; } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java index 1e16c2a0..f538d742 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java @@ -186,6 +186,7 @@ public final class FileUtil { if (!pOverWrite && pToFile.exists()) { return false; } + InputStream in = null; OutputStream out = null; @@ -202,6 +203,7 @@ public final class FileUtil { close(in); close(out); } + return true; // If we got here, everything's probably okay.. ;-) } @@ -307,6 +309,8 @@ public final class FileUtil { Validate.notNull(pFrom, "from"); Validate.notNull(pTo, "to"); + // TODO: Consider using file channels for faster copy where possible + // Use buffer size two times byte array, to avoid i/o bottleneck // TODO: Consider letting the client decide as this is sometimes not a good thing! InputStream in = new BufferedInputStream(pFrom, BUF_SIZE * 2); @@ -322,31 +326,9 @@ public final class FileUtil { // Flush out stream, to write any remaining buffered data out.flush(); - return true; // If we got here, everything's probably okay.. ;-) + return true; // If we got here, everything is probably okay.. ;-) } - /* - // Consider using the example from - // http://developer.java.sun.com/developer/Books/performance/ch04.pdf - // Test if this is really faster. And what about a lot of concurrence? - // Have a pool of buffers? :-) - - static final int BUFF_SIZE = 100000; - static final byte[] buffer = new byte[BUFF_SIZE]; - - public static void copy(InputStream in, OutputStream out) throws IOException { - while (true) { - synchronized (buffer) { - int amountRead = in.read(buffer); - if (amountRead == -1) { - break; - } - out.write(buffer, 0, amountRead); - } - } - } - */ - /** * Gets the file (type) extension of the given file. * A file extension is the part of the filename, after the last occurence @@ -568,6 +550,7 @@ public final class FileUtil { if (!pFile.exists()) { throw new FileNotFoundException(pFile.toString()); } + byte[] bytes = new byte[(int) pFile.length()]; InputStream in = null; @@ -586,6 +569,7 @@ public final class FileUtil { finally { close(in); } + return bytes; } @@ -685,28 +669,28 @@ public final class FileUtil { // a file array, which may throw OutOfMemoryExceptions for // large directories/in low memory situations class DeleteFilesVisitor implements Visitor { - private int mFailedCount = 0; - private IOException mException = null; + private int failedCount = 0; + private IOException exception = null; public void visit(final File pFile) { try { if (!delete(pFile, true)) { - mFailedCount++; + failedCount++; } } catch (IOException e) { - mFailedCount++; - if (mException == null) { - mException = e; + failedCount++; + if (exception == null) { + exception = e; } } } boolean succeeded() throws IOException { - if (mException != null) { - throw mException; + if (exception != null) { + throw exception; } - return mFailedCount == 0; + return failedCount == 0; } } DeleteFilesVisitor fileDeleter = new DeleteFilesVisitor(); @@ -886,6 +870,8 @@ public final class FileUtil { return folder.listFiles(); } + // TODO: Rewrite to use regexp + FilenameFilter filter = new FilenameMaskFilter(pFilenameMask); return folder.listFiles(filter); } @@ -1029,6 +1015,7 @@ public final class FileUtil { * @return a human readable string representation */ public static String toHumanReadableSize(final long pSizeInBytes) { + // TODO: Rewrite to use String.format? if (pSizeInBytes < 1024L) { return pSizeInBytes + " Bytes"; } @@ -1053,7 +1040,7 @@ public final class FileUtil { private static ThreadLocal sNumberFormat = new ThreadLocal() { protected NumberFormat initialValue() { NumberFormat format = NumberFormat.getNumberInstance(); - // TODO: Consider making this locale/platfor specific, OR a method parameter... + // TODO: Consider making this locale/platform specific, OR a method parameter... // format.setMaximumFractionDigits(2); format.setMaximumFractionDigits(0); return format; @@ -1075,6 +1062,7 @@ public final class FileUtil { * * @see com.twelvemonkeys.util.Visitor */ + @SuppressWarnings({"ResultOfMethodCallIgnored"}) public static void visitFiles(final File pDirectory, final FileFilter pFilter, final Visitor pVisitor) { Validate.notNull(pDirectory, "directory"); Validate.notNull(pVisitor, "visitor"); diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java index ed3cd477..6a541621 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java @@ -56,13 +56,16 @@ import java.io.FilenameFilter; * @see File#list(java.io.FilenameFilter) java.io.File.list * @see FilenameFilter java.io.FilenameFilter * @see WildcardStringParser + * @deprecated */ public class FilenameMaskFilter implements FilenameFilter { + // TODO: Rewrite to use regexp, or create new class + // Members - private String[] mFilenameMasksForInclusion; - private String[] mFilenameMasksForExclusion; - private boolean mInclusion = true; + private String[] filenameMasksForInclusion; + private String[] filenameMasksForExclusion; + private boolean inclusion = true; /** @@ -127,29 +130,29 @@ public class FilenameMaskFilter implements FilenameFilter { * @param pFilenameMasksForInclusion the filename masks to include */ public void setFilenameMasksForInclusion(String[] pFilenameMasksForInclusion) { - mFilenameMasksForInclusion = pFilenameMasksForInclusion; + filenameMasksForInclusion = pFilenameMasksForInclusion; } /** * @return the current inclusion masks */ public String[] getFilenameMasksForInclusion() { - return mFilenameMasksForInclusion.clone(); + return filenameMasksForInclusion.clone(); } /** * @param pFilenameMasksForExclusion the filename masks to exclude */ public void setFilenameMasksForExclusion(String[] pFilenameMasksForExclusion) { - mFilenameMasksForExclusion = pFilenameMasksForExclusion; - mInclusion = false; + filenameMasksForExclusion = pFilenameMasksForExclusion; + inclusion = false; } /** * @return the current exclusion masks */ public String[] getFilenameMasksForExclusion() { - return mFilenameMasksForExclusion.clone(); + return filenameMasksForExclusion.clone(); } /** @@ -164,8 +167,8 @@ public class FilenameMaskFilter implements FilenameFilter { WildcardStringParser parser; // Check each filename string mask whether the file is to be accepted - if (mInclusion) { // Inclusion - for (String mask : mFilenameMasksForInclusion) { + if (inclusion) { // Inclusion + for (String mask : filenameMasksForInclusion) { parser = new WildcardStringParser(mask); if (parser.parseString(pName)) { @@ -181,7 +184,7 @@ public class FilenameMaskFilter implements FilenameFilter { } else { // Exclusion - for (String mask : mFilenameMasksForExclusion) { + for (String mask : filenameMasksForExclusion) { parser = new WildcardStringParser(mask); if (parser.parseString(pName)) { @@ -204,32 +207,32 @@ public class FilenameMaskFilter implements FilenameFilter { StringBuilder retVal = new StringBuilder(); int i; - if (mInclusion) { + if (inclusion) { // Inclusion - if (mFilenameMasksForInclusion == null) { - retVal.append("No filename masks set - property mFilenameMasksForInclusion is null!"); + if (filenameMasksForInclusion == null) { + retVal.append("No filename masks set - property filenameMasksForInclusion is null!"); } else { - retVal.append(mFilenameMasksForInclusion.length); + retVal.append(filenameMasksForInclusion.length); retVal.append(" filename mask(s) - "); - for (i = 0; i < mFilenameMasksForInclusion.length; i++) { + for (i = 0; i < filenameMasksForInclusion.length; i++) { retVal.append("\""); - retVal.append(mFilenameMasksForInclusion[i]); + retVal.append(filenameMasksForInclusion[i]); retVal.append("\", \""); } } } else { // Exclusion - if (mFilenameMasksForExclusion == null) { - retVal.append("No filename masks set - property mFilenameMasksForExclusion is null!"); + if (filenameMasksForExclusion == null) { + retVal.append("No filename masks set - property filenameMasksForExclusion is null!"); } else { - retVal.append(mFilenameMasksForExclusion.length); + retVal.append(filenameMasksForExclusion.length); retVal.append(" exclusion filename mask(s) - "); - for (i = 0; i < mFilenameMasksForExclusion.length; i++) { + for (i = 0; i < filenameMasksForExclusion.length; i++) { retVal.append("\""); - retVal.append(mFilenameMasksForExclusion[i]); + retVal.append(filenameMasksForExclusion[i]); retVal.append("\", \""); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java deleted file mode 100755 index fc91efd9..00000000 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.io; - - -import com.twelvemonkeys.lang.StringUtil; - -import java.io.File; -import java.io.FilenameFilter; - - -/** - * A Java Bean used for approving file names which are to be included in a - * {@code java.io.File} listing. The file name suffixes are used as a - * filter input and is given to the class via the string array property:
- *

{@code filenameSuffixesToExclude} - *

- * A recommended way of doing this is by referencing to the component which uses - * this class for file listing. In this way all properties are set in the same - * component and this utility component is kept in the background with only - * initial configuration necessary. - * - * @author Eirik Torske - * @see File#list(java.io.FilenameFilter) java.io.File.list - * @see FilenameFilter java.io.FilenameFilter - */ -public class FilenameSuffixFilter implements FilenameFilter { - - // Members - String[] mFilenameSuffixesToExclude; - - /** Creates a {@code FileNameSuffixFilter} */ - public FilenameSuffixFilter() { - } - - public void setFilenameSuffixesToExclude(String[] pFilenameSuffixesToExclude) { - mFilenameSuffixesToExclude = pFilenameSuffixesToExclude; - } - - public String[] getFilenameSuffixesToExclude() { - return mFilenameSuffixesToExclude; - } - - /** - * This method implements the {@code java.io.FilenameFilter} interface. - *

- * - * @param pDir the directory in which the file was found. - * @param pName the pName of the file. - * @return {@code true} if the pName should be included in the file list; - * {@code false} otherwise. - */ - public boolean accept(final File pDir, final String pName) { - if (StringUtil.isEmpty(mFilenameSuffixesToExclude)) { - return true; - } - - for (String aMFilenameSuffixesToExclude : mFilenameSuffixesToExclude) { - // -- Edit by haraldK, to make interfaces more consistent - // if (StringUtil.filenameSuffixIs(pName, mFilenameSuffixesToExclude[i])) { - if (aMFilenameSuffixesToExclude.equals(FileUtil.getExtension(pName))) { - return false; - } - } - return true; - } -} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java index 013a8e2c..adceaead 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java @@ -38,6 +38,8 @@ package com.twelvemonkeys.io; +import com.twelvemonkeys.lang.Validate; + import java.io.*; /** @@ -75,10 +77,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da * @see java.io.FilterInputStream#in */ public LittleEndianDataInputStream(final InputStream pStream) { - super(pStream); - if (pStream == null) { - throw new IllegalArgumentException("stream == null"); - } + super(Validate.notNull(pStream, "stream")); } /** @@ -93,9 +92,11 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da */ public boolean readBoolean() throws IOException { int b = in.read(); + if (b < 0) { throw new EOFException(); } + return b != 0; } @@ -110,9 +111,11 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da */ public byte readByte() throws IOException { int b = in.read(); + if (b < 0) { throw new EOFException(); } + return (byte) b; } @@ -128,9 +131,11 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da */ public int readUnsignedByte() throws IOException { int b = in.read(); + if (b < 0) { throw new EOFException(); } + return b; } @@ -146,11 +151,13 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da public short readShort() throws IOException { int byte1 = in.read(); int byte2 = in.read(); + // only need to test last byte read // if byte1 is -1 so is byte2 if (byte2 < 0) { throw new EOFException(); } + return (short) (((byte2 << 24) >>> 16) + (byte1 << 24) >>> 24); } @@ -166,10 +173,11 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da public int readUnsignedShort() throws IOException { int byte1 = in.read(); int byte2 = in.read(); + if (byte2 < 0) { throw new EOFException(); } - //return ((byte2 << 24) >> 16) + ((byte1 << 24) >> 24); + return (byte2 << 8) + byte1; } @@ -185,9 +193,11 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da public char readChar() throws IOException { int byte1 = in.read(); int byte2 = in.read(); + if (byte2 < 0) { throw new EOFException(); } + return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); } @@ -210,6 +220,7 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da if (byte4 < 0) { throw new EOFException(); } + return (byte4 << 24) + ((byte3 << 24) >>> 8) + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); } @@ -236,11 +247,11 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da if (byte8 < 0) { throw new EOFException(); } + return (byte8 << 56) + ((byte7 << 56) >>> 8) + ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24) + ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40) + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); - } /** @@ -260,16 +271,17 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da public String readUTF() throws IOException { int byte1 = in.read(); int byte2 = in.read(); + if (byte2 < 0) { throw new EOFException(); } + int numbytes = (byte1 << 8) + byte2; char result[] = new char[numbytes]; int numread = 0; int numchars = 0; while (numread < numbytes) { - int c1 = readUnsignedByte(); int c2, c3; @@ -281,27 +293,34 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da } else if (test == 12 || test == 13) { // two bytes numread += 2; + if (numread > numbytes) { throw new UTFDataFormatException(); } + c2 = readUnsignedByte(); + if ((c2 & 0xC0) != 0x80) { throw new UTFDataFormatException(); } + result[numchars++] = (char) (((c1 & 0x1F) << 6) | (c2 & 0x3F)); } else if (test == 14) { // three bytes numread += 3; + if (numread > numbytes) { throw new UTFDataFormatException(); } + c2 = readUnsignedByte(); c3 = readUnsignedByte(); + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { throw new UTFDataFormatException(); } - result[numchars++] = (char) - (((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); + + result[numchars++] = (char) (((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); } else { // malformed throw new UTFDataFormatException(); @@ -396,12 +415,16 @@ public class LittleEndianDataInputStream extends FilterInputStream implements Da if (pLength < 0) { throw new IndexOutOfBoundsException(); } + int count = 0; + while (count < pLength) { int read = in.read(pBytes, pOffset + count, pLength - count); + if (read < 0) { throw new EOFException(); } + count += read; } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java index 518cf890..78699d73 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java @@ -38,6 +38,8 @@ package com.twelvemonkeys.io; +import com.twelvemonkeys.lang.Validate; + import java.io.*; /** @@ -69,7 +71,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements /** * The number of bytes written so far to the little endian output stream. */ - protected int mWritten; + protected int bytesWritten; /** * Creates a new little endian output stream and chains it to the @@ -79,10 +81,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements * @see java.io.FilterOutputStream#out */ public LittleEndianDataOutputStream(OutputStream pStream) { - super(pStream); - if (pStream == null) { - throw new IllegalArgumentException("stream == null"); - } + super(Validate.notNull(pStream, "stream")); } /** @@ -93,7 +92,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements */ public synchronized void write(int pByte) throws IOException { out.write(pByte); - mWritten++; + bytesWritten++; } /** @@ -105,10 +104,9 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements * @param pLength the number of bytes to write. * @throws IOException if the underlying stream throws an IOException. */ - public synchronized void write(byte[] pBytes, int pOffset, int pLength) - throws IOException { + public synchronized void write(byte[] pBytes, int pOffset, int pLength) throws IOException { out.write(pBytes, pOffset, pLength); - mWritten += pLength; + bytesWritten += pLength; } @@ -137,7 +135,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements */ public void writeByte(int pByte) throws IOException { out.write(pByte); - mWritten++; + bytesWritten++; } /** @@ -150,7 +148,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements public void writeShort(int pShort) throws IOException { out.write(pShort & 0xFF); out.write((pShort >>> 8) & 0xFF); - mWritten += 2; + bytesWritten += 2; } /** @@ -163,7 +161,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements public void writeChar(int pChar) throws IOException { out.write(pChar & 0xFF); out.write((pChar >>> 8) & 0xFF); - mWritten += 2; + bytesWritten += 2; } /** @@ -178,7 +176,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements out.write((pInt >>> 8) & 0xFF); out.write((pInt >>> 16) & 0xFF); out.write((pInt >>> 24) & 0xFF); - mWritten += 4; + bytesWritten += 4; } @@ -198,7 +196,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements out.write((int) (pLong >>> 40) & 0xFF); out.write((int) (pLong >>> 48) & 0xFF); out.write((int) (pLong >>> 56) & 0xFF); - mWritten += 8; + bytesWritten += 8; } /** @@ -235,10 +233,12 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements */ public void writeBytes(String pString) throws IOException { int length = pString.length(); + for (int i = 0; i < length; i++) { out.write((byte) pString.charAt(i)); } - mWritten += length; + + bytesWritten += length; } /** @@ -253,12 +253,14 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements */ public void writeChars(String pString) throws IOException { int length = pString.length(); + for (int i = 0; i < length; i++) { int c = pString.charAt(i); out.write(c & 0xFF); out.write((c >>> 8) & 0xFF); } - mWritten += length * 2; + + bytesWritten += length * 2; } /** @@ -282,6 +284,7 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements for (int i = 0; i < numchars; i++) { int c = pString.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { numbytes++; } @@ -299,8 +302,10 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements out.write((numbytes >>> 8) & 0xFF); out.write(numbytes & 0xFF); + for (int i = 0; i < numchars; i++) { int c = pString.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { out.write(c); } @@ -308,16 +313,16 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements out.write(0xE0 | ((c >> 12) & 0x0F)); out.write(0x80 | ((c >> 6) & 0x3F)); out.write(0x80 | (c & 0x3F)); - mWritten += 2; + bytesWritten += 2; } else { out.write(0xC0 | ((c >> 6) & 0x1F)); out.write(0x80 | (c & 0x3F)); - mWritten += 1; + bytesWritten += 1; } } - mWritten += numchars + 2; + bytesWritten += numchars + 2; } /** @@ -326,9 +331,9 @@ public class LittleEndianDataOutputStream extends FilterOutputStream implements * possible that this number is temporarily less than the actual * number of bytes written.) * @return the value of the {@code written} field. - * @see #mWritten + * @see #bytesWritten */ public int size() { - return mWritten; + return bytesWritten; } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java index 35eb4c95..6cb701dc 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java @@ -56,58 +56,58 @@ import java.nio.channels.FileChannel; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java#1 $ */ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { - private RandomAccessFile mFile; + private RandomAccessFile file; public LittleEndianRandomAccessFile(final String pName, final String pMode) throws FileNotFoundException { this(FileUtil.resolve(pName), pMode); } public LittleEndianRandomAccessFile(final File pFile, final String pMode) throws FileNotFoundException { - mFile = new RandomAccessFile(pFile, pMode); + file = new RandomAccessFile(pFile, pMode); } public void close() throws IOException { - mFile.close(); + file.close(); } public FileChannel getChannel() { - return mFile.getChannel(); + return file.getChannel(); } public FileDescriptor getFD() throws IOException { - return mFile.getFD(); + return file.getFD(); } public long getFilePointer() throws IOException { - return mFile.getFilePointer(); + return file.getFilePointer(); } public long length() throws IOException { - return mFile.length(); + return file.length(); } public int read() throws IOException { - return mFile.read(); + return file.read(); } public int read(final byte[] b) throws IOException { - return mFile.read(b); + return file.read(b); } public int read(final byte[] b, final int off, final int len) throws IOException { - return mFile.read(b, off, len); + return file.read(b, off, len); } public void readFully(final byte[] b) throws IOException { - mFile.readFully(b); + file.readFully(b); } public void readFully(final byte[] b, final int off, final int len) throws IOException { - mFile.readFully(b, off, len); + file.readFully(b, off, len); } public String readLine() throws IOException { - return mFile.readLine(); + return file.readLine(); } /** @@ -121,10 +121,12 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public boolean readBoolean() throws IOException { - int b = mFile.read(); + int b = file.read(); + if (b < 0) { throw new EOFException(); } + return b != 0; } @@ -138,10 +140,12 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public byte readByte() throws IOException { - int b = mFile.read(); + int b = file.read(); + if (b < 0) { throw new EOFException(); } + return (byte) b; } @@ -156,10 +160,12 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public int readUnsignedByte() throws IOException { - int b = mFile.read(); + int b = file.read(); + if (b < 0) { throw new EOFException(); } + return b; } @@ -173,13 +179,15 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public short readShort() throws IOException { - int byte1 = mFile.read(); - int byte2 = mFile.read(); + int byte1 = file.read(); + int byte2 = file.read(); + // only need to test last byte read // if byte1 is -1 so is byte2 if (byte2 < 0) { throw new EOFException(); } + return (short) (((byte2 << 24) >>> 16) + (byte1 << 24) >>> 24); } @@ -193,11 +201,13 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public int readUnsignedShort() throws IOException { - int byte1 = mFile.read(); - int byte2 = mFile.read(); + int byte1 = file.read(); + int byte2 = file.read(); + if (byte2 < 0) { throw new EOFException(); } + //return ((byte2 << 24) >> 16) + ((byte1 << 24) >> 24); return (byte2 << 8) + byte1; } @@ -212,11 +222,13 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public char readChar() throws IOException { - int byte1 = mFile.read(); - int byte2 = mFile.read(); + int byte1 = file.read(); + int byte2 = file.read(); + if (byte2 < 0) { throw new EOFException(); } + return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); } @@ -231,16 +243,16 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public int readInt() throws IOException { - int byte1 = mFile.read(); - int byte2 = mFile.read(); - int byte3 = mFile.read(); - int byte4 = mFile.read(); + int byte1 = file.read(); + int byte2 = file.read(); + int byte3 = file.read(); + int byte4 = file.read(); if (byte4 < 0) { throw new EOFException(); } - return (byte4 << 24) + ((byte3 << 24) >>> 8) - + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); + + return (byte4 << 24) + ((byte3 << 24) >>> 8) + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); } /** @@ -253,18 +265,19 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public long readLong() throws IOException { - long byte1 = mFile.read(); - long byte2 = mFile.read(); - long byte3 = mFile.read(); - long byte4 = mFile.read(); - long byte5 = mFile.read(); - long byte6 = mFile.read(); - long byte7 = mFile.read(); - long byte8 = mFile.read(); + long byte1 = file.read(); + long byte2 = file.read(); + long byte3 = file.read(); + long byte4 = file.read(); + long byte5 = file.read(); + long byte6 = file.read(); + long byte7 = file.read(); + long byte8 = file.read(); if (byte8 < 0) { throw new EOFException(); } + return (byte8 << 56) + ((byte7 << 56) >>> 8) + ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24) + ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40) @@ -287,11 +300,13 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public String readUTF() throws IOException { - int byte1 = mFile.read(); - int byte2 = mFile.read(); + int byte1 = file.read(); + int byte2 = file.read(); + if (byte2 < 0) { throw new EOFException(); } + int numbytes = (byte1 << 8) + byte2; char result[] = new char[numbytes]; int numread = 0; @@ -310,27 +325,34 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { } else if (test == 12 || test == 13) { // two bytes numread += 2; + if (numread > numbytes) { throw new UTFDataFormatException(); } + c2 = readUnsignedByte(); + if ((c2 & 0xC0) != 0x80) { throw new UTFDataFormatException(); } + result[numchars++] = (char) (((c1 & 0x1F) << 6) | (c2 & 0x3F)); } else if (test == 14) { // three bytes numread += 3; + if (numread > numbytes) { throw new UTFDataFormatException(); } + c2 = readUnsignedByte(); c3 = readUnsignedByte(); + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { throw new UTFDataFormatException(); } - result[numchars++] = (char) - (((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); + + result[numchars++] = (char) (((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); } else { // malformed throw new UTFDataFormatException(); @@ -378,27 +400,27 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * {@code 0} or if an I/O error occurs. */ public void seek(final long pos) throws IOException { - mFile.seek(pos); + file.seek(pos); } public void setLength(final long newLength) throws IOException { - mFile.setLength(newLength); + file.setLength(newLength); } public int skipBytes(final int n) throws IOException { - return mFile.skipBytes(n); + return file.skipBytes(n); } public void write(final byte[] b) throws IOException { - mFile.write(b); + file.write(b); } public void write(final byte[] b, final int off, final int len) throws IOException { - mFile.write(b, off, len); + file.write(b, off, len); } public void write(final int b) throws IOException { - mFile.write(b); + file.write(b); } /** @@ -425,7 +447,7 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public void writeByte(int pByte) throws IOException { - mFile.write(pByte); + file.write(pByte); } /** @@ -436,8 +458,8 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public void writeShort(int pShort) throws IOException { - mFile.write(pShort & 0xFF); - mFile.write((pShort >>> 8) & 0xFF); + file.write(pShort & 0xFF); + file.write((pShort >>> 8) & 0xFF); } /** @@ -448,8 +470,8 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public void writeChar(int pChar) throws IOException { - mFile.write(pChar & 0xFF); - mFile.write((pChar >>> 8) & 0xFF); + file.write(pChar & 0xFF); + file.write((pChar >>> 8) & 0xFF); } /** @@ -460,11 +482,10 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public void writeInt(int pInt) throws IOException { - mFile.write(pInt & 0xFF); - mFile.write((pInt >>> 8) & 0xFF); - mFile.write((pInt >>> 16) & 0xFF); - mFile.write((pInt >>> 24) & 0xFF); - + file.write(pInt & 0xFF); + file.write((pInt >>> 8) & 0xFF); + file.write((pInt >>> 16) & 0xFF); + file.write((pInt >>> 24) & 0xFF); } /** @@ -475,14 +496,14 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @throws IOException if the underlying stream throws an IOException. */ public void writeLong(long pLong) throws IOException { - mFile.write((int) pLong & 0xFF); - mFile.write((int) (pLong >>> 8) & 0xFF); - mFile.write((int) (pLong >>> 16) & 0xFF); - mFile.write((int) (pLong >>> 24) & 0xFF); - mFile.write((int) (pLong >>> 32) & 0xFF); - mFile.write((int) (pLong >>> 40) & 0xFF); - mFile.write((int) (pLong >>> 48) & 0xFF); - mFile.write((int) (pLong >>> 56) & 0xFF); + file.write((int) pLong & 0xFF); + file.write((int) (pLong >>> 8) & 0xFF); + file.write((int) (pLong >>> 16) & 0xFF); + file.write((int) (pLong >>> 24) & 0xFF); + file.write((int) (pLong >>> 32) & 0xFF); + file.write((int) (pLong >>> 40) & 0xFF); + file.write((int) (pLong >>> 48) & 0xFF); + file.write((int) (pLong >>> 56) & 0xFF); } /** @@ -515,12 +536,13 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @param pString the {@code String} value to be written. * @throws IOException if the underlying stream throws an IOException. * @see #writeByte(int) - * @see #mFile + * @see #file */ public void writeBytes(String pString) throws IOException { int length = pString.length(); + for (int i = 0; i < length; i++) { - mFile.write((byte) pString.charAt(i)); + file.write((byte) pString.charAt(i)); } } @@ -532,14 +554,15 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { * @param pString a {@code String} value to be written. * @throws IOException if the underlying stream throws an IOException. * @see #writeChar(int) - * @see #mFile + * @see #file */ public void writeChars(String pString) throws IOException { int length = pString.length(); + for (int i = 0; i < length; i++) { int c = pString.charAt(i); - mFile.write(c & 0xFF); - mFile.write((c >>> 8) & 0xFF); + file.write(c & 0xFF); + file.write((c >>> 8) & 0xFF); } } @@ -564,6 +587,7 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { for (int i = 0; i < numchars; i++) { int c = pString.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { numbytes++; } @@ -579,21 +603,23 @@ public class LittleEndianRandomAccessFile implements DataInput, DataOutput { throw new UTFDataFormatException(); } - mFile.write((numbytes >>> 8) & 0xFF); - mFile.write(numbytes & 0xFF); + file.write((numbytes >>> 8) & 0xFF); + file.write(numbytes & 0xFF); + for (int i = 0; i < numchars; i++) { int c = pString.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { - mFile.write(c); + file.write(c); } else if (c > 0x07FF) { - mFile.write(0xE0 | ((c >> 12) & 0x0F)); - mFile.write(0x80 | ((c >> 6) & 0x3F)); - mFile.write(0x80 | (c & 0x3F)); + file.write(0xE0 | ((c >> 12) & 0x0F)); + file.write(0x80 | ((c >> 6) & 0x3F)); + file.write(0x80 | (c & 0x3F)); } else { - mFile.write(0xC0 | ((c >> 6) & 0x1F)); - mFile.write(0x80 | (c & 0x3F)); + file.write(0xC0 | ((c >> 6) & 0x1F)); + file.write(0x80 | (c & 0x3F)); } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java index 7a0631c3..70bf86ff 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java @@ -65,13 +65,13 @@ public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStrea final static class MemoryCache extends StreamCache { final static int BLOCK_SIZE = 1 << 13; - private final List mCache = new ArrayList(); - private long mLength; - private long mPosition; - private long mStart; + private final List cache = new ArrayList(); + private long length; + private long position; + private long start; private byte[] getBlock() throws IOException { - final long currPos = mPosition - mStart; + final long currPos = position - start; if (currPos < 0) { throw new IOException("StreamCache flushed before read position"); } @@ -82,31 +82,31 @@ public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStrea throw new IOException("Memory cache max size exceeded"); } - if (index >= mCache.size()) { + if (index >= cache.size()) { try { - mCache.add(new byte[BLOCK_SIZE]); + cache.add(new byte[BLOCK_SIZE]); // System.out.println("Allocating new block, size: " + BLOCK_SIZE); -// System.out.println("New total size: " + mCache.size() * BLOCK_SIZE + " (" + mCache.size() + " blocks)"); +// System.out.println("New total size: " + cache.size() * BLOCK_SIZE + " (" + cache.size() + " blocks)"); } catch (OutOfMemoryError e) { - throw new IOException("No more memory for cache: " + mCache.size() * BLOCK_SIZE); + throw new IOException("No more memory for cache: " + cache.size() * BLOCK_SIZE); } } //System.out.println("index: " + index); - return mCache.get((int) index); + return cache.get((int) index); } public void write(final int pByte) throws IOException { byte[] buffer = getBlock(); - int idx = (int) (mPosition % BLOCK_SIZE); + int idx = (int) (position % BLOCK_SIZE); buffer[idx] = (byte) pByte; - mPosition++; + position++; - if (mPosition > mLength) { - mLength = mPosition; + if (position > length) { + length = position; } } @@ -115,28 +115,28 @@ public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStrea public void write(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { byte[] buffer = getBlock(); for (int i = 0; i < pLength; i++) { - int index = (int) mPosition % BLOCK_SIZE; + int index = (int) position % BLOCK_SIZE; if (index == 0) { buffer = getBlock(); } buffer[index] = pBuffer[pOffset + i]; - mPosition++; + position++; } - if (mPosition > mLength) { - mLength = mPosition; + if (position > length) { + length = position; } } public int read() throws IOException { - if (mPosition >= mLength) { + if (position >= length) { return -1; } byte[] buffer = getBlock(); - int idx = (int) (mPosition % BLOCK_SIZE); - mPosition++; + int idx = (int) (position % BLOCK_SIZE); + position++; return buffer[idx] & 0xff; } @@ -144,33 +144,33 @@ public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStrea // TODO: OptimizeMe!!! @Override public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (mPosition >= mLength) { + if (position >= length) { return -1; } byte[] buffer = getBlock(); - int bufferPos = (int) (mPosition % BLOCK_SIZE); + int bufferPos = (int) (position % BLOCK_SIZE); // Find maxIdx and simplify test in for-loop - int maxLen = (int) Math.min(Math.min(pLength, buffer.length - bufferPos), mLength - mPosition); + int maxLen = (int) Math.min(Math.min(pLength, buffer.length - bufferPos), length - position); int i; - //for (i = 0; i < pLength && i < buffer.length - idx && i < mLength - mPosition; i++) { + //for (i = 0; i < pLength && i < buffer.length - idx && i < length - position; i++) { for (i = 0; i < maxLen; i++) { pBytes[pOffset + i] = buffer[bufferPos + i]; } - mPosition += i; + position += i; return i; } public void seek(final long pPosition) throws IOException { - if (pPosition < mStart) { + if (pPosition < start) { throw new IOException("Seek before flush position"); } - mPosition = pPosition; + position = pPosition; } @Override @@ -178,14 +178,14 @@ public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStrea int firstPos = (int) (pPosition / BLOCK_SIZE) - 1; for (int i = 0; i < firstPos; i++) { - mCache.remove(0); + cache.remove(0); } - mStart = pPosition; + start = pPosition; } public long getPosition() { - return mPosition; + return position; } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java index e717cbd2..196da6e9 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java @@ -50,13 +50,12 @@ public abstract class RandomAccessStream implements Seekable, DataInput, DataOut // TODO: Package private SeekableDelegate? // TODO: Both read and write must update stream position - //private int mPosition = -1; + //private int position = -1; /** This random access stream, wrapped in an {@code InputStream} */ - SeekableInputStream mInputView = null; + SeekableInputStream inputView = null; /** This random access stream, wrapped in an {@code OutputStream} */ - SeekableOutputStream mOutputView = null; - + SeekableOutputStream outputView = null; // TODO: Create an Input and an Output interface matching InputStream and OutputStream? public int read() throws IOException { @@ -119,10 +118,10 @@ public abstract class RandomAccessStream implements Seekable, DataInput, DataOut * @return a {@code SeekableInputStream} reading from this stream */ public final SeekableInputStream asInputStream() { - if (mInputView == null) { - mInputView = new InputStreamView(this); + if (inputView == null) { + inputView = new InputStreamView(this); } - return mInputView; + return inputView; } /** @@ -134,15 +133,15 @@ public abstract class RandomAccessStream implements Seekable, DataInput, DataOut * @return a {@code SeekableOutputStream} writing to this stream */ public final SeekableOutputStream asOutputStream() { - if (mOutputView == null) { - mOutputView = new OutputStreamView(this); + if (outputView == null) { + outputView = new OutputStreamView(this); } - return mOutputView; + return outputView; } static final class InputStreamView extends SeekableInputStream { - // TODO: Consider adding synchonization (on mStream) for all operations - // TODO: Is is a good thing that close/flush etc works on mStream? + // TODO: Consider adding synchonization (on stream) for all operations + // TODO: Is is a good thing that close/flush etc works on stream? // - Or should it rather just work on the views? // - Allow multiple views? @@ -190,8 +189,8 @@ public abstract class RandomAccessStream implements Seekable, DataInput, DataOut } static final class OutputStreamView extends SeekableOutputStream { - // TODO: Consider adding synchonization (on mStream) for all operations - // TODO: Is is a good thing that close/flush etc works on mStream? + // TODO: Consider adding synchonization (on stream) for all operations + // TODO: Is is a good thing that close/flush etc works on stream? // - Or should it rather just work on the views? // - Allow multiple views? diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java index 1ff511a2..0a605174 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java @@ -43,15 +43,15 @@ import java.util.Stack; public abstract class SeekableInputStream extends InputStream implements Seekable { // TODO: It's at the moment not possible to create subclasses outside this - // package, as there's no access to mPosition. mPosition needs to be + // package, as there's no access to position. position needs to be // updated from the read/read/read methods... /** The stream position in this stream */ - long mPosition; - long mFlushedPosition; - boolean mClosed; + long position; + long flushedPosition; + boolean closed; - protected Stack mMarkedPositions = new Stack(); + protected Stack markedPositions = new Stack(); /// InputStream overrides @Override @@ -69,17 +69,29 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl * @throws IOException if an I/O exception occurs during skip */ @Override - public final long skip(long pLength) throws IOException { - long pos = mPosition; - if (pos + pLength < mFlushedPosition) { + public final long skip(final long pLength) throws IOException { + long pos = position; + long wantedPosition = pos + pLength; + if (wantedPosition < flushedPosition) { throw new IOException("position < flushedPosition"); } - // Stop at stream length for compatibility, even though it's allowed + // Stop at stream length for compatibility, even though it might be allowed // to seek past end of stream - seek(Math.min(pos + pLength, pos + available())); + int available = available(); + if (available > 0) { + seek(Math.min(wantedPosition, pos + available)); + } + // TODO: Add optimization for streams with known length! + else { + // Slow mode... + int toSkip = (int) Math.max(Math.min(pLength, 512), -512); + while (toSkip > 0 && read() >= 0) { + toSkip--; + } + } - return mPosition - pos; + return position - pos; } @Override @@ -88,7 +100,7 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl // TODO: We don't really need to do this.. Is it a good idea? try { - flushBefore(Math.max(mPosition - pLimit, mFlushedPosition)); + flushBefore(Math.max(position - pLimit, flushedPosition)); } catch (IOException ignore) { // Ignore, as it's not really critical @@ -111,29 +123,29 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl // NOTE: This is correct according to javax.imageio (IndexOutOfBoundsException), // but it's kind of inconsistent with reset that throws IOException... - if (pPosition < mFlushedPosition) { + if (pPosition < flushedPosition) { throw new IndexOutOfBoundsException("position < flushedPosition"); } seekImpl(pPosition); - mPosition = pPosition; + position = pPosition; } protected abstract void seekImpl(long pPosition) throws IOException; public final void mark() { - mMarkedPositions.push(mPosition); + markedPositions.push(position); } @Override public final void reset() throws IOException { checkOpen(); - if (!mMarkedPositions.isEmpty()) { - long newPos = mMarkedPositions.pop(); + if (!markedPositions.isEmpty()) { + long newPos = markedPositions.pop(); // NOTE: This is correct according to javax.imageio (IOException), // but it's kind of inconsistent with seek that throws IndexOutOfBoundsException... - if (newPos < mFlushedPosition) { + if (newPos < flushedPosition) { throw new IOException("Previous marked position has been discarded"); } @@ -150,7 +162,7 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl } public final void flushBefore(long pPosition) throws IOException { - if (pPosition < mFlushedPosition) { + if (pPosition < flushedPosition) { throw new IndexOutOfBoundsException("position < flushedPosition"); } if (pPosition > getStreamPosition()) { @@ -158,7 +170,7 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl } checkOpen(); flushBeforeImpl(pPosition); - mFlushedPosition = pPosition; + flushedPosition = pPosition; } /** @@ -172,21 +184,21 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl protected abstract void flushBeforeImpl(long pPosition) throws IOException; public final void flush() throws IOException { - flushBefore(mFlushedPosition); + flushBefore(flushedPosition); } public final long getFlushedPosition() throws IOException { checkOpen(); - return mFlushedPosition; + return flushedPosition; } public final long getStreamPosition() throws IOException { checkOpen(); - return mPosition; + return position; } protected final void checkOpen() throws IOException { - if (mClosed) { + if (closed) { throw new IOException("closed"); } } @@ -194,7 +206,7 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl @Override public final void close() throws IOException { checkOpen(); - mClosed = true; + closed = true; closeImpl(); } @@ -211,7 +223,7 @@ public abstract class SeekableInputStream extends InputStream implements Seekabl */ @Override protected void finalize() throws Throwable { - if (!mClosed) { + if (!closed) { try { close(); } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java index 2061e833..133597ec 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java @@ -43,11 +43,11 @@ import java.util.Stack; */ public abstract class SeekableOutputStream extends OutputStream implements Seekable { // TODO: Implement - long mPosition; - long mFlushedPosition; - boolean mClosed; + long position; + long flushedPosition; + boolean closed; - protected Stack mMarkedPositions = new Stack(); + protected Stack markedPositions = new Stack(); /// Outputstream overrides @Override @@ -63,28 +63,28 @@ public abstract class SeekableOutputStream extends OutputStream implements Seeka // TODO: This is correct according to javax.imageio (IndexOutOfBoundsException), // but it's inconsistent with reset that throws IOException... - if (pPosition < mFlushedPosition) { + if (pPosition < flushedPosition) { throw new IndexOutOfBoundsException("position < flushedPosition!"); } seekImpl(pPosition); - mPosition = pPosition; + position = pPosition; } protected abstract void seekImpl(long pPosition) throws IOException; public final void mark() { - mMarkedPositions.push(mPosition); + markedPositions.push(position); } public final void reset() throws IOException { checkOpen(); - if (!mMarkedPositions.isEmpty()) { - long newPos = mMarkedPositions.pop(); + if (!markedPositions.isEmpty()) { + long newPos = markedPositions.pop(); // TODO: This is correct according to javax.imageio (IOException), // but it's inconsistent with seek that throws IndexOutOfBoundsException... - if (newPos < mFlushedPosition) { + if (newPos < flushedPosition) { throw new IOException("Previous marked position has been discarded!"); } @@ -93,7 +93,7 @@ public abstract class SeekableOutputStream extends OutputStream implements Seeka } public final void flushBefore(long pPosition) throws IOException { - if (pPosition < mFlushedPosition) { + if (pPosition < flushedPosition) { throw new IndexOutOfBoundsException("position < flushedPosition!"); } if (pPosition > getStreamPosition()) { @@ -101,28 +101,28 @@ public abstract class SeekableOutputStream extends OutputStream implements Seeka } checkOpen(); flushBeforeImpl(pPosition); - mFlushedPosition = pPosition; + flushedPosition = pPosition; } protected abstract void flushBeforeImpl(long pPosition) throws IOException; @Override public final void flush() throws IOException { - flushBefore(mFlushedPosition); + flushBefore(flushedPosition); } public final long getFlushedPosition() throws IOException { checkOpen(); - return mFlushedPosition; + return flushedPosition; } public final long getStreamPosition() throws IOException { checkOpen(); - return mPosition; + return position; } protected final void checkOpen() throws IOException { - if (mClosed) { + if (closed) { throw new IOException("closed"); } } @@ -130,7 +130,7 @@ public abstract class SeekableOutputStream extends OutputStream implements Seeka @Override public final void close() throws IOException { checkOpen(); - mClosed = true; + closed = true; closeImpl(); } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/StringArrayReader.java b/common/common-io/src/main/java/com/twelvemonkeys/io/StringArrayReader.java index 27d59d89..d90b3e1b 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/StringArrayReader.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/StringArrayReader.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.io; +import com.twelvemonkeys.lang.Validate; + import java.io.StringReader; import java.io.IOException; import java.io.Reader; @@ -42,13 +44,13 @@ import java.io.Reader; */ public class StringArrayReader extends StringReader { - private StringReader mCurrent; - private String[] mStrings; - protected final Object mLock; - private int mCurrentSting; - private int mMarkedString; - private int mMark; - private int mNext; + private StringReader current; + private String[] strings; + protected final Object finalLock; + private int currentSting; + private int markedString; + private int mark; + private int next; /** * Create a new string array reader. @@ -57,28 +59,28 @@ public class StringArrayReader extends StringReader { */ public StringArrayReader(final String[] pStrings) { super(""); - if (pStrings == null) { - throw new NullPointerException("strings == null"); - } - mLock = lock = pStrings; // NOTE: It's ok to sync on pStrings, as the + Validate.notNull(pStrings, "strings"); + + finalLock = lock = pStrings; // NOTE: It's ok to sync on pStrings, as the // reference can't change, only it's elements - mStrings = pStrings.clone(); // Defensive copy for content + strings = pStrings.clone(); // Defensive copy for content nextReader(); } protected final Reader nextReader() { - if (mCurrentSting >= mStrings.length) { - mCurrent = new EmptyReader(); + if (currentSting >= strings.length) { + current = new EmptyReader(); } else { - mCurrent = new StringReader(mStrings[mCurrentSting++]); + current = new StringReader(strings[currentSting++]); } - // NOTE: Reset mNext for every reader, and record marked reader in mark/reset methods! - mNext = 0; + + // NOTE: Reset next for every reader, and record marked reader in mark/reset methods! + next = 0; - return mCurrent; + return current; } /** @@ -87,15 +89,15 @@ public class StringArrayReader extends StringReader { * @throws IOException if the stream is closed */ protected final void ensureOpen() throws IOException { - if (mStrings == null) { + if (strings == null) { throw new IOException("Stream closed"); } } public void close() { super.close(); - mStrings = null; - mCurrent.close(); + strings = null; + current.close(); } public void mark(int pReadLimit) throws IOException { @@ -103,29 +105,29 @@ public class StringArrayReader extends StringReader { throw new IllegalArgumentException("Read limit < 0"); } - synchronized (mLock) { + synchronized (finalLock) { ensureOpen(); - mMark = mNext; - mMarkedString = mCurrentSting; + mark = next; + markedString = currentSting; - mCurrent.mark(pReadLimit); + current.mark(pReadLimit); } } public void reset() throws IOException { - synchronized (mLock) { + synchronized (finalLock) { ensureOpen(); - if (mCurrentSting != mMarkedString) { - mCurrentSting = mMarkedString - 1; + if (currentSting != markedString) { + currentSting = markedString - 1; nextReader(); - mCurrent.skip(mMark); + current.skip(mark); } else { - mCurrent.reset(); + current.reset(); } - mNext = mMark; + next = mark; } } @@ -134,49 +136,49 @@ public class StringArrayReader extends StringReader { } public int read() throws IOException { - synchronized (mLock) { - int read = mCurrent.read(); + synchronized (finalLock) { + int read = current.read(); - if (read < 0 && mCurrentSting < mStrings.length) { + if (read < 0 && currentSting < strings.length) { nextReader(); return read(); // In case of empty strings } - mNext++; + next++; return read; } } public int read(char pBuffer[], int pOffset, int pLength) throws IOException { - synchronized (mLock) { - int read = mCurrent.read(pBuffer, pOffset, pLength); + synchronized (finalLock) { + int read = current.read(pBuffer, pOffset, pLength); - if (read < 0 && mCurrentSting < mStrings.length) { + if (read < 0 && currentSting < strings.length) { nextReader(); return read(pBuffer, pOffset, pLength); // In case of empty strings } - mNext += read; + next += read; return read; } } public boolean ready() throws IOException { - return mCurrent.ready(); + return current.ready(); } public long skip(long pChars) throws IOException { - synchronized (mLock) { - long skipped = mCurrent.skip(pChars); + synchronized (finalLock) { + long skipped = current.skip(pChars); - if (skipped == 0 && mCurrentSting < mStrings.length) { + if (skipped == 0 && currentSting < strings.length) { nextReader(); return skip(pChars); } - mNext += skipped; + next += skipped; return skipped; } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/SubStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/SubStream.java index e611ba8f..d45d6b78 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/SubStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/SubStream.java @@ -43,8 +43,8 @@ import java.io.InputStream; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SubStream.java#2 $ */ public final class SubStream extends FilterInputStream { - private long mLeft; - private int mMarkLimit; + private long bytesLeft; + private int markLimit; /** * Creates a {@code SubStream} of the given {@code pStream}. @@ -54,7 +54,7 @@ public final class SubStream extends FilterInputStream { */ public SubStream(final InputStream pStream, final long pLength) { super(Validate.notNull(pStream, "stream")); - mLeft = pLength; + bytesLeft = pLength; } /** @@ -64,32 +64,32 @@ public final class SubStream extends FilterInputStream { @Override public void close() throws IOException { // NOTE: Do not close the underlying stream - while (mLeft > 0) { + while (bytesLeft > 0) { //noinspection ResultOfMethodCallIgnored - skip(mLeft); + skip(bytesLeft); } } @Override public int available() throws IOException { - return (int) Math.min(super.available(), mLeft); + return (int) Math.min(super.available(), bytesLeft); } @Override public void mark(int pReadLimit) { super.mark(pReadLimit);// This either succeeds or does nothing... - mMarkLimit = pReadLimit; + markLimit = pReadLimit; } @Override public void reset() throws IOException { super.reset();// This either succeeds or throws IOException - mLeft += mMarkLimit; + bytesLeft += markLimit; } @Override public int read() throws IOException { - if (mLeft-- <= 0) { + if (bytesLeft-- <= 0) { return -1; } return super.read(); @@ -102,12 +102,12 @@ public final class SubStream extends FilterInputStream { @Override public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (mLeft <= 0) { + if (bytesLeft <= 0) { return -1; } int read = super.read(pBytes, pOffset, (int) findMaxLen(pLength)); - mLeft = read < 0 ? 0 : mLeft - read; + bytesLeft = read < 0 ? 0 : bytesLeft - read; return read; } @@ -118,8 +118,8 @@ public final class SubStream extends FilterInputStream { * @return the maximum number of bytes to read */ private long findMaxLen(long pLength) { - if (mLeft < pLength) { - return (int) Math.max(mLeft, 0); + if (bytesLeft < pLength) { + return (int) Math.max(bytesLeft, 0); } else { return pLength; @@ -129,7 +129,7 @@ public final class SubStream extends FilterInputStream { @Override public long skip(long pLength) throws IOException { long skipped = super.skip(findMaxLen(pLength));// Skips 0 or more, never -1 - mLeft -= skipped; + bytesLeft -= skipped; return skipped; } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/Win32Lnk.java b/common/common-io/src/main/java/com/twelvemonkeys/io/Win32Lnk.java index d2e1191b..738243e1 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/Win32Lnk.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/Win32Lnk.java @@ -50,7 +50,8 @@ final class Win32Lnk extends File { (byte) 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'F' }; - private final File mTarget; + private final File target; + private static final int FLAG_ITEM_ID_LIST = 0x01; private static final int FLAG_FILE_LOC_INFO = 0x02; private static final int FLAG_DESC_STRING = 0x04; @@ -65,10 +66,10 @@ final class Win32Lnk extends File { File target = parse(this); if (target == this) { // NOTE: This is a workaround - // mTarget = this causes infinite loops in some methods + // target = this causes infinite loops in some methods target = new File(pPath); } - mTarget = target; + this.target = target; } Win32Lnk(final File pPath) throws IOException { @@ -336,24 +337,24 @@ final class Win32Lnk extends File { */ public File getTarget() { - return mTarget; + return target; } // java.io.File overrides below @Override public boolean isDirectory() { - return mTarget.isDirectory(); + return target.isDirectory(); } @Override public boolean canRead() { - return mTarget.canRead(); + return target.canRead(); } @Override public boolean canWrite() { - return mTarget.canWrite(); + return target.canWrite(); } // NOTE: equals is implemented using compareto == 0 @@ -362,7 +363,7 @@ final class Win32Lnk extends File { // TODO: Verify this // Probably not a good idea, as it IS NOT THE SAME file // It's probably better to not override - return mTarget.compareTo(pathname); + return target.compareTo(pathname); } */ @@ -375,7 +376,7 @@ final class Win32Lnk extends File { @Override public boolean exists() { - return mTarget.exists(); + return target.exists(); } // A .lnk may be absolute @@ -385,12 +386,12 @@ final class Win32Lnk extends File { // Theses should be resolved according to the API (for Unix). @Override public File getCanonicalFile() throws IOException { - return mTarget.getCanonicalFile(); + return target.getCanonicalFile(); } @Override public String getCanonicalPath() throws IOException { - return mTarget.getCanonicalPath(); + return target.getCanonicalPath(); } //public String getName() { @@ -402,47 +403,47 @@ final class Win32Lnk extends File { // public boolean isAbsolute() { @Override public boolean isFile() { - return mTarget.isFile(); + return target.isFile(); } @Override public boolean isHidden() { - return mTarget.isHidden(); + return target.isHidden(); } @Override public long lastModified() { - return mTarget.lastModified(); + return target.lastModified(); } @Override public long length() { - return mTarget.length(); + return target.length(); } @Override public String[] list() { - return mTarget.list(); + return target.list(); } @Override public String[] list(final FilenameFilter filter) { - return mTarget.list(filter); + return target.list(filter); } @Override public File[] listFiles() { - return Win32File.wrap(mTarget.listFiles()); + return Win32File.wrap(target.listFiles()); } @Override public File[] listFiles(final FileFilter filter) { - return Win32File.wrap(mTarget.listFiles(filter)); + return Win32File.wrap(target.listFiles(filter)); } @Override public File[] listFiles(final FilenameFilter filter) { - return Win32File.wrap(mTarget.listFiles(filter)); + return Win32File.wrap(target.listFiles(filter)); } // Makes no sense, does it? @@ -454,19 +455,19 @@ final class Win32Lnk extends File { @Override public boolean setLastModified(long time) { - return mTarget.setLastModified(time); + return target.setLastModified(time); } @Override public boolean setReadOnly() { - return mTarget.setReadOnly(); + return target.setReadOnly(); } @Override public String toString() { - if (mTarget.equals(this)) { + if (target.equals(this)) { return super.toString(); } - return super.toString() + " -> " + mTarget.toString(); + return super.toString() + " -> " + target.toString(); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java index 3aa4a41d..d3c0a1a7 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java @@ -51,10 +51,11 @@ import java.nio.CharBuffer; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java#2 $ */ public class WriterOutputStream extends OutputStream { - protected Writer mWriter; - final protected Decoder mDecoder; - final ByteArrayOutputStream mBufferStream = new FastByteArrayOutputStream(1024); - private volatile boolean mIsFlushing = false; // Ugly but critical... + protected Writer writer; + final protected Decoder decoder; + final ByteArrayOutputStream bufferStream = new FastByteArrayOutputStream(1024); + + private volatile boolean isFlushing = false; // Ugly but critical... private static final boolean NIO_AVAILABLE = isNIOAvailable(); @@ -71,8 +72,8 @@ public class WriterOutputStream extends OutputStream { } public WriterOutputStream(final Writer pWriter, final String pCharset) { - mWriter = pWriter; - mDecoder = getDecoder(pCharset); + writer = pWriter; + decoder = getDecoder(pCharset); } public WriterOutputStream(final Writer pWriter) { @@ -94,14 +95,14 @@ public class WriterOutputStream extends OutputStream { @Override public void close() throws IOException { flush(); - mWriter.close(); - mWriter = null; + writer.close(); + writer = null; } @Override public void flush() throws IOException { flushBuffer(); - mWriter.flush(); + writer.flush(); } @Override @@ -115,22 +116,22 @@ public class WriterOutputStream extends OutputStream { @Override public final void write(byte[] pBytes, int pOffset, int pLength) throws IOException { flushBuffer(); - mDecoder.decodeTo(mWriter, pBytes, pOffset, pLength); + decoder.decodeTo(writer, pBytes, pOffset, pLength); } @Override public final void write(int pByte) { // TODO: Is it possible to know if this is a good place in the stream to // flush? It might be in the middle of a multi-byte encoded character.. - mBufferStream.write(pByte); + bufferStream.write(pByte); } private void flushBuffer() throws IOException { - if (!mIsFlushing && mBufferStream.size() > 0) { - mIsFlushing = true; - mBufferStream.writeTo(this); // NOTE: Avoids cloning buffer array - mBufferStream.reset(); - mIsFlushing = false; + if (!isFlushing && bufferStream.size() > 0) { + isFlushing = true; + bufferStream.writeTo(this); // NOTE: Avoids cloning buffer array + bufferStream.reset(); + isFlushing = false; } } @@ -138,7 +139,7 @@ public class WriterOutputStream extends OutputStream { public static void main(String[] pArgs) throws IOException { int iterations = 1000000; - byte[] bytes = "åøæÅØÆ klashf lkash ljah lhaaklhghdfgu ksd".getBytes("UTF-8"); + byte[] bytes = "������ klashf lkash ljah lhaaklhghdfgu ksd".getBytes("UTF-8"); Decoder d; long start; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java index e02fba8a..661a2dcf 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java @@ -41,12 +41,12 @@ import java.io.InputStream; */ // TODO: Move to other package or make public abstract class AbstractRLEDecoder implements Decoder { - protected final byte[] mRow; - protected final int mWidth; - protected int mSrcX; - protected int mSrcY; - protected int mDstX; - protected int mDstY; + protected final byte[] row; + protected final int width; + protected int srcX; + protected int srcY; + protected int dstX; + protected int dstY; /** * Creates an RLEDecoder. As RLE encoded BMP's may contain x and y deltas, @@ -56,21 +56,21 @@ abstract class AbstractRLEDecoder implements Decoder { * @param pHeight heigth of the image */ AbstractRLEDecoder(int pWidth, int pHeight) { - mWidth = pWidth; - int bytesPerRow = mWidth; + width = pWidth; + int bytesPerRow = width; int mod = bytesPerRow % 4; if (mod != 0) { bytesPerRow += 4 - mod; } - mRow = new byte[bytesPerRow]; + row = new byte[bytesPerRow]; - mSrcX = 0; - mSrcY = pHeight - 1; + srcX = 0; + srcY = pHeight - 1; - mDstX = mSrcX; - mDstY = mSrcY; + dstX = srcX; + dstY = srcY; } /** @@ -95,26 +95,26 @@ abstract class AbstractRLEDecoder implements Decoder { public final int decode(InputStream pStream, byte[] pBuffer) throws IOException { int decoded = 0; - while (decoded < pBuffer.length && mDstY >= 0) { + while (decoded < pBuffer.length && dstY >= 0) { // NOTE: Decode only full rows, don't decode if y delta - if (mDstX == 0 && mSrcY == mDstY) { + if (dstX == 0 && srcY == dstY) { decodeRow(pStream); } - int length = Math.min(mRow.length - mDstX, pBuffer.length - decoded); - System.arraycopy(mRow, mDstX, pBuffer, decoded, length); - mDstX += length; + int length = Math.min(row.length - dstX, pBuffer.length - decoded); + System.arraycopy(row, dstX, pBuffer, decoded, length); + dstX += length; decoded += length; - if (mDstX == mRow.length) { - mDstX = 0; - mDstY--; + if (dstX == row.length) { + dstX = 0; + dstY--; // NOTE: If src Y is < dst Y, we have a delta, and have to fill the // gap with zero-bytes - if (mDstY > mSrcY) { - for (int i = 0; i < mRow.length; i++) { - mRow[i] = 0x00; + if (dstY > srcY) { + for (int i = 0; i < row.length; i++) { + row[i] = 0x00; } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java index d6799af2..38179949 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java @@ -61,9 +61,9 @@ public final class Base64Decoder implements Decoder { final static byte[] PEM_CONVERT_ARRAY; - private byte[] mDecodeBuffer = new byte[4]; - private ByteArrayOutputStream mWrapped; - private Object mWrappedObject; + private byte[] decodeBuffer = new byte[4]; + private ByteArrayOutputStream wrapped; + private Object wrappedObject; static { PEM_CONVERT_ARRAY = new byte[256]; @@ -116,8 +116,8 @@ public final class Base64Decoder implements Decoder { } } while (read == 10 || read == 13); - mDecodeBuffer[0] = (byte) read; - read = readFully(pInput, mDecodeBuffer, 1, pLength - 1); + decodeBuffer[0] = (byte) read; + read = readFully(pInput, decodeBuffer, 1, pLength - 1); if (read == -1) { return false; @@ -125,24 +125,24 @@ public final class Base64Decoder implements Decoder { int length = pLength; - if (length > 3 && mDecodeBuffer[3] == 61) { + if (length > 3 && decodeBuffer[3] == 61) { length = 3; } - if (length > 2 && mDecodeBuffer[2] == 61) { + if (length > 2 && decodeBuffer[2] == 61) { length = 2; } switch (length) { case 4: - byte3 = PEM_CONVERT_ARRAY[mDecodeBuffer[3] & 255]; + byte3 = PEM_CONVERT_ARRAY[decodeBuffer[3] & 255]; // fall through case 3: - byte2 = PEM_CONVERT_ARRAY[mDecodeBuffer[2] & 255]; + byte2 = PEM_CONVERT_ARRAY[decodeBuffer[2] & 255]; // fall through case 2: - byte1 = PEM_CONVERT_ARRAY[mDecodeBuffer[1] & 255]; - byte0 = PEM_CONVERT_ARRAY[mDecodeBuffer[0] & 255]; + byte1 = PEM_CONVERT_ARRAY[decodeBuffer[1] & 255]; + byte0 = PEM_CONVERT_ARRAY[decodeBuffer[0] & 255]; // fall through default: switch (length) { @@ -185,15 +185,15 @@ public final class Base64Decoder implements Decoder { } public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { - if (mWrappedObject != pBuffer) { + if (wrappedObject != pBuffer) { // NOTE: Array not cloned in FastByteArrayOutputStream - mWrapped = new FastByteArrayOutputStream(pBuffer); - mWrappedObject = pBuffer; + wrapped = new FastByteArrayOutputStream(pBuffer); + wrappedObject = pBuffer; } - mWrapped.reset(); // NOTE: This only resets count to 0 - decodeBuffer(pStream, mWrapped, pBuffer.length); + wrapped.reset(); // NOTE: This only resets count to 0 + decodeBuffer(pStream, wrapped, pBuffer.length); - return mWrapped.size(); + return wrapped.size(); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java index 45ab9455..7acf88af 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java @@ -31,7 +31,7 @@ package com.twelvemonkeys.io.enc; import java.io.IOException; /** - * Thrown by {@code Decoder}s when encoded data can not be decocded. + * Thrown by {@code Decoder}s when encoded data can not be decoded. *

* * @author Harald Kuhr diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java index 44752512..45219c91 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java @@ -47,13 +47,13 @@ import java.io.IOException; public interface Decoder { /** - * Decodes up to {@code pBuffer.length} bytes from the given inputstream, + * Decodes up to {@code pBuffer.length} bytes from the given input stream, * into the given buffer. * - * @param pStream the inputstream to decode data from + * @param pStream the input stream to decode data from * @param pBuffer buffer to store the read data * - * @return the total number of bytes read into the buffer, or {@code -1} + * @return the total number of bytes read into the buffer, or {@code 0} * if there is no more data because the end of the stream has been reached. * * @throws DecodeException if encoded data is corrupt diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java index 5cd672bf..318ccb86 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java @@ -44,10 +44,10 @@ import java.io.FilterInputStream; */ public final class DecoderStream extends FilterInputStream { - protected int mBufferPos; - protected int mBufferLimit; - protected final byte[] mBuffer; - protected final Decoder mDecoder; + protected int bufferPos; + protected int bufferLimit; + protected final byte[] buffer; + protected final Decoder decoder; /** * Creates a new decoder stream and chains it to the @@ -76,26 +76,26 @@ public final class DecoderStream extends FilterInputStream { */ public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) { super(pStream); - mDecoder = pDecoder; - mBuffer = new byte[pBufferSize]; - mBufferPos = 0; - mBufferLimit = 0; + decoder = pDecoder; + buffer = new byte[pBufferSize]; + bufferPos = 0; + bufferLimit = 0; } public int available() throws IOException { - return mBufferLimit - mBufferPos + super.available(); + return bufferLimit - bufferPos + super.available(); } public int read() throws IOException { - if (mBufferPos == mBufferLimit) { - mBufferLimit = fill(); + if (bufferPos == bufferLimit) { + bufferLimit = fill(); } - if (mBufferLimit < 0) { + if (bufferLimit < 0) { return -1; } - return mBuffer[mBufferPos++] & 0xff; + return buffer[bufferPos++] & 0xff; } public int read(final byte pBytes[]) throws IOException { @@ -115,7 +115,7 @@ public final class DecoderStream extends FilterInputStream { } // End of file? - if ((mBufferLimit - mBufferPos) < 0) { + if ((bufferLimit - bufferPos) < 0) { return -1; } @@ -124,26 +124,26 @@ public final class DecoderStream extends FilterInputStream { int off = pOffset; while (pLength > count) { - int avail = mBufferLimit - mBufferPos; + int avail = bufferLimit - bufferPos; if (avail <= 0) { - mBufferLimit = fill(); + bufferLimit = fill(); - if (mBufferLimit < 0) { + if (bufferLimit < 0) { break; } } // Copy as many bytes as possible int dstLen = Math.min(pLength - count, avail); - System.arraycopy(mBuffer, mBufferPos, pBytes, off, dstLen); + System.arraycopy(buffer, bufferPos, pBytes, off, dstLen); - mBufferPos += dstLen; + bufferPos += dstLen; // Update offset (rest) off += dstLen; - // Inrease count + // Increase count count += dstLen; } @@ -152,7 +152,7 @@ public final class DecoderStream extends FilterInputStream { public long skip(final long pLength) throws IOException { // End of file? - if (mBufferLimit - mBufferPos < 0) { + if (bufferLimit - bufferPos < 0) { return 0; } @@ -160,12 +160,12 @@ public final class DecoderStream extends FilterInputStream { long total = 0; while (total < pLength) { - int avail = mBufferLimit - mBufferPos; + int avail = bufferLimit - bufferPos; if (avail == 0) { - mBufferLimit = fill(); + bufferLimit = fill(); - if (mBufferLimit < 0) { + if (bufferLimit < 0) { break; } } @@ -174,7 +174,7 @@ public final class DecoderStream extends FilterInputStream { // an int, so the cast is safe int skipped = (int) Math.min(pLength - total, avail); - mBufferPos += skipped; // Just skip these bytes + bufferPos += skipped; // Just skip these bytes total += skipped; } @@ -190,19 +190,19 @@ public final class DecoderStream extends FilterInputStream { * @throws IOException if an I/O error occurs */ protected int fill() throws IOException { - int read = mDecoder.decode(in, mBuffer); + int read = decoder.decode(in, buffer); // TODO: Enforce this in test case, leave here to aid debugging - if (read > mBuffer.length) { + if (read > buffer.length) { throw new AssertionError( String.format( "Decode beyond buffer (%d): %d (using %s decoder)", - mBuffer.length, read, mDecoder.getClass().getName() + buffer.length, read, decoder.getClass().getName() ) ); } - mBufferPos = 0; + bufferPos = 0; if (read == 0) { return -1; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java index d142bfc5..c6f45bc7 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java @@ -57,8 +57,7 @@ public interface Encoder { * * @throws java.io.IOException if an I/O error occurs */ - void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) - throws IOException; + void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException; //TODO: int requiredBufferSize(): -1 == any, otherwise, use this buffer size // void flush()? diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java index de8a7b5b..0a4f0fce 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java @@ -44,11 +44,11 @@ import java.io.IOException; */ public final class EncoderStream extends FilterOutputStream { - protected final Encoder mEncoder; - private final boolean mFlushOnWrite; + protected final Encoder encoder; + private final boolean flushOnWrite; - protected int mBufferPos; - protected final byte[] mBuffer; + protected int bufferPos; + protected final byte[] buffer; /** * Creates an output stream filter built on top of the specified @@ -73,11 +73,11 @@ public final class EncoderStream extends FilterOutputStream { public EncoderStream(final OutputStream pStream, final Encoder pEncoder, final boolean pFlushOnWrite) { super(pStream); - mEncoder = pEncoder; - mFlushOnWrite = pFlushOnWrite; + encoder = pEncoder; + flushOnWrite = pFlushOnWrite; - mBuffer = new byte[1024]; - mBufferPos = 0; + buffer = new byte[1024]; + bufferPos = 0; } public void close() throws IOException { @@ -91,12 +91,12 @@ public final class EncoderStream extends FilterOutputStream { } private void encodeBuffer() throws IOException { - if (mBufferPos != 0) { + if (bufferPos != 0) { // Make sure all remaining data in buffer is written to the stream - mEncoder.encode(out, mBuffer, 0, mBufferPos); + encoder.encode(out, buffer, 0, bufferPos); // Reset buffer - mBufferPos = 0; + bufferPos = 0; } } @@ -109,27 +109,25 @@ public final class EncoderStream extends FilterOutputStream { // that the encoder can't buffer. In that case, the encoder should probably // tell the EncoderStream how large buffer it prefers... public void write(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (!mFlushOnWrite && mBufferPos + pLength < mBuffer.length) { + if (!flushOnWrite && bufferPos + pLength < buffer.length) { // Buffer data - System.arraycopy(pBytes, pOffset, mBuffer, mBufferPos, pLength); - mBufferPos += pLength; + System.arraycopy(pBytes, pOffset, buffer, bufferPos, pLength); + bufferPos += pLength; } else { // Encode data already in the buffer - if (mBufferPos != 0) { - encodeBuffer(); - } + encodeBuffer(); // Encode rest without buffering - mEncoder.encode(out, pBytes, pOffset, pLength); + encoder.encode(out, pBytes, pOffset, pLength); } } public void write(final int pByte) throws IOException { - if (mBufferPos >= mBuffer.length - 1) { - encodeBuffer(); // Resets mBufferPos to 0 + if (bufferPos >= buffer.length - 1) { + encodeBuffer(); // Resets bufferPos to 0 } - mBuffer[mBufferPos++] = (byte) pByte; + buffer[bufferPos++] = (byte) pByte; } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java index ef4efe1b..a7b1dde0 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java @@ -46,11 +46,11 @@ import java.io.EOFException; */ public final class PackBits16Decoder implements Decoder { // TODO: Refactor this into an option for the PackBitsDecoder? - private final boolean mDisableNoop; + private final boolean disableNoop; - private int mLeftOfRun; - private boolean mSplitRun; - private boolean mEOF; + private int leftOfRun; + private boolean splitRun; + private boolean reachedEOF; /** * Creates a {@code PackBitsDecoder}. @@ -71,7 +71,7 @@ public final class PackBits16Decoder implements Decoder { * @param pDisableNoop {@code true} if {@code -128} should be treated as a compressed run, and not a no-op */ public PackBits16Decoder(final boolean pDisableNoop) { - mDisableNoop = pDisableNoop; + disableNoop = pDisableNoop; } /** @@ -85,7 +85,7 @@ public final class PackBits16Decoder implements Decoder { * @throws java.io.IOException */ public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { - if (mEOF) { + if (reachedEOF) { return -1; } @@ -95,16 +95,16 @@ public final class PackBits16Decoder implements Decoder { while (read < max) { int n; - if (mSplitRun) { + if (splitRun) { // Continue run - n = mLeftOfRun; - mSplitRun = false; + n = leftOfRun; + splitRun = false; } else { // Start new run int b = pStream.read(); if (b < 0) { - mEOF = true; + reachedEOF = true; break; } n = (byte) b; @@ -112,13 +112,13 @@ public final class PackBits16Decoder implements Decoder { // Split run at or before max if (n >= 0 && 2 * (n + 1) + read > max) { - mLeftOfRun = n; - mSplitRun = true; + leftOfRun = n; + splitRun = true; break; } else if (n < 0 && 2 * (-n + 1) + read > max) { - mLeftOfRun = n; - mSplitRun = true; + leftOfRun = n; + splitRun = true; break; } @@ -130,7 +130,7 @@ public final class PackBits16Decoder implements Decoder { read += len; } // Allow -128 for compatibility, see above - else if (mDisableNoop || n != -128) { + else if (disableNoop || n != -128) { // Replicate the next short -n + 1 times byte value1 = readByte(pStream); byte value2 = readByte(pStream); diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java index b50556f1..4140f298 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java @@ -28,9 +28,9 @@ package com.twelvemonkeys.io.enc; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -import java.io.EOFException; /** * Decoder implementation for Apple PackBits run-length encoding. @@ -63,11 +63,13 @@ import java.io.EOFException; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java#1 $ */ public final class PackBitsDecoder implements Decoder { - private final boolean mDisableNoop; + // TODO: Look at ICNSImageReader#unpackbits... What is this weirdness? - private int mLeftOfRun; - private boolean mSplitRun; - private boolean mEOF; + private final boolean disableNoop; + + private int leftOfRun; + private boolean splitRun; + private boolean reachedEOF; /** Creates a {@code PackBitsDecoder}. */ public PackBitsDecoder() { @@ -78,29 +80,26 @@ public final class PackBitsDecoder implements Decoder { * Creates a {@code PackBitsDecoder}, with optional compatibility mode. *

* As some implementations of PackBits-like encoders treat {@code -128} as length of - * a compressed run, instead of a no-op, it's possible to disable no-ops - * for compatibility. - * Should be used with caution, even though, most known encoders never write - * no-ops in the compressed streams. + * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. + * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. * * @param pDisableNoop {@code true} if {@code -128} should be treated as a compressed run, and not a no-op */ public PackBitsDecoder(final boolean pDisableNoop) { - mDisableNoop = pDisableNoop; + disableNoop = pDisableNoop; } /** * Decodes bytes from the given input stream, to the given buffer. * * @param pStream the stream to decode from - * @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) - * bytes long + * @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long * @return The number of bytes decoded * - * @throws IOException + * @throws java.io.IOException */ public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { - if (mEOF) { + if (reachedEOF) { return -1; } @@ -111,16 +110,16 @@ public final class PackBitsDecoder implements Decoder { while (read < max) { int n; - if (mSplitRun) { + if (splitRun) { // Continue run - n = mLeftOfRun; - mSplitRun = false; + n = leftOfRun; + splitRun = false; } else { // Start new run int b = pStream.read(); if (b < 0) { - mEOF = true; + reachedEOF = true; break; } n = (byte) b; @@ -128,13 +127,13 @@ public final class PackBitsDecoder implements Decoder { // Split run at or before max if (n >= 0 && n + 1 + read > max) { - mLeftOfRun = n; - mSplitRun = true; + leftOfRun = n; + splitRun = true; break; } else if (n < 0 && -n + 1 + read > max) { - mLeftOfRun = n; - mSplitRun = true; + leftOfRun = n; + splitRun = true; break; } @@ -146,7 +145,7 @@ public final class PackBitsDecoder implements Decoder { read += n + 1; } // Allow -128 for compatibility, see above - else if (mDisableNoop || n != -128) { + else if (disableNoop || n != -128) { // Replicate the next byte -n + 1 times byte value = readByte(pStream); @@ -164,7 +163,7 @@ public final class PackBitsDecoder implements Decoder { return read; } - private static byte readByte(final InputStream pStream) throws IOException { + static byte readByte(final InputStream pStream) throws IOException { int read = pStream.read(); if (read < 0) { @@ -174,21 +173,21 @@ public final class PackBitsDecoder implements Decoder { return (byte) read; } - private static void readFully(final InputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + static void readFully(final InputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { if (pLength < 0) { - throw new IndexOutOfBoundsException(); + throw new IndexOutOfBoundsException(String.format("Negative length: %d", pLength)); } - int read = 0; + int total = 0; - while (read < pLength) { - int count = pStream.read(pBuffer, pOffset + read, pLength - read); + while (total < pLength) { + int count = pStream.read(pBuffer, pOffset + total, pLength - total); if (count < 0) { throw new EOFException("Unexpected end of PackBits stream"); } - read += count; + total += count; } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java index 6e775984..cfc80a31 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java @@ -63,7 +63,7 @@ import java.io.IOException; */ public final class PackBitsEncoder implements Encoder { - final private byte[] mBuffer = new byte[128]; + final private byte[] buffer = new byte[128]; /** * Creates a {@code PackBitsEncoder}. @@ -101,17 +101,17 @@ public final class PackBitsEncoder implements Encoder { run = 0; while ((run < 128 && ((offset < max && pBuffer[offset] != pBuffer[offset + 1]) || (offset < maxMinus1 && pBuffer[offset] != pBuffer[offset + 2])))) { - mBuffer[run++] = pBuffer[offset++]; + buffer[run++] = pBuffer[offset++]; } // If last byte, include it in literal run, if space if (offset == max && run > 0 && run < 128) { - mBuffer[run++] = pBuffer[offset++]; + buffer[run++] = pBuffer[offset++]; } if (run > 0) { pStream.write(run - 1); - pStream.write(mBuffer, 0, run); + pStream.write(buffer, 0, run); } // If last byte, and not space, start new literal run diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java index 8a07b99d..6ea57d85 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java @@ -49,7 +49,7 @@ final class RLE4Decoder extends AbstractRLEDecoder { int deltaX = 0; int deltaY = 0; - while (mSrcY >= 0) { + while (srcY >= 0) { int byte1 = pInput.read(); int byte2 = checkEOF(pInput.read()); @@ -58,20 +58,20 @@ final class RLE4Decoder extends AbstractRLEDecoder { case 0x00: // End of line // NOTE: Some BMPs have double EOLs.. - if (mSrcX != 0) { - mSrcX = mRow.length; + if (srcX != 0) { + srcX = row.length; } break; case 0x01: // End of bitmap - mSrcX = mRow.length; - mSrcY = 0; + srcX = row.length; + srcY = 0; break; case 0x02: // Delta - deltaX = mSrcX + pInput.read(); - deltaY = mSrcY - checkEOF(pInput.read()); - mSrcX = mRow.length; + deltaX = srcX + pInput.read(); + deltaY = srcY - checkEOF(pInput.read()); + srcX = row.length; break; default: // Absolute mode @@ -82,13 +82,13 @@ final class RLE4Decoder extends AbstractRLEDecoder { boolean paddingByte = (((byte2 + 1) / 2) % 2) != 0; while (byte2 > 1) { int packed = checkEOF(pInput.read()); - mRow[mSrcX++] = (byte) packed; + row[srcX++] = (byte) packed; byte2 -= 2; } if (byte2 == 1) { // TODO: Half byte alignment? Seems to be ok... int packed = checkEOF(pInput.read()); - mRow[mSrcX++] = (byte) (packed & 0xf0); + row[srcX++] = (byte) (packed & 0xf0); } if (paddingByte) { checkEOF(pInput.read()); @@ -100,24 +100,24 @@ final class RLE4Decoder extends AbstractRLEDecoder { // Encoded mode // Replicate the two samples in byte2 as many times as byte1 says while (byte1 > 1) { - mRow[mSrcX++] = (byte) byte2; + row[srcX++] = (byte) byte2; byte1 -= 2; } if (byte1 == 1) { // TODO: Half byte alignment? Seems to be ok... - mRow[mSrcX++] = (byte) (byte2 & 0xf0); + row[srcX++] = (byte) (byte2 & 0xf0); } } // If we're done with a complete row, copy the data - if (mSrcX == mRow.length) { + if (srcX == row.length) { // Move to new position, either absolute (delta) or next line if (deltaX != 0 || deltaY != 0) { - mSrcX = (deltaX + 1) / 2; + srcX = (deltaX + 1) / 2; - if (deltaY > mSrcY) { - mSrcY = deltaY; + if (deltaY > srcY) { + srcY = deltaY; break; } @@ -125,8 +125,8 @@ final class RLE4Decoder extends AbstractRLEDecoder { deltaY = 0; } else { - mSrcX = 0; - mSrcY--; + srcX = 0; + srcY--; break; } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java index f2bf78cd..bc75dd2e 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java @@ -49,7 +49,7 @@ final class RLE8Decoder extends AbstractRLEDecoder { int deltaX = 0; int deltaY = 0; - while (mSrcY >= 0) { + while (srcY >= 0) { int byte1 = pInput.read(); int byte2 = checkEOF(pInput.read()); @@ -58,27 +58,27 @@ final class RLE8Decoder extends AbstractRLEDecoder { case 0x00: // End of line // NOTE: Some BMPs have double EOLs.. - if (mSrcX != 0) { - mSrcX = mRow.length; + if (srcX != 0) { + srcX = row.length; } break; case 0x01: // End of bitmap - mSrcX = mRow.length; - mSrcY = 0; + srcX = row.length; + srcY = 0; break; case 0x02: // Delta - deltaX = mSrcX + pInput.read(); - deltaY = mSrcY - checkEOF(pInput.read()); - mSrcX = mRow.length; + deltaX = srcX + pInput.read(); + deltaY = srcY - checkEOF(pInput.read()); + srcX = row.length; break; default: // Absolute mode // Copy the next byte2 (3..255) bytes from file to output boolean paddingByte = (byte2 % 2) != 0; while (byte2-- > 0) { - mRow[mSrcX++] = (byte) checkEOF(pInput.read()); + row[srcX++] = (byte) checkEOF(pInput.read()); } if (paddingByte) { checkEOF(pInput.read()); @@ -90,26 +90,26 @@ final class RLE8Decoder extends AbstractRLEDecoder { // Replicate byte2 as many times as byte1 says byte value = (byte) byte2; while (byte1-- > 0) { - mRow[mSrcX++] = value; + row[srcX++] = value; } } // If we're done with a complete row, copy the data - if (mSrcX == mRow.length) { + if (srcX == row.length) { // Move to new position, either absolute (delta) or next line if (deltaX != 0 || deltaY != 0) { - mSrcX = deltaX; - if (deltaY != mSrcY) { - mSrcY = deltaY; + srcX = deltaX; + if (deltaY != srcY) { + srcY = deltaY; break; } deltaX = 0; deltaY = 0; } else { - mSrcX = 0; - mSrcY--; + srcX = 0; + srcY--; break; } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java index 952e6d6e..8dcffcc9 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java @@ -54,41 +54,44 @@ public final class CompoundDocument { // TODO: Write support... // TODO: Properties: http://support.microsoft.com/kb/186898 - private static final byte[] MAGIC = new byte[]{ + static final byte[] MAGIC = new byte[]{ (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1, }; - public static final int HEADER_SIZE = 512; - private final DataInput mInput; - - private UUID mUID; - - private int mSectorSize; - private int mShortSectorSize; - - private int mDirectorySId; - - private int mMinStreamSize; - - private int mShortSATSID; - private int mShortSATSize; - - // Master Sector Allocation Table - private int[] mMasterSAT; - private int[] mSAT; - private int[] mShortSAT; - - private Entry mRootEntry; - private SIdChain mShortStreamSIdChain; - private SIdChain mDirectorySIdChain; - - private static final int END_OF_CHAIN_SID = -2; private static final int FREE_SID = -1; + private static final int END_OF_CHAIN_SID = -2; + private static final int SAT_SECTOR_SID = -3; // Sector used by SAT + private static final int MSAT_SECTOR_SID = -4; // Sector used my Master SAT + + public static final int HEADER_SIZE = 512; /** The epoch offset of CompoundDocument time stamps */ public final static long EPOCH_OFFSET = -11644477200000L; + private final DataInput input; + + private UUID uUID; + + private int sectorSize; + private int shortSectorSize; + + private int directorySId; + + private int minStreamSize; + + private int shortSATSId; + private int shortSATSize; + + // Master Sector Allocation Table + private int[] masterSAT; + private int[] SAT; + private int[] shortSAT; + + private Entry rootEntry; + private SIdChain shortStreamSIdChain; + private SIdChain directorySIdChain; + /** * Creates a (for now) read only {@code CompoundDocument}. * @@ -97,7 +100,7 @@ public final class CompoundDocument { * @throws IOException if an I/O exception occurs while reading the header */ public CompoundDocument(final File pFile) throws IOException { - mInput = new LittleEndianRandomAccessFile(FileUtil.resolve(pFile), "r"); + input = new LittleEndianRandomAccessFile(FileUtil.resolve(pFile), "r"); // TODO: Might be better to read header on first read operation?! // OTOH: It's also good to be fail-fast, so at least we should make @@ -118,7 +121,7 @@ public final class CompoundDocument { // For testing only, consider exposing later CompoundDocument(final SeekableInputStream pInput) throws IOException { - mInput = new SeekableLittleEndianDataInputStream(pInput); + input = new SeekableLittleEndianDataInputStream(pInput); // TODO: Might be better to read header on first read operation?! // OTOH: It's also good to be fail-fast, so at least we should make @@ -134,7 +137,7 @@ public final class CompoundDocument { * @throws IOException if an I/O exception occurs while reading the header */ public CompoundDocument(final ImageInputStream pInput) throws IOException { - mInput = pInput; + input = pInput; // TODO: Might be better to read header on first read operation?! // OTOH: It's also good to be fail-fast, so at least we should make @@ -210,74 +213,81 @@ public final class CompoundDocument { } private void readHeader() throws IOException { - if (mMasterSAT != null) { + if (masterSAT != null) { return; } - if (!canRead(mInput, false)) { + if (!canRead(input, false)) { throw new CorruptDocumentException("Not an OLE 2 Compound Document"); } // UID (seems to be all 0s) - mUID = new UUID(mInput.readLong(), mInput.readLong()); + uUID = new UUID(input.readLong(), input.readLong()); +// System.out.println("uUID: " + uUID); - /*int version = */mInput.readUnsignedShort(); - //System.out.println("version: " + version); - /*int revision = */mInput.readUnsignedShort(); - //System.out.println("revision: " + revision); + // int version = + input.readUnsignedShort(); +// System.out.println("version: " + version); + // int revision = + input.readUnsignedShort(); +// System.out.println("revision: " + revision); - int byteOrder = mInput.readUnsignedShort(); - if (byteOrder != 0xfffe) { - // Reversed, as I'm allready reading little-endian + int byteOrder = input.readUnsignedShort(); +// System.out.printf("byteOrder: 0x%04x\n", byteOrder); + if (byteOrder == 0xffff) { throw new CorruptDocumentException("Cannot read big endian OLE 2 Compound Documents"); } + else if (byteOrder != 0xfffe) { + // Reversed, as I'm already reading little-endian + throw new CorruptDocumentException(String.format("Unknown byte order marker: 0x%04x, expected 0xfffe or 0xffff", byteOrder)); + } - mSectorSize = 1 << mInput.readUnsignedShort(); - //System.out.println("sectorSize: " + mSectorSize + " bytes"); - mShortSectorSize = 1 << mInput.readUnsignedShort(); - //System.out.println("shortSectorSize: " + mShortSectorSize + " bytes"); + sectorSize = 1 << input.readUnsignedShort(); +// System.out.println("sectorSize: " + sectorSize + " bytes"); + shortSectorSize = 1 << input.readUnsignedShort(); +// System.out.println("shortSectorSize: " + shortSectorSize + " bytes"); // Reserved - if (mInput.skipBytes(10) != 10) { + if (skipBytesFully(10) != 10) { throw new CorruptDocumentException(); } - int SATSize = mInput.readInt(); - //System.out.println("normalSATSize: " + mSATSize); + int SATSize = input.readInt(); +// System.out.println("normalSATSize: " + SATSize); - mDirectorySId = mInput.readInt(); - //System.out.println("directorySId: " + mDirectorySId); + directorySId = input.readInt(); +// System.out.println("directorySId: " + directorySId); // Reserved - if (mInput.skipBytes(4) != 4) { + if (skipBytesFully(4) != 4) { throw new CorruptDocumentException(); } - mMinStreamSize = mInput.readInt(); - //System.out.println("minStreamSize: " + mMinStreamSize + " bytes"); + minStreamSize = input.readInt(); +// System.out.println("minStreamSize: " + minStreamSize + " bytes"); - mShortSATSID = mInput.readInt(); - //System.out.println("shortSATSID: " + mShortSATSID); - mShortSATSize = mInput.readInt(); - //System.out.println("shortSATSize: " + mShortSATSize); - int masterSATSId = mInput.readInt(); - //System.out.println("masterSATSId: " + mMasterSATSID); - int masterSATSize = mInput.readInt(); - //System.out.println("masterSATSize: " + mMasterSATSize); + shortSATSId = input.readInt(); +// System.out.println("shortSATSId: " + shortSATSId); + shortSATSize = input.readInt(); +// System.out.println("shortSATSize: " + shortSATSize); + int masterSATSId = input.readInt(); +// System.out.println("masterSATSId: " + masterSATSId); + int masterSATSize = input.readInt(); +// System.out.println("masterSATSize: " + masterSATSize); // Read masterSAT: 436 bytes, containing up to 109 SIDs //System.out.println("MSAT:"); - mMasterSAT = new int[SATSize]; + masterSAT = new int[SATSize]; final int headerSIds = Math.min(SATSize, 109); for (int i = 0; i < headerSIds; i++) { - mMasterSAT[i] = mInput.readInt(); - //System.out.println("\tSID(" + i + "): " + mMasterSAT[i]); + masterSAT[i] = input.readInt(); + //System.out.println("\tSID(" + i + "): " + masterSAT[i]); } if (masterSATSId == END_OF_CHAIN_SID) { // End of chain int freeSIdLength = 436 - (SATSize * 4); - if (mInput.skipBytes(freeSIdLength) != freeSIdLength) { + if (skipBytesFully(freeSIdLength) != freeSIdLength) { throw new CorruptDocumentException(); } } @@ -288,17 +298,17 @@ public final class CompoundDocument { int index = headerSIds; for (int i = 0; i < masterSATSize; i++) { for (int j = 0; j < 127; j++) { - int sid = mInput.readInt(); + int sid = input.readInt(); switch (sid) { case FREE_SID:// Free break; default: - mMasterSAT[index++] = sid; + masterSAT[index++] = sid; break; } } - int next = mInput.readInt(); + int next = input.readInt(); if (next == END_OF_CHAIN_SID) {// End of chain break; } @@ -308,38 +318,53 @@ public final class CompoundDocument { } } + private int skipBytesFully(final int n) throws IOException { + int toSkip = n; + + while (toSkip > 0) { + int skipped = input.skipBytes(n); + if (skipped <= 0) { + break; + } + + toSkip -= skipped; + } + + return n - toSkip; + } + private void readSAT() throws IOException { - if (mSAT != null) { + if (SAT != null) { return; } - final int intsPerSector = mSectorSize / 4; + final int intsPerSector = sectorSize / 4; // Read the Sector Allocation Table - mSAT = new int[mMasterSAT.length * intsPerSector]; + SAT = new int[masterSAT.length * intsPerSector]; - for (int i = 0; i < mMasterSAT.length; i++) { - seekToSId(mMasterSAT[i], FREE_SID); + for (int i = 0; i < masterSAT.length; i++) { + seekToSId(masterSAT[i], FREE_SID); for (int j = 0; j < intsPerSector; j++) { - int nextSID = mInput.readInt(); + int nextSID = input.readInt(); int index = (j + (i * intsPerSector)); - mSAT[index] = nextSID; + SAT[index] = nextSID; } } // Read the short-stream Sector Allocation Table - SIdChain chain = getSIdChain(mShortSATSID, FREE_SID); - mShortSAT = new int[mShortSATSize * intsPerSector]; - for (int i = 0; i < mShortSATSize; i++) { + SIdChain chain = getSIdChain(shortSATSId, FREE_SID); + shortSAT = new int[shortSATSize * intsPerSector]; + for (int i = 0; i < shortSATSize; i++) { seekToSId(chain.get(i), FREE_SID); for (int j = 0; j < intsPerSector; j++) { - int nextSID = mInput.readInt(); + int nextSID = input.readInt(); int index = (j + (i * intsPerSector)); - mShortSAT[index] = nextSID; + shortSAT[index] = nextSID; } } } @@ -355,7 +380,7 @@ public final class CompoundDocument { private SIdChain getSIdChain(final int pSId, final long pStreamSize) throws IOException { SIdChain chain = new SIdChain(); - int[] sat = isShortStream(pStreamSize) ? mShortSAT : mSAT; + int[] sat = isShortStream(pStreamSize) ? shortSAT : SAT; int sid = pSId; while (sid != END_OF_CHAIN_SID && sid != FREE_SID) { @@ -367,7 +392,7 @@ public final class CompoundDocument { } private boolean isShortStream(final long pStreamSize) { - return pStreamSize != FREE_SID && pStreamSize < mMinStreamSize; + return pStreamSize != FREE_SID && pStreamSize < minStreamSize; } /** @@ -381,70 +406,76 @@ public final class CompoundDocument { long pos; if (isShortStream(pStreamSize)) { - // The short-stream is not continouos... + // The short stream is not continuous... Entry root = getRootEntry(); - if (mShortStreamSIdChain == null) { - mShortStreamSIdChain = getSIdChain(root.startSId, root.streamSize); + if (shortStreamSIdChain == null) { + shortStreamSIdChain = getSIdChain(root.startSId, root.streamSize); } - - int shortPerStd = mSectorSize / mShortSectorSize; - int offset = pSId / shortPerStd; - int shortOffset = pSId - (offset * shortPerStd); + +// System.err.println("pSId: " + pSId); + int shortPerSId = sectorSize / shortSectorSize; +// System.err.println("shortPerSId: " + shortPerSId); + int offset = pSId / shortPerSId; +// System.err.println("offset: " + offset); + int shortOffset = pSId - (offset * shortPerSId); +// System.err.println("shortOffset: " + shortOffset); +// System.err.println("shortStreamSIdChain.offset: " + shortStreamSIdChain.get(offset)); pos = HEADER_SIZE - + (mShortStreamSIdChain.get(offset) * (long) mSectorSize) - + (shortOffset * (long) mShortSectorSize); + + (shortStreamSIdChain.get(offset) * (long) sectorSize) + + (shortOffset * (long) shortSectorSize); +// System.err.println("pos: " + pos); } else { - pos = HEADER_SIZE + pSId * (long) mSectorSize; + pos = HEADER_SIZE + pSId * (long) sectorSize; } - if (mInput instanceof LittleEndianRandomAccessFile) { - ((LittleEndianRandomAccessFile) mInput).seek(pos); + if (input instanceof LittleEndianRandomAccessFile) { + ((LittleEndianRandomAccessFile) input).seek(pos); } - else if (mInput instanceof ImageInputStream) { - ((ImageInputStream) mInput).seek(pos); + else if (input instanceof ImageInputStream) { + ((ImageInputStream) input).seek(pos); } else { - ((SeekableLittleEndianDataInputStream) mInput).seek(pos); + ((SeekableLittleEndianDataInputStream) input).seek(pos); } } private void seekToDId(final int pDId) throws IOException { - if (mDirectorySIdChain == null) { - mDirectorySIdChain = getSIdChain(mDirectorySId, FREE_SID); + if (directorySIdChain == null) { + directorySIdChain = getSIdChain(directorySId, FREE_SID); } - int dIdsPerSId = mSectorSize / Entry.LENGTH; + int dIdsPerSId = sectorSize / Entry.LENGTH; int sIdOffset = pDId / dIdsPerSId; int dIdOffset = pDId - (sIdOffset * dIdsPerSId); - int sId = mDirectorySIdChain.get(sIdOffset); + int sId = directorySIdChain.get(sIdOffset); seekToSId(sId, FREE_SID); - if (mInput instanceof LittleEndianRandomAccessFile) { - LittleEndianRandomAccessFile input = (LittleEndianRandomAccessFile) mInput; + if (input instanceof LittleEndianRandomAccessFile) { + LittleEndianRandomAccessFile input = (LittleEndianRandomAccessFile) this.input; input.seek(input.getFilePointer() + dIdOffset * Entry.LENGTH); } - else if (mInput instanceof ImageInputStream) { - ImageInputStream input = (ImageInputStream) mInput; + else if (input instanceof ImageInputStream) { + ImageInputStream input = (ImageInputStream) this.input; input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); } else { - SeekableLittleEndianDataInputStream input = (SeekableLittleEndianDataInputStream) mInput; + SeekableLittleEndianDataInputStream input = (SeekableLittleEndianDataInputStream) this.input; input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); } } - SeekableInputStream getInputStreamForSId(final int pStreamId, final int pStreamSize) throws IOException { + SeekableInputStream getInputStreamForSId(final int pStreamId, final int pStreamSize) throws IOException { SIdChain chain = getSIdChain(pStreamId, pStreamSize); // TODO: Detach? Means, we have to copy to a byte buffer, or keep track of // positions, and seek back and forth (would be cool, but difficult).. - int sectorSize = pStreamSize < mMinStreamSize ? mShortSectorSize : mSectorSize; + int sectorSize = pStreamSize < minStreamSize ? shortSectorSize : this.sectorSize; - return new Stream(chain, pStreamSize, sectorSize, this); + return new MemoryCacheSeekableStream(new Stream(chain, pStreamSize, sectorSize, this)); } private InputStream getDirectoryStreamForDId(final int pDirectoryId) throws IOException { @@ -453,7 +484,7 @@ public final class CompoundDocument { byte[] bytes = new byte[Entry.LENGTH]; seekToDId(pDirectoryId); - mInput.readFully(bytes); + input.readFully(bytes); return new ByteArrayInputStream(bytes); } @@ -462,8 +493,8 @@ public final class CompoundDocument { Entry entry = Entry.readEntry(new LittleEndianDataInputStream( getDirectoryStreamForDId(pDirectoryId) )); - entry.mParent = pParent; - entry.mDocument = this; + entry.parent = pParent; + entry.document = this; return entry; } @@ -527,21 +558,23 @@ public final class CompoundDocument { } public Entry getRootEntry() throws IOException { - if (mRootEntry == null) { + if (rootEntry == null) { readSAT(); - mRootEntry = getEntry(0, null); + rootEntry = getEntry(0, null); - if (mRootEntry.type != Entry.ROOT_STORAGE) { - throw new CorruptDocumentException("Invalid root storage type: " + mRootEntry.type); + if (rootEntry.type != Entry.ROOT_STORAGE) { + throw new CorruptDocumentException("Invalid root storage type: " + rootEntry.type); } } - return mRootEntry; + + return rootEntry; } + // This is useless, as most documents on file have all-zero UUIDs... // @Override // public int hashCode() { -// return mUID.hashCode(); +// return uUID.hashCode(); // } // // @Override @@ -555,7 +588,7 @@ public final class CompoundDocument { // } // // if (pOther.getClass() == getClass()) { -// return mUID.equals(((CompoundDocument) pOther).mUID); +// return uUID.equals(((CompoundDocument) pOther).uUID); // } // // return false; @@ -565,7 +598,7 @@ public final class CompoundDocument { public String toString() { return String.format( "%s[uuid: %s, sector size: %d/%d bytes, directory SID: %d, master SAT: %s entries]", - getClass().getSimpleName(), mUID, mSectorSize, mShortSectorSize, mDirectorySId, mMasterSAT.length + getClass().getSimpleName(), uUID, sectorSize, shortSectorSize, directorySId, masterSAT.length ); } @@ -601,29 +634,29 @@ public final class CompoundDocument { return ((pMSTime >> 1) / 5000) + EPOCH_OFFSET; } - // TODO: Enforce stream length! - static class Stream extends SeekableInputStream { - private SIdChain mChain; - int mNextSectorPos; - byte[] mBuffer; - int mBufferPos; + static class Stream extends InputStream { + private final SIdChain chain; + private final CompoundDocument document; + private final long length; - private final CompoundDocument mDocument; - private final long mLength; + private long streamPos; + private int nextSectorPos; + private byte[] buffer; + private int bufferPos; - public Stream(final SIdChain pChain, final long pLength, final int pSectorSize, final CompoundDocument pDocument) { - mChain = pChain; - mLength = pLength; + public Stream(SIdChain chain, int streamSize, int sectorSize, CompoundDocument document) { + this.chain = chain; + this.length = streamSize; - mBuffer = new byte[pSectorSize]; - mBufferPos = mBuffer.length; + this.buffer = new byte[sectorSize]; + this.bufferPos = buffer.length; - mDocument = pDocument; + this.document = document; } @Override public int available() throws IOException { - return (int) Math.min(mBuffer.length - mBufferPos, mLength - getStreamPosition()); + return (int) Math.min(buffer.length - bufferPos, length - streamPos); } public int read() throws IOException { @@ -633,20 +666,23 @@ public final class CompoundDocument { } } - return mBuffer[mBufferPos++] & 0xff; + streamPos++; + + return buffer[bufferPos++] & 0xff; } private boolean fillBuffer() throws IOException { - if (mNextSectorPos < mChain.length()) { - // TODO: Sync on mDocument.mInput here, and we are completely detached... :-) - // TODO: We also need to sync other places... - synchronized (mDocument) { - mDocument.seekToSId(mChain.get(mNextSectorPos), mLength); - mDocument.mInput.readFully(mBuffer); + if (streamPos < length && nextSectorPos < chain.length()) { + // TODO: Sync on document.input here, and we are completely detached... :-) + // TODO: Update: We also need to sync other places... :-P + synchronized (document) { + document.seekToSId(chain.get(nextSectorPos), length); + document.input.readFully(buffer); } - mNextSectorPos++; - mBufferPos = 0; + nextSectorPos++; + bufferPos = 0; + return true; } @@ -663,99 +699,66 @@ public final class CompoundDocument { int toRead = Math.min(len, available()); - System.arraycopy(mBuffer, mBufferPos, b, off, toRead); - mBufferPos += toRead; + System.arraycopy(buffer, bufferPos, b, off, toRead); + bufferPos += toRead; + streamPos += toRead; return toRead; } - public boolean isCached() { - return true; - } - - public boolean isCachedMemory() { - return false; - } - - public boolean isCachedFile() { - return true; - } - - protected void closeImpl() throws IOException { - mBuffer = null; - mChain = null; - } - - protected void seekImpl(final long pPosition) throws IOException { - long pos = getStreamPosition(); - - if (pos - mBufferPos >= pPosition && pPosition <= pos + available()) { - // Skip inside buffer only - mBufferPos += (pPosition - pos); - } - else { - // Skip outside buffer - mNextSectorPos = (int) (pPosition / mBuffer.length); - if (!fillBuffer()) { - throw new EOFException(); - } - mBufferPos = (int) (pPosition % mBuffer.length); - } - } - - protected void flushBeforeImpl(long pPosition) throws IOException { - // No need to do anything here + @Override + public void close() throws IOException { + buffer = null; } } - // TODO: Add test case for this class!!! static class SeekableLittleEndianDataInputStream extends LittleEndianDataInputStream implements Seekable { - private final SeekableInputStream mSeekable; + private final SeekableInputStream seekable; public SeekableLittleEndianDataInputStream(final SeekableInputStream pInput) { super(pInput); - mSeekable = pInput; + seekable = pInput; } public void seek(final long pPosition) throws IOException { - mSeekable.seek(pPosition); + seekable.seek(pPosition); } public boolean isCachedFile() { - return mSeekable.isCachedFile(); + return seekable.isCachedFile(); } public boolean isCachedMemory() { - return mSeekable.isCachedMemory(); + return seekable.isCachedMemory(); } public boolean isCached() { - return mSeekable.isCached(); + return seekable.isCached(); } public long getStreamPosition() throws IOException { - return mSeekable.getStreamPosition(); + return seekable.getStreamPosition(); } public long getFlushedPosition() throws IOException { - return mSeekable.getFlushedPosition(); + return seekable.getFlushedPosition(); } public void flushBefore(final long pPosition) throws IOException { - mSeekable.flushBefore(pPosition); + seekable.flushBefore(pPosition); } public void flush() throws IOException { - mSeekable.flush(); + seekable.flush(); } @Override public void reset() throws IOException { - mSeekable.reset(); + seekable.reset(); } public void mark() { - mSeekable.mark(); + seekable.mark(); } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java index 29790583..d4ac6b60 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java @@ -32,6 +32,7 @@ import com.twelvemonkeys.io.SeekableInputStream; import java.io.DataInput; import java.io.IOException; +import java.nio.charset.Charset; import java.util.Collections; import java.util.SortedSet; import java.util.TreeSet; @@ -61,9 +62,9 @@ public final class Entry implements Comparable { int startSId; int streamSize; - CompoundDocument mDocument; - Entry mParent; - SortedSet mChildren; + CompoundDocument document; + Entry parent; + SortedSet children; public final static int LENGTH = 128; @@ -99,28 +100,26 @@ public final class Entry implements Comparable { * @throws IOException if an i/o exception occurs during reading */ private void read(final DataInput pInput) throws IOException { - char[] chars = new char[32]; - for (int i = 0; i < chars.length; i++) { - chars[i] = pInput.readChar(); - } + byte[] bytes = new byte[64]; + pInput.readFully(bytes); // NOTE: Length is in bytes, including the null-terminator... int nameLength = pInput.readShort(); - name = new String(chars, 0, (nameLength - 1) / 2); - //System.out.println("name: " + name); + name = new String(bytes, 0, nameLength - 2, Charset.forName("UTF-16LE")); +// System.out.println("name: " + name); type = pInput.readByte(); - //System.out.println("type: " + type); +// System.out.println("type: " + type); nodeColor = pInput.readByte(); - //System.out.println("nodeColor: " + nodeColor); +// System.out.println("nodeColor: " + nodeColor); prevDId = pInput.readInt(); - //System.out.println("prevDID: " + prevDID); +// System.out.println("prevDId: " + prevDId); nextDId = pInput.readInt(); - //System.out.println("nextDID: " + nextDID); +// System.out.println("nextDId: " + nextDId); rootNodeDId = pInput.readInt(); - //System.out.println("rootNodeDID: " + rootNodeDID); +// System.out.println("rootNodeDId: " + rootNodeDId); // UID (16) + user flags (4), ignored if (pInput.skipBytes(20) != 20) { @@ -131,9 +130,9 @@ public final class Entry implements Comparable { modifiedTimestamp = CompoundDocument.toJavaTimeInMillis(pInput.readLong()); startSId = pInput.readInt(); - //System.out.println("startSID: " + startSID); +// System.out.println("startSId: " + startSId); streamSize = pInput.readInt(); - //System.out.println("streamSize: " + streamSize); +// System.out.println("streamSize: " + streamSize); // Reserved pInput.readInt(); @@ -186,11 +185,11 @@ public final class Entry implements Comparable { * @see #length() */ public SeekableInputStream getInputStream() throws IOException { - if (isDirectory()) { + if (!isFile()) { return null; } - return mDocument.getInputStreamForSId(startSId, streamSize); + return document.getInputStreamForSId(startSId, streamSize); } /** @@ -201,9 +200,10 @@ public final class Entry implements Comparable { * @see #getInputStream() */ public long length() { - if (isDirectory()) { + if (!isFile()) { return 0L; } + return streamSize; } @@ -248,7 +248,7 @@ public final class Entry implements Comparable { * the root {@code Entry} */ public Entry getParentEntry() { - return mParent; + return parent; } /** @@ -266,7 +266,7 @@ public final class Entry implements Comparable { Entry dummy = new Entry(); dummy.name = pName; - dummy.mParent = this; + dummy.parent = this; SortedSet child = getChildEntries().tailSet(dummy); return (Entry) child.first(); @@ -279,26 +279,26 @@ public final class Entry implements Comparable { * @throws java.io.IOException if an I/O exception occurs */ public SortedSet getChildEntries() throws IOException { - if (mChildren == null) { + if (children == null) { if (isFile() || rootNodeDId == -1) { - mChildren = NO_CHILDREN; + children = NO_CHILDREN; } else { - // Start at root node in R/B tree, and raed to the left and right, + // Start at root node in R/B tree, and read to the left and right, // re-build tree, according to the docs - mChildren = mDocument.getEntries(rootNodeDId, this); + children = Collections.unmodifiableSortedSet(document.getEntries(rootNodeDId, this)); } } - return mChildren; + return children; } @Override public String toString() { return "\"" + name + "\"" + " (" + (isFile() ? "Document" : (isDirectory() ? "Directory" : "Root")) - + (mParent != null ? ", parent: \"" + mParent.getName() + "\"" : "") - + (isFile() ? "" : ", children: " + (mChildren != null ? String.valueOf(mChildren.size()) : "(unknown)")) + + (parent != null ? ", parent: \"" + parent.getName() + "\"" : "") + + (isFile() ? "" : ", children: " + (children != null ? String.valueOf(children.size()) : "(unknown)")) + ", SId=" + startSId + ", length=" + streamSize + ")"; } @@ -312,8 +312,8 @@ public final class Entry implements Comparable { } Entry other = (Entry) pOther; - return name.equals(other.name) && (mParent == other.mParent - || (mParent != null && mParent.equals(other.mParent))); + return name.equals(other.name) && (parent == other.parent + || (parent != null && parent.equals(other.parent))); } @Override diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java index a918c28b..b1bb7b73 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java @@ -37,7 +37,7 @@ import java.util.NoSuchElementException; * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java#1 $ */ -class SIdChain { +final class SIdChain { int[] chain; int size = 0; int next = 0; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java b/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java index 26e9d8c9..6a3f993f 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java @@ -37,11 +37,10 @@ import java.net.*; * @see SimpleAuthenticator * @see java.net.Authenticator * - * @author Harald Kuhr (haraldk@iconmedialab.no), + * @author Harald Kuhr * @version 1.0 */ public interface AuthenticatorFilter { - public boolean accept(InetAddress pAddress, int pPort, String pProtocol, - String pPrompt, String pScheme); + public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/util/BASE64.java b/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java similarity index 95% rename from common/common-io/src/main/java/com/twelvemonkeys/util/BASE64.java rename to common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java index ad3d128b..0ebbd67d 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/util/BASE64.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.util; +package com.twelvemonkeys.net; import com.twelvemonkeys.io.*; import com.twelvemonkeys.io.enc.Base64Decoder; @@ -41,8 +41,9 @@ import java.io.*; * @author unascribed * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ + * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead */ -public class BASE64 { +class BASE64 { /** * This array maps the characters to their 6 bit values diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java b/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java index c0ac21ab..bd337235 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java @@ -29,7 +29,6 @@ package com.twelvemonkeys.net; import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.util.BASE64; import java.io.*; import java.net.*; @@ -65,18 +64,18 @@ public class HttpURLConnection extends java.net.HttpURLConnection { private final static String HTTP_HEADER_END = "\r\n\r\n"; private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; private final static int BUF_SIZE = 8192; - private int mMaxRedirects = (System.getProperty("http.maxRedirects") != null) + private int maxRedirects = (System.getProperty("http.maxRedirects") != null) ? Integer.parseInt(System.getProperty("http.maxRedirects")) : 20; - protected int mTimeout = -1; - protected int mConnectTimeout = -1; - private Socket mSocket = null; - protected InputStream mErrorStream = null; - protected InputStream mInputStream = null; - protected OutputStream mOutputStream = null; - private String[] mResponseHeaders = null; - protected Properties mResponseHeaderFields = null; - protected Properties mRequestProperties = new Properties(); + protected int timeout = -1; + protected int connectTimeout = -1; + private Socket socket = null; + protected InputStream errorStream = null; + protected InputStream inputStream = null; + protected OutputStream outputStream = null; + private String[] responseHeaders = null; + protected Properties responseHeaderFields = null; + protected Properties requestProperties = new Properties(); /** * Creates a HttpURLConnection. @@ -114,7 +113,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { super(pURL); setTimeout(pTimeout); - mConnectTimeout = pConnectTimeout; + connectTimeout = pConnectTimeout; } /** @@ -135,13 +134,13 @@ public class HttpURLConnection extends java.net.HttpURLConnection { if (connected) { throw new IllegalAccessError("Already connected"); } - String oldValue = mRequestProperties.getProperty(pKey); + String oldValue = requestProperties.getProperty(pKey); if (oldValue == null) { - mRequestProperties.setProperty(pKey, pValue); + requestProperties.setProperty(pKey, pValue); } else { - mRequestProperties.setProperty(pKey, oldValue + ", " + pValue); + requestProperties.setProperty(pKey, oldValue + ", " + pValue); } } @@ -158,7 +157,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { if (connected) { throw new IllegalAccessError("Already connected"); } - return mRequestProperties.getProperty(pKey); + return requestProperties.getProperty(pKey); } /** @@ -212,7 +211,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { * if there is no such field in the header. */ public String getHeaderField(String pName) { - return mResponseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); + return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); } /** @@ -230,10 +229,10 @@ public class HttpURLConnection extends java.net.HttpURLConnection { */ public String getHeaderField(int pIndex) { // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= mResponseHeaders.length) { + if (pIndex >= responseHeaders.length) { return null; } - String field = mResponseHeaders[pIndex]; + String field = responseHeaders[pIndex]; // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). if ((pIndex == 0) || (field == null)) { @@ -256,10 +255,10 @@ public class HttpURLConnection extends java.net.HttpURLConnection { */ public String getHeaderFieldKey(int pIndex) { // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= mResponseHeaders.length) { + if (pIndex >= responseHeaders.length) { return null; } - String field = mResponseHeaders[pIndex]; + String field = responseHeaders[pIndex]; if (StringUtil.isEmpty(field)) { return null; @@ -283,10 +282,10 @@ public class HttpURLConnection extends java.net.HttpURLConnection { if (pTimeout < 0) { // Must be positive throw new IllegalArgumentException("Timeout must be positive."); } - mTimeout = pTimeout; - if (mSocket != null) { + timeout = pTimeout; + if (socket != null) { try { - mSocket.setSoTimeout(pTimeout); + socket.setSoTimeout(pTimeout); } catch (SocketException se) { // Not much to do about that... @@ -305,12 +304,12 @@ public class HttpURLConnection extends java.net.HttpURLConnection { public int getTimeout() { try { - return ((mSocket != null) - ? mSocket.getSoTimeout() - : mTimeout); + return ((socket != null) + ? socket.getSoTimeout() + : timeout); } catch (SocketException se) { - return mTimeout; + return timeout; } } @@ -332,24 +331,24 @@ public class HttpURLConnection extends java.net.HttpURLConnection { } int length; - if (mInputStream == null) { + if (inputStream == null) { return null; } // "De-chunk" the output stream else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { - if (!(mInputStream instanceof ChunkedInputStream)) { - mInputStream = new ChunkedInputStream(mInputStream); + if (!(inputStream instanceof ChunkedInputStream)) { + inputStream = new ChunkedInputStream(inputStream); } } // Make sure we don't wait forever, if the content-length is known else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { - if (!(mInputStream instanceof FixedLengthInputStream)) { - mInputStream = new FixedLengthInputStream(mInputStream, length); + if (!(inputStream instanceof FixedLengthInputStream)) { + inputStream = new FixedLengthInputStream(inputStream, length); } } - return mInputStream; + return inputStream; } /** @@ -364,7 +363,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { if (!connected) { connect(); } - return mOutputStream; + return outputStream; } /** @@ -374,15 +373,15 @@ public class HttpURLConnection extends java.net.HttpURLConnection { * instance can be reused for other requests. */ public void disconnect() { - if (mSocket != null) { + if (socket != null) { try { - mSocket.close(); + socket.close(); } catch (IOException ioe) { // Does not matter, I guess. } - mSocket = null; + socket = null; } connected = false; } @@ -397,37 +396,37 @@ public class HttpURLConnection extends java.net.HttpURLConnection { : HTTP_DEFAULT_PORT; // Create socket if we don't have one - if (mSocket == null) { - //mSocket = new Socket(pURL.getHost(), port); // Blocks... - mSocket = createSocket(pURL, port, mConnectTimeout); - mSocket.setSoTimeout(mTimeout); + if (socket == null) { + //socket = new Socket(pURL.getHost(), port); // Blocks... + socket = createSocket(pURL, port, connectTimeout); + socket.setSoTimeout(timeout); } // Get Socket output stream - OutputStream os = mSocket.getOutputStream(); + OutputStream os = socket.getOutputStream(); // Connect using HTTP - writeRequestHeaders(os, pURL, method, mRequestProperties, usingProxy(), pAuth, pAuthType); + writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); // Get response input stream - InputStream sis = mSocket.getInputStream(); + InputStream sis = socket.getInputStream(); BufferedInputStream is = new BufferedInputStream(sis); // Detatch reponse headers from reponse input stream InputStream header = detatchResponseHeader(is); // Parse headers and set response code/message - mResponseHeaders = parseResponseHeader(header); - mResponseHeaderFields = parseHeaderFields(mResponseHeaders); + responseHeaders = parseResponseHeader(header); + responseHeaderFields = parseHeaderFields(responseHeaders); //System.err.println("Headers fields:"); - //mResponseHeaderFields.list(System.err); + //responseHeaderFields.list(System.err); // Test HTTP response code, to see if further action is needed switch (getResponseCode()) { case HTTP_OK: // 200 OK - mInputStream = is; - mErrorStream = null; + inputStream = is; + errorStream = null; break; /* @@ -472,7 +471,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { // Avoid infinite loop if (pRetries++ <= 0) { - throw new ProtocolException("Server redirected too many times (" + mMaxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does + throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does } else if (pa != null) { connect(pURL, pa, method, pRetries); @@ -506,8 +505,8 @@ public class HttpURLConnection extends java.net.HttpURLConnection { // Test if we can reuse the Socket if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { - mSocket.close(); // Close the socket, won't need it anymore - mSocket = null; + socket.close(); // Close the socket, won't need it anymore + socket = null; } if (location != null) { //System.err.println("Redirecting to " + location); @@ -526,22 +525,22 @@ public class HttpURLConnection extends java.net.HttpURLConnection { default : // Not 200 OK, or any of the redirect responses // Probably an error... - mErrorStream = is; - mInputStream = null; + errorStream = is; + inputStream = null; } // --- Need rethinking... // No further questions, let the Socket wait forever (until the server // closes the connection) - //mSocket.setSoTimeout(0); + //socket.setSoTimeout(0); // Probably not... The timeout should only kick if the read BLOCKS. // Shutdown output, meaning any writes to the outputstream below will // probably fail... - //mSocket.shutdownOutput(); + //socket.shutdownOutput(); // Not a good idea at all... POSTs need the outputstream to send the // form-data. // --- /Need rethinking. - mOutputStream = os; + outputStream = os; } private static interface SocketConnector extends Runnable { @@ -663,7 +662,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { return; // Ignore } connected = true; - connect(url, null, null, mMaxRedirects); + connect(url, null, null, maxRedirects); } /** diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java index d2637fcb..ae7faefd 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java @@ -3,7 +3,6 @@ package com.twelvemonkeys.net; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.DateUtil; -import com.twelvemonkeys.util.BASE64; import com.twelvemonkeys.util.CollectionUtil; import java.io.*; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java b/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java index ccff833d..d036fb2b 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java @@ -28,29 +28,31 @@ package com.twelvemonkeys.net; +import com.twelvemonkeys.lang.Validate; + import java.net.Authenticator; import java.net.InetAddress; import java.net.PasswordAuthentication; import java.net.URL; -import java.util.Hashtable; +import java.util.HashMap; +import java.util.Map; /** * A simple Authenticator implementation. - * Singleton class, obtain reference through the static + * Singleton class, obtain reference through the static * {@code getInstance} method. - *

+ *

* After swearing, sweating, pulling my hair, banging my head repeatedly - * into the walls and reading the java.net.Authenticator API documentation + * into the walls and reading the java.net.Authenticator API documentation * once more, an idea came to my mind. This is the result. I hope you find it * useful. -- Harald K. * - * @see java.net.Authenticator - * * @author Harald Kuhr (haraldk@iconmedialab.no) - * @version 1.0 + * @version 1.0 + * @see java.net.Authenticator */ public class SimpleAuthenticator extends Authenticator { - + /** The reference to the single instance of this class. */ private static SimpleAuthenticator sInstance = null; /** Keeps track of the state of this class. */ @@ -63,237 +65,179 @@ public class SimpleAuthenticator extends Authenticator { /** Basic authentication scheme. */ public final static String BASIC = "Basic"; - /** - * The hastable that keeps track of the PasswordAuthentications. - */ + /** The hastable that keeps track of the PasswordAuthentications. */ + protected Map passwordAuthentications = null; - protected Hashtable mPasswordAuthentications = null; - - /** - * The hastable that keeps track of the Authenticators. - */ - - protected Hashtable mAuthenticators = null; - - /** - * Creates a SimpleAuthenticator. - */ + /** The hastable that keeps track of the Authenticators. */ + protected Map authenticators = null; + /** Creates a SimpleAuthenticator. */ private SimpleAuthenticator() { - mPasswordAuthentications = new Hashtable(); - mAuthenticators = new Hashtable(); + passwordAuthentications = new HashMap(); + authenticators = new HashMap(); } /** - * Gets the SimpleAuthenticator instance and registers it through the + * Gets the SimpleAuthenticator instance and registers it through the * Authenticator.setDefault(). If there is no current instance * of the SimpleAuthenticator in the VM, one is created. This method will - * try to figure out if the setDefault() succeeded (a hack), and will + * try to figure out if the setDefault() succeeded (a hack), and will * return null if it was not able to register the instance as default. * - * @return The single instance of this class, or null, if another - * Authenticator is allready registered as default. + * @return The single instance of this class, or null, if another + * Authenticator is allready registered as default. */ - public static synchronized SimpleAuthenticator getInstance() { - if (!sInitialized) { - // Create an instance - sInstance = new SimpleAuthenticator(); + if (!sInitialized) { + // Create an instance + sInstance = new SimpleAuthenticator(); - // Try to set default (this may quietly fail...) - Authenticator.setDefault(sInstance); + // Try to set default (this may quietly fail...) + Authenticator.setDefault(sInstance); - // A hack to figure out if we really did set the authenticator - PasswordAuthentication pa = - Authenticator.requestPasswordAuthentication(null, FOURTYTWO, - null, null, MAGIC); + // A hack to figure out if we really did set the authenticator + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); - // If this test returns false, we didn't succeed, so we set the - // instance back to null. - if (pa == null || !MAGIC.equals(pa.getUserName()) || - !("" + FOURTYTWO).equals(new String(pa.getPassword()))) - sInstance = null; + // If this test returns false, we didn't succeed, so we set the + // instance back to null. + if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { + sInstance = null; + } - // Done - sInitialized = true; - } + // Done + sInitialized = true; + } - return sInstance; + return sInstance; } /** - * Gets the PasswordAuthentication for the request. Called when password + * Gets the PasswordAuthentication for the request. Called when password * authorization is needed. * - * @return The PasswordAuthentication collected from the user, or null if + * @return The PasswordAuthentication collected from the user, or null if * none is provided. */ - protected PasswordAuthentication getPasswordAuthentication() { - // Don't worry, this is just a hack to figure out if we were able - // to set this Authenticator through the setDefault method. - if (!sInitialized && MAGIC.equals(getRequestingScheme()) - && getRequestingPort() == FOURTYTWO) - return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO) - .toCharArray()); - /* - System.err.println("getPasswordAuthentication"); - System.err.println(getRequestingSite()); - System.err.println(getRequestingPort()); - System.err.println(getRequestingProtocol()); - System.err.println(getRequestingPrompt()); - System.err.println(getRequestingScheme()); - */ + // Don't worry, this is just a hack to figure out if we were able + // to set this Authenticator through the setDefault method. + if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { + return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); + } + /* + System.err.println("getPasswordAuthentication"); + System.err.println(getRequestingSite()); + System.err.println(getRequestingPort()); + System.err.println(getRequestingProtocol()); + System.err.println(getRequestingPrompt()); + System.err.println(getRequestingScheme()); + */ - // TODO: - // Look for a more specific PasswordAuthenticatior before using - // Default: - // - // if (...) - // return pa.requestPasswordAuthentication(getRequestingSite(), - // getRequestingPort(), - // getRequestingProtocol(), - // getRequestingPrompt(), - // getRequestingScheme()); + // TODO: + // Look for a more specific PasswordAuthenticatior before using + // Default: + // + // if (...) + // return pa.requestPasswordAuthentication(getRequestingSite(), + // getRequestingPort(), + // getRequestingProtocol(), + // getRequestingPrompt(), + // getRequestingScheme()); - return (PasswordAuthentication) - mPasswordAuthentications.get(new AuthKey(getRequestingSite(), - getRequestingPort(), - getRequestingProtocol(), - getRequestingPrompt(), - getRequestingScheme())); + return passwordAuthentications.get(new AuthKey(getRequestingSite(), + getRequestingPort(), + getRequestingProtocol(), + getRequestingPrompt(), + getRequestingScheme())); } - /** - * Registers a PasswordAuthentication with a given URL address. - * - */ - - public PasswordAuthentication registerPasswordAuthentication(URL pURL, - PasswordAuthentication pPA) { - return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), - pURL.getPort(), - pURL.getProtocol(), - null, // Prompt/Realm - BASIC, - pPA); + /** Registers a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { + return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), + pURL.getPort(), + pURL.getProtocol(), + null, // Prompt/Realm + BASIC, + pPA); } - /** - * Registers a PasswordAuthentication with a given net address. - * - */ + /** Registers a PasswordAuthentication with a given net address. */ + public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { + /* + System.err.println("registerPasswordAuthentication"); + System.err.println(pAddress); + System.err.println(pPort); + System.err.println(pProtocol); + System.err.println(pPrompt); + System.err.println(pScheme); + */ - public PasswordAuthentication registerPasswordAuthentication( - InetAddress pAddress, int pPort, String pProtocol, - String pPrompt, String pScheme, PasswordAuthentication pPA) - { - /* - System.err.println("registerPasswordAuthentication"); - System.err.println(pAddress); - System.err.println(pPort); - System.err.println(pProtocol); - System.err.println(pPrompt); - System.err.println(pScheme); - */ - - return (PasswordAuthentication) - mPasswordAuthentications.put(new AuthKey(pAddress, pPort, - pProtocol, pPrompt, - pScheme), - pPA); + return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); } - /** - * Unregisters a PasswordAuthentication with a given URL address. - * - */ - + /** Unregisters a PasswordAuthentication with a given URL address. */ public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { - return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), - pURL.getPort(), - pURL.getProtocol(), - null, - BASIC); + return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); } - /** - * Unregisters a PasswordAuthentication with a given net address. - * - */ - - public PasswordAuthentication unregisterPasswordAuthentication( - InetAddress pAddress, int pPort, String pProtocol, - String pPrompt, String pScheme) - { - return (PasswordAuthentication) - mPasswordAuthentications.remove(new AuthKey(pAddress, pPort, - pProtocol, pPrompt, - pScheme)); + /** Unregisters a PasswordAuthentication with a given net address. */ + public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); } /** * TODO: Registers a PasswordAuthenticator that can answer authentication * requests. - * + * * @see PasswordAuthenticator */ - - public void registerPasswordAuthenticator(PasswordAuthenticator pPA, - AuthenticatorFilter pFilter) { - mAuthenticators.put(pPA, pFilter); + public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { + authenticators.put(pPA, pFilter); } /** * TODO: Unregisters a PasswordAuthenticator that can answer authentication * requests. - * + * * @see PasswordAuthenticator */ - public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { - mAuthenticators.remove(pPA); + authenticators.remove(pPA); } - } /** * Utility class, used for caching the PasswordAuthentication objects. * Everything but address may be null */ - class AuthKey { - - InetAddress mAddress = null; - int mPort = -1; - String mProtocol = null; - String mPrompt = null; - String mScheme = null; - AuthKey(InetAddress pAddress, int pPort, String pProtocol, - String pPrompt, String pScheme) { - if (pAddress == null) - throw new IllegalArgumentException("Address argument can't be null!"); + InetAddress address = null; + int port = -1; + String protocol = null; + String prompt = null; + String scheme = null; - mAddress = pAddress; - mPort = pPort; - mProtocol = pProtocol; - mPrompt = pPrompt; - mScheme = pScheme; + AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + Validate.notNull(pAddress, "address"); - // System.out.println("Created: " + this); + address = pAddress; + port = pPort; + protocol = pProtocol; + prompt = pPrompt; + scheme = pScheme; + + // System.out.println("Created: " + this); } - /** - * Creates a string representation of this object. - */ + /** Creates a string representation of this object. */ public String toString() { - return "AuthKey[" + mAddress + ":" + mPort + "/" + mProtocol + " \"" + mPrompt + "\" (" + mScheme + ")]"; + return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; } public boolean equals(Object pObj) { - return (pObj instanceof AuthKey ? equals((AuthKey) pObj) : false); + return (pObj instanceof AuthKey && equals((AuthKey) pObj)); } // Ahem.. Breaks the rule from Object.equals(Object): @@ -302,25 +246,25 @@ class AuthKey { // should return true. public boolean equals(AuthKey pKey) { - // Maybe allow nulls, and still be equal? - return (mAddress.equals(pKey.mAddress) - && (mPort == -1 - || pKey.mPort == -1 - || mPort == pKey.mPort) - && (mProtocol == null - || pKey.mProtocol == null - || mProtocol.equals(pKey.mProtocol)) - && (mPrompt == null - || pKey.mPrompt == null - || mPrompt.equals(pKey.mPrompt)) - && (mScheme == null - || pKey.mScheme == null - || mScheme.equalsIgnoreCase(pKey.mScheme))); + // Maybe allow nulls, and still be equal? + return (address.equals(pKey.address) + && (port == -1 + || pKey.port == -1 + || port == pKey.port) + && (protocol == null + || pKey.protocol == null + || protocol.equals(pKey.protocol)) + && (prompt == null + || pKey.prompt == null + || prompt.equals(pKey.prompt)) + && (scheme == null + || pKey.scheme == null + || scheme.equalsIgnoreCase(pKey.scheme))); } public int hashCode() { - // There won't be too many pr address, will it? ;-) - return mAddress.hashCode(); + // There won't be too many pr address, will it? ;-) + return address.hashCode(); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java b/common/common-io/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java index 25e27cc0..4dd47830 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java @@ -52,13 +52,13 @@ public final class DOMSerializer { private static final String PARAM_PRETTY_PRINT = "format-pretty-print"; private static final String PARAM_XML_DECLARATION = "xml-declaration"; - private final LSSerializer mSerializer; - private final LSOutput mOutput; + private final LSSerializer serializer; + private final LSOutput output; private DOMSerializer() { DOMImplementationLS domImpl = Support.getImplementation(); - mSerializer = domImpl.createLSSerializer(); - mOutput = domImpl.createLSOutput(); + serializer = domImpl.createLSSerializer(); + output = domImpl.createLSOutput(); } /** @@ -71,8 +71,8 @@ public final class DOMSerializer { public DOMSerializer(final OutputStream pStream, final String pEncoding) { this(); - mOutput.setByteStream(pStream); - mOutput.setEncoding(pEncoding); + output.setByteStream(pStream); + output.setEncoding(pEncoding); } /** @@ -84,17 +84,17 @@ public final class DOMSerializer { public DOMSerializer(final Writer pStream) { this(); - mOutput.setCharacterStream(pStream); + output.setCharacterStream(pStream); } /* // TODO: Is it useful? public void setNewLine(final String pNewLine) { - mSerializer.setNewLine(pNewLine); + serializer.setNewLine(pNewLine); } public String getNewLine() { - return mSerializer.getNewLine(); + return serializer.getNewLine(); } */ @@ -107,18 +107,18 @@ public final class DOMSerializer { * @param pPrettyPrint {@code true} to enable pretty printing */ public void setPrettyPrint(final boolean pPrettyPrint) { - DOMConfiguration configuration = mSerializer.getDomConfig(); + DOMConfiguration configuration = serializer.getDomConfig(); if (configuration.canSetParameter(PARAM_PRETTY_PRINT, pPrettyPrint)) { configuration.setParameter(PARAM_PRETTY_PRINT, pPrettyPrint); } } public boolean getPrettyPrint() { - return Boolean.TRUE.equals(mSerializer.getDomConfig().getParameter(PARAM_PRETTY_PRINT)); + return Boolean.TRUE.equals(serializer.getDomConfig().getParameter(PARAM_PRETTY_PRINT)); } private void setXMLDeclaration(boolean pXMLDeclaration) { - mSerializer.getDomConfig().setParameter(PARAM_XML_DECLARATION, pXMLDeclaration); + serializer.getDomConfig().setParameter(PARAM_XML_DECLARATION, pXMLDeclaration); } /** @@ -142,7 +142,7 @@ public final class DOMSerializer { private void serializeImpl(final Node pNode, final boolean pOmitDecl) { setXMLDeclaration(pOmitDecl); - mSerializer.write(pNode, mOutput); + serializer.write(pNode, output); } private static class Support { diff --git a/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java b/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java index b2488300..96e79c50 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java @@ -59,28 +59,22 @@ public class XMLSerializer { // TODO: Consider using IOException to communicate trouble, rather than RTE, // to be more compatible... - // TODO: Idea: Create a SerializationContext that stores attributes on - // serialization, to keep the serialization thread-safe - // Store preserveSpace attribute in this context, to avoid costly traversals - // Store user options here too - // TODO: Push/pop? - - private final OutputStream mOutput; - private final Charset mEncoding; - private final SerializationContext mContext; + private final OutputStream output; + private final Charset encoding; + private final SerializationContext context; public XMLSerializer(final OutputStream pOutput, final String pEncoding) { - mOutput = pOutput; - mEncoding = Charset.forName(pEncoding); - mContext = new SerializationContext(); + output = pOutput; + encoding = Charset.forName(pEncoding); + context = new SerializationContext(); } public final void setIndentation(String pIndent) { - mContext.indent = pIndent != null ? pIndent : " "; + context.indent = pIndent != null ? pIndent : "\t"; } public final void setStripComments(boolean pStrip) { - mContext.stripComments = pStrip; + context.stripComments = pStrip; } /** @@ -101,12 +95,12 @@ public class XMLSerializer { * @param pWriteXMLDeclaration {@code true} if the XML declaration should be included, otherwise {@code false}. */ public void serialize(final Node pRootNode, final boolean pWriteXMLDeclaration) { - PrintWriter out = new PrintWriter(new OutputStreamWriter(mOutput, mEncoding)); + PrintWriter out = new PrintWriter(new OutputStreamWriter(output, encoding)); try { if (pWriteXMLDeclaration) { writeXMLDeclaration(out); } - writeXML(out, pRootNode, mContext.copy()); + writeXML(out, pRootNode, context.copy()); } finally { out.flush(); @@ -115,7 +109,7 @@ public class XMLSerializer { private void writeXMLDeclaration(final PrintWriter pOut) { pOut.print(""); } @@ -279,11 +273,7 @@ public class XMLSerializer { pos = appendAndEscape(pValue, pos, i, builder, ">"); break; //case '\'': - // pos = appendAndEscape(pString, pos, i, builder, "'"); - // break; //case '"': - // pos = appendAndEscape(pString, pos, i, builder, """); - // break; default: break; } @@ -347,17 +337,6 @@ public class XMLSerializer { } } - //StringBuilder builder = new StringBuilder(pValue.length() + 30); - // - //int start = 0; - //while (end >= 0) { - // builder.append(pValue.substring(start, end)); - // builder.append("""); - // start = end + 1; - // end = pValue.indexOf('"', start); - //} - //builder.append(pValue.substring(start)); - builder.append(pValue.substring(pos)); return builder.toString(); @@ -389,14 +368,14 @@ public class XMLSerializer { } private static String validateCDataValue(final String pValue) { - if (pValue.indexOf("]]>") >= 0) { + if (pValue.contains("]]>")) { throw new IllegalArgumentException("Malformed input document: CDATA block may not contain the string ']]>'"); } return pValue; } private static String validateCommentValue(final String pValue) { - if (pValue.indexOf("--") >= 0) { + if (pValue.contains("--")) { throw new IllegalArgumentException("Malformed input document: Comment may not contain the string '--'"); } return pValue; @@ -420,8 +399,6 @@ public class XMLSerializer { // even if the document was created using attributes instead of namespaces... // In that case, prefix will be null... - // TODO: Don't insert duplicate/unnecessary namesspace declarations - // Handle namespace String namespace = pNode.getNamespaceURI(); if (namespace != null && !namespace.equals(pContext.defaultNamespace)) { @@ -570,6 +547,11 @@ public class XMLSerializer { pre.appendChild(document.createTextNode(" \t \n\r some text & white ' ' \n ")); test.appendChild(pre); + Element pre2 = document.createElementNS("http://www.twelvemonkeys.com/xml/test", "tight"); + pre2.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); + pre2.appendChild(document.createTextNode("no-space-around-me")); + test.appendChild(pre2); + // Create serializer and output document //XMLSerializer serializer = new XMLSerializer(pOutput, new OutputFormat(document, UTF_8_ENCODING, true)); System.out.println("XMLSerializer:"); @@ -612,7 +594,7 @@ public class XMLSerializer { } static class SerializationContext implements Cloneable { - String indent = " "; + String indent = "\t"; int level = 0; boolean preserveSpace = false; boolean stripComments = false; diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java index 5419741a..a3a4ed2f 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java @@ -2,6 +2,7 @@ package com.twelvemonkeys.io; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.util.CollectionUtil; +import org.junit.Test; import java.io.Reader; import java.io.IOException; @@ -9,6 +10,8 @@ import java.io.StringReader; import java.util.List; import java.util.ArrayList; +import static org.junit.Assert.*; + /** * CompoundReaderTestCase *

@@ -18,7 +21,6 @@ import java.util.ArrayList; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java#2 $ */ public class CompoundReaderTestCase extends ReaderAbstractTestCase { - protected Reader makeReader(String pInput) { // Split String[] input = StringUtil.toStringArray(pInput, " "); @@ -36,6 +38,7 @@ public class CompoundReaderTestCase extends ReaderAbstractTestCase { return new CompoundReader(readers.iterator()); } + @Test public void testNullConstructor() { try { new CompoundReader(null); @@ -46,11 +49,13 @@ public class CompoundReaderTestCase extends ReaderAbstractTestCase { } } + @Test public void testEmptyIteratorConstructor() throws IOException { Reader reader = new CompoundReader(CollectionUtil.iterator(new Reader[0])); assertEquals(-1, reader.read()); } + @Test public void testIteratorWithNullConstructor() throws IOException { try { new CompoundReader(CollectionUtil.iterator(new Reader[] {null})); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java index 389184bf..95f09ccf 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java @@ -1,8 +1,12 @@ package com.twelvemonkeys.io; +import org.junit.Test; + import java.io.IOException; import java.io.InputStream; +import static org.junit.Assert.assertEquals; + /** * FastByteArrayOutputStreamTestCase *

@@ -16,6 +20,7 @@ public class FastByteArrayOutputStreamTestCase extends OutputStreamAbstractTestC return new FastByteArrayOutputStream(256); } + @Test public void testCreateInputStream() throws IOException { FastByteArrayOutputStream out = makeObject(); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java index 3f66aa38..d12e9ca5 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java @@ -11,10 +11,6 @@ import java.io.InputStream; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java#3 $ */ public class FileCacheSeekableStreamTestCase extends SeekableInputStreamAbstractTestCase { - public FileCacheSeekableStreamTestCase(String name) { - super(name); - } - protected SeekableInputStream makeInputStream(final InputStream pStream) { try { return new FileCacheSeekableStream(pStream); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java index 6dbb401e..65f3c989 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java @@ -1,7 +1,11 @@ package com.twelvemonkeys.io; +import org.junit.Test; + import java.io.*; +import static org.junit.Assert.*; + /** * MemoryCacheSeekableStreamTestCase *

@@ -10,10 +14,6 @@ import java.io.*; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java#3 $ */ public class FileSeekableStreamTestCase extends SeekableInputStreamAbstractTestCase { - public FileSeekableStreamTestCase(String name) { - super(name); - } - protected SeekableInputStream makeInputStream(final InputStream pStream) { try { return new FileSeekableStream(createFileWithContent(pStream)); @@ -37,11 +37,13 @@ public class FileSeekableStreamTestCase extends SeekableInputStreamAbstractTestC return temp; } + @Test @Override public void testCloseUnderlyingStream() throws IOException { // There is no underlying stream here... } + @Test public void testCloseUnderlyingFile() throws IOException { final boolean[] closed = new boolean[1]; diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java index 70e862cb..22f5defa 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java @@ -17,12 +17,15 @@ package com.twelvemonkeys.io; import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Random; +import static org.junit.Assert.*; + /** * InputStreamAbstractTestCase *

@@ -38,10 +41,6 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase final static private long SEED = 29487982745l; final static Random sRandom = new Random(SEED); - public InputStreamAbstractTestCase(String name) { - super(name); - } - protected final Object makeObject() { return makeInputStream(); } @@ -71,11 +70,12 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase return bytes; } + @Test public void testRead() throws Exception { int size = 5; InputStream input = makeInputStream(makeOrderedArray(size)); for (int i = 0; i < size; i++) { - assertEquals("Check Size [" + i + "]", (size - i), input.available()); + assertTrue("Check Size [" + i + "]", (size - i) >= input.available()); assertEquals("Check Value [" + i + "]", i, input.read()); } assertEquals("Available after contents all read", 0, input.available()); @@ -90,6 +90,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testAvailable() throws Exception { InputStream input = makeInputStream(1); assertFalse("Unexpected EOF", input.read() < 0); @@ -100,6 +101,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase assertEquals("Available after End of File", 0, input.available()); } + @Test public void testReadByteArray() throws Exception { byte[] bytes = new byte[10]; byte[] data = makeOrderedArray(15); @@ -145,6 +147,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testEOF() throws Exception { InputStream input = makeInputStream(makeOrderedArray(2)); assertEquals("Read 1", 0, input.read()); @@ -154,6 +157,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase assertEquals("Read 5", -1, input.read()); } + @Test public void testMarkResetUnsupported() throws IOException { InputStream input = makeInputStream(10); if (input.markSupported()) { @@ -176,6 +180,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testResetNoMark() throws Exception { InputStream input = makeInputStream(makeOrderedArray(10)); @@ -196,6 +201,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testMarkReset() throws Exception { InputStream input = makeInputStream(makeOrderedArray(25)); @@ -226,6 +232,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testResetAfterReadLimit() throws Exception { InputStream input = makeInputStream(makeOrderedArray(25)); @@ -257,6 +264,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testResetAfterReset() throws Exception { InputStream input = makeInputStream(makeOrderedArray(25)); @@ -264,7 +272,8 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase return; // Not supported, skip test } - assertTrue("Expected to read positive value", input.read() >= 0); + int first = input.read(); + assertTrue("Expected to read positive value", first >= 0); int readlimit = 5; @@ -273,19 +282,24 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase int read = input.read(); assertTrue("Expected to read positive value", read >= 0); - input.reset(); - assertEquals("Expected value read differes from actual", read, input.read()); + assertTrue(input.read() >= 0); + assertTrue(input.read() >= 0); - // Reset after read limit passed, may either throw exception, or reset to last mark + input.reset(); + assertEquals("Expected value read differs from actual", read, input.read()); + + // Reset after read limit passed, may either throw exception, or reset to last good mark try { input.reset(); - assertEquals("Re-read of reset data should be same", read, input.read()); + int reRead = input.read(); + assertTrue("Re-read of reset data should be same as initially marked or first", reRead == read || reRead == first); } catch (Exception e) { assertTrue("Wrong read-limit IOException message", e.getMessage().contains("mark")); } } + @Test public void testSkip() throws Exception { InputStream input = makeInputStream(makeOrderedArray(10)); @@ -302,6 +316,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase assertEquals("Unexpected value read after EOF", -1, input.read()); } + @Test public void testSanityOrdered() throws IOException { // This is to sanity check that the test itself is correct... byte[] bytes = makeOrderedArray(25); @@ -314,6 +329,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testSanityOrdered2() throws IOException { // This is to sanity check that the test itself is correct... byte[] bytes = makeOrderedArray(25); @@ -332,6 +348,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testSanityNegative() throws IOException { // This is to sanity check that the test itself is correct... byte[] bytes = new byte[25]; @@ -347,6 +364,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testSanityNegative2() throws IOException { // This is to sanity check that the test itself is correct... byte[] bytes = new byte[25]; @@ -368,6 +386,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testSanityRandom() throws IOException { // This is to sanity check that the test itself is correct... byte[] bytes = makeRandomArray(25); @@ -380,6 +399,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase } } + @Test public void testSanityRandom2() throws IOException { // This is to sanity check that the test itself is correct... byte[] bytes = makeRandomArray(25); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java index e0ee5289..53387cb5 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java @@ -10,10 +10,6 @@ import java.io.InputStream; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java#2 $ */ public class MemoryCacheSeekableStreamTestCase extends SeekableInputStreamAbstractTestCase { - public MemoryCacheSeekableStreamTestCase(String name) { - super(name); - } - protected SeekableInputStream makeInputStream(final InputStream pStream) { return new MemoryCacheSeekableStream(pStream); } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java index 4ed2568b..d3405dae 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java @@ -1,10 +1,13 @@ package com.twelvemonkeys.io; import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; import java.io.OutputStream; import java.io.IOException; +import static org.junit.Assert.*; + /** * InputStreamAbstractTestCase *

@@ -15,6 +18,7 @@ import java.io.IOException; public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCase { protected abstract OutputStream makeObject(); + @Test public void testWrite() throws IOException { OutputStream os = makeObject(); @@ -23,12 +27,14 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testWriteByteArray() throws IOException { OutputStream os = makeObject(); os.write(new byte[256]); } + @Test public void testWriteByteArrayNull() { OutputStream os = makeObject(); try { @@ -46,7 +52,8 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } - public void testWriteByteArrayOffsetLenght() throws IOException { + @Test + public void testWriteByteArrayOffsetLength() throws IOException { byte[] input = new byte[256]; OutputStream os = makeObject(); @@ -65,7 +72,8 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } - public void testWriteByteArrayZeroLenght() { + @Test + public void testWriteByteArrayZeroLength() { OutputStream os = makeObject(); try { os.write(new byte[1], 0, 0); @@ -75,7 +83,8 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } - public void testWriteByteArrayOffsetLenghtNull() { + @Test + public void testWriteByteArrayOffsetLengthNull() { OutputStream os = makeObject(); try { os.write(null, 5, 10); @@ -92,6 +101,7 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testWriteByteArrayNegativeOffset() { OutputStream os = makeObject(); try { @@ -109,6 +119,7 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testWriteByteArrayNegativeLength() { OutputStream os = makeObject(); try { @@ -126,6 +137,7 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testWriteByteArrayOffsetOutOfBounds() { OutputStream os = makeObject(); try { @@ -143,6 +155,7 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testWriteByteArrayLengthOutOfBounds() { OutputStream os = makeObject(); try { @@ -160,14 +173,17 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testFlush() { // TODO: Implement } + @Test public void testClose() { // TODO: Implement } + @Test public void testWriteAfterClose() throws IOException { OutputStream os = makeObject(); @@ -200,6 +216,7 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testFlushAfterClose() throws IOException { OutputStream os = makeObject(); @@ -221,6 +238,7 @@ public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCas } } + @Test public void testCloseAfterClose() throws IOException { OutputStream os = makeObject(); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java index cee9bc6f..cf9e63d5 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java @@ -1,10 +1,13 @@ package com.twelvemonkeys.io; import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; import java.io.Reader; import java.io.IOException; +import static org.junit.Assert.*; + /** * ReaderAbstractTestCase *

@@ -36,6 +39,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { protected abstract Reader makeReader(String pInput); + @Test public void testRead() throws IOException { Reader reader = makeReader(); @@ -51,6 +55,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { assertEquals(mInput, buffer.toString()); } + @Test public void testReadBuffer() throws IOException { Reader reader = makeReader(); @@ -70,6 +75,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { assertEquals(mInput, new String(chars)); } + @Test public void testSkipToEnd() throws IOException { Reader reader = makeReader(); @@ -83,6 +89,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { assertEquals(0, toSkip); } + @Test public void testSkipToEndAndRead() throws IOException { Reader reader = makeReader(); @@ -95,6 +102,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { } // TODO: It's possible to support reset and not mark (resets to beginning of stream, for example) + @Test public void testResetMarkSupported() throws IOException { Reader reader = makeReader(); @@ -154,6 +162,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { } } + @Test public void testResetMarkNotSupported() throws IOException { Reader reader = makeReader(); @@ -198,7 +207,7 @@ public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { } } - + @Test public void testReadAfterClose() throws IOException { Reader reader = makeReader("foo bar"); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java index 61b471ab..28dfb036 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java @@ -1,6 +1,8 @@ package com.twelvemonkeys.io; -import junit.framework.TestCase; +import org.junit.Test; + +import static org.junit.Assert.*; /** * SeekableAbstractTestCase @@ -9,14 +11,16 @@ import junit.framework.TestCase; * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java#1 $ */ -public abstract class SeekableAbstractTestCase extends TestCase implements SeekableInterfaceTest { +public abstract class SeekableAbstractTestCase implements SeekableInterfaceTest { protected abstract Seekable createSeekable(); + @Test public void testFail() { - fail(); + fail("Do not create stand-alone test classes based on this class. Instead, create an inner class and delegate to it."); } + @Test public void testSeekable() { assertTrue(createSeekable() instanceof Seekable); } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java index f0c0b844..7a995e49 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java @@ -1,10 +1,14 @@ package com.twelvemonkeys.io; +import org.junit.Test; + import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import static org.junit.Assert.*; + /** * SeekableInputStreamAbstractTestCase *

@@ -13,13 +17,8 @@ import java.io.InputStream; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java#4 $ */ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbstractTestCase implements SeekableInterfaceTest { - - public SeekableInputStreamAbstractTestCase(String name) { - super(name); - } - //// TODO: Figure out a better way of creating interface tests without duplicating code - final SeekableAbstractTestCase mSeekableTestCase = new SeekableAbstractTestCase() { + final SeekableAbstractTestCase seekableTestCase = new SeekableAbstractTestCase() { protected Seekable createSeekable() { return makeInputStream(); } @@ -41,6 +40,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs protected abstract SeekableInputStream makeInputStream(InputStream pStream); + @Test @Override public void testResetAfterReset() throws Exception { InputStream input = makeInputStream(makeOrderedArray(25)); @@ -59,9 +59,9 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs assertTrue("Expected to read positive value", read >= 0); input.reset(); - assertEquals("Expected value read differes from actual", read, input.read()); + assertEquals("Expected value read differs from actual", read, input.read()); - // Reset after read limit passed, may either throw exception, or reset to last mark + // Reset after read limit passed, may either throw exception, or reset to last good mark try { input.reset(); assertEquals("Re-read of reset data should be first", 0, input.read()); @@ -71,10 +71,12 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs } } + @Test public void testSeekable() { - mSeekableTestCase.testSeekable(); + seekableTestCase.testSeekable(); } + @Test public void testFlushBeyondCurrentPos() throws Exception { SeekableInputStream seekable = makeInputStream(20); @@ -88,6 +90,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs } } + @Test public void testSeek() throws Exception { SeekableInputStream seekable = makeInputStream(55); int pos = 37; @@ -97,6 +100,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs assertEquals("Stream positon should match seeked position", pos, streamPos); } + @Test public void testSeekFlush() throws Exception { SeekableInputStream seekable = makeInputStream(133); int pos = 45; @@ -114,6 +118,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs } } + @Test public void testMarkFlushReset() throws Exception { SeekableInputStream seekable = makeInputStream(77); @@ -134,6 +139,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs assertEquals(position, seekable.getStreamPosition()); } + @Test public void testSeekSkipRead() throws Exception { SeekableInputStream seekable = makeInputStream(133); int pos = 45; @@ -147,7 +153,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs } } - public void testSeekSkip(SeekableInputStream pSeekable, String pStr) throws IOException { + protected void testSeekSkip(SeekableInputStream pSeekable, String pStr) throws IOException { System.out.println(); pSeekable.seek(pStr.length()); FileUtil.read(pSeekable); @@ -330,6 +336,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs } */ + @Test public void testReadResetReadDirectBufferBug() throws IOException { // Make sure we use the exact size of the buffer final int size = 1024; @@ -365,6 +372,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs assertTrue(rangeEquals(bytes, size, result, 0, size)); } + @Test public void testReadAllByteValuesRegression() throws IOException { final int size = 128; @@ -401,6 +409,7 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs } + @Test public void testCloseUnderlyingStream() throws IOException { final boolean[] closed = new boolean[1]; @@ -476,5 +485,4 @@ public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbs return true; } - } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java index 42af1ddf..cf1594c9 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java @@ -1,10 +1,13 @@ package com.twelvemonkeys.io; import com.twelvemonkeys.lang.StringUtil; +import org.junit.Test; import java.io.Reader; import java.io.IOException; +import static org.junit.Assert.*; + /** * StringArrayReaderTestCase *

@@ -28,6 +31,7 @@ public class StringArrayReaderTestCase extends ReaderAbstractTestCase { return new StringArrayReader(input); } + @Test public void testNullConstructor() { try { new StringArrayReader(null); @@ -38,15 +42,15 @@ public class StringArrayReaderTestCase extends ReaderAbstractTestCase { } } + @Test public void testEmptyArrayConstructor() throws IOException { Reader reader = new StringArrayReader(new String[0]); assertEquals(-1, reader.read()); } + @Test public void testEmptyStringConstructor() throws IOException { Reader reader = new StringArrayReader(new String[] {""}); assertEquals(-1, reader.read()); } - - } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java index 3a304437..922bf5b5 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java @@ -2,9 +2,12 @@ package com.twelvemonkeys.io.enc; import com.twelvemonkeys.io.FileUtil; +import org.junit.Test; import java.io.*; +import static org.junit.Assert.*; + /** * Base64DecoderTest *

@@ -22,6 +25,7 @@ public class Base64DecoderTestCase extends DecoderAbstractTestCase { return new Base64Encoder(); } + @Test public void testEmptyDecode2() throws IOException { String data = ""; @@ -33,6 +37,7 @@ public class Base64DecoderTestCase extends DecoderAbstractTestCase { assertEquals("Strings does not match", "", new String(bytes.toByteArray())); } + @Test public void testShortDecode() throws IOException { String data = "dGVzdA=="; @@ -44,6 +49,7 @@ public class Base64DecoderTestCase extends DecoderAbstractTestCase { assertEquals("Strings does not match", "test", new String(bytes.toByteArray())); } + @Test public void testLongDecode() throws IOException { String data = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVlciBhZGlwaXNjaW5nIGVsaXQuIEZ1" + "c2NlIGVzdC4gTW9yYmkgbHVjdHVzIGNvbnNlY3RldHVlciBqdXN0by4gVml2YW11cyBkYXBpYnVzIGxh" + @@ -62,4 +68,4 @@ public class Base64DecoderTestCase extends DecoderAbstractTestCase { "ullamcorper, nisi in dictum amet.", new String(bytes.toByteArray())); } -} +} \ No newline at end of file diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java index c8331751..c1177538 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java @@ -1,7 +1,11 @@ package com.twelvemonkeys.io.enc; +import org.junit.Test; + import java.io.*; +import static org.junit.Assert.*; + /** * Base64EncoderTest *

@@ -19,6 +23,7 @@ public class Base64EncoderTestCase extends EncoderAbstractTestCase { return new Base64Decoder(); } + @Test public void testNegativeEncode() throws IOException { Encoder encoder = createEncoder(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -31,6 +36,7 @@ public class Base64EncoderTestCase extends EncoderAbstractTestCase { } } + @Test public void testEmptyEncode() throws IOException { String data = ""; @@ -41,6 +47,7 @@ public class Base64EncoderTestCase extends EncoderAbstractTestCase { assertEquals("Strings does not match", "", new String(bytes.toByteArray())); } + @Test public void testShortEncode() throws IOException { String data = "test"; @@ -51,6 +58,7 @@ public class Base64EncoderTestCase extends EncoderAbstractTestCase { assertEquals("Strings does not match", "dGVzdA==", new String(bytes.toByteArray())); } + @Test public void testLongEncode() throws IOException { String data = "Lorem ipsum dolor sit amet, consectetuer adipiscing " + "elit. Fusce est. Morbi luctus consectetuer justo. Vivamus " + diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java index 6522363e..70dcfa04 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java @@ -2,9 +2,11 @@ package com.twelvemonkeys.io.enc; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; import java.io.*; -import java.util.Arrays; + +import static org.junit.Assert.*; /** * AbstractDecoderTest @@ -22,24 +24,22 @@ public abstract class DecoderAbstractTestCase extends ObjectAbstractTestCase { return createDecoder(); } + @Test(expected = NullPointerException.class) public final void testNullDecode() throws IOException { Decoder decoder = createDecoder(); ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[20]); - try { - decoder.decode(bytes, null); - fail("null should throw NullPointerException"); - } - catch (NullPointerException e) { - } + decoder.decode(bytes, null); + fail("null should throw NullPointerException"); } + @Test public final void testEmptyDecode() throws IOException { Decoder decoder = createDecoder(); ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[0]); try { - int count = decoder.decode(bytes, new byte[2]); + int count = decoder.decode(bytes, new byte[128]); assertEquals("Should not be able to read any bytes", 0, count); } catch (EOFException allowed) { @@ -63,26 +63,25 @@ public abstract class DecoderAbstractTestCase extends ObjectAbstractTestCase { byte[] encoded = outBytes.toByteArray(); byte[] decoded = FileUtil.read(new DecoderStream(new ByteArrayInputStream(encoded), createDecoder())); - assertTrue(Arrays.equals(data, decoded)); + assertArrayEquals(String.format("Data %d", pLength), data, decoded); InputStream in = new DecoderStream(new ByteArrayInputStream(encoded), createDecoder()); outBytes = new ByteArrayOutputStream(); - /* - byte[] buffer = new byte[3]; - for (int n = in.read(buffer); n > 0; n = in.read(buffer)) { - outBytes.write(buffer, 0, n); - } - */ FileUtil.copy(in, outBytes); - outBytes.close(); in.close(); + decoded = outBytes.toByteArray(); - assertTrue(Arrays.equals(data, decoded)); + assertArrayEquals(String.format("Data %d", pLength), data, decoded); } + @Test public final void testStreams() throws Exception { - for (int i = 0; i < 100; i++) { + if (createCompatibleEncoder() == null) { + return; + } + + for (int i = 1; i < 100; i++) { try { runStreamTest(i); } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java index 32266fe6..8bf50fc2 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java @@ -2,11 +2,14 @@ package com.twelvemonkeys.io.enc; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; import java.io.*; import java.util.Arrays; import java.util.Random; +import static org.junit.Assert.*; + /** * AbstractEncoderTest *

@@ -26,6 +29,7 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase { return createEncoder(); } + @Test public final void testNullEncode() throws IOException { Encoder encoder = createEncoder(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -79,6 +83,7 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase { assertTrue(Arrays.equals(data, decoded)); } + @Test public final void testStreams() throws Exception { for (int i = 0; i < 100; i++) { try { diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java index b69b57f0..584be832 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java @@ -1,10 +1,5 @@ package com.twelvemonkeys.io.enc; -import com.twelvemonkeys.io.enc.Decoder; -import com.twelvemonkeys.io.enc.Encoder; -import com.twelvemonkeys.io.enc.PackBitsDecoder; -import com.twelvemonkeys.io.enc.PackBitsEncoder; - /** * PackBitsDecoderTest *

diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java index 24d9b3da..ccbf18da 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java @@ -1,6 +1,7 @@ package com.twelvemonkeys.io.ole2; -import junit.framework.TestCase; +import com.twelvemonkeys.io.MemoryCacheSeekableStream; +import org.junit.Test; import javax.imageio.stream.MemoryCacheImageInputStream; import java.io.File; @@ -9,6 +10,10 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteOrder; +import java.util.SortedSet; +import java.util.TreeSet; + +import static org.junit.Assert.*; /** * CompoundDocumentTestCase @@ -17,9 +22,89 @@ import java.nio.ByteOrder; * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java#1 $ */ -public class CompoundDocumentTestCase extends TestCase { +public class CompoundDocumentTestCase { + + private static final String SAMPLE_DATA = "/Thumbs-camera.db"; + + protected final CompoundDocument createTestDocument() throws IOException { + URL input = getClass().getResource(SAMPLE_DATA); + + assertNotNull("Missing test resource!", input); + assertEquals("Test resource not a file:// resource", "file", input.getProtocol()); + + try { + return new CompoundDocument(new File(input.toURI())); + } + catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Test + public void testRoot() throws IOException { + CompoundDocument document = createTestDocument(); + + Entry root = document.getRootEntry(); + + assertNotNull(root); + assertEquals("Root Entry", root.getName()); + assertTrue(root.isRoot()); + assertFalse(root.isFile()); + assertFalse(root.isDirectory()); + assertEquals(0, root.length()); + assertNull(root.getInputStream()); + } + + @Test + public void testContents() throws IOException { + CompoundDocument document = createTestDocument(); + + Entry root = document.getRootEntry(); + + assertNotNull(root); + + SortedSet children = new TreeSet(root.getChildEntries()); + assertEquals(25, children.size()); + + // Weirdness in the file format, name is *written backwards* 1-24 + Catalog + for (String name : "1,2,3,4,5,6,7,8,9,01,02,11,12,21,22,31,32,41,42,51,61,71,81,91,Catalog".split(",")) { + assertEquals(name, children.first().getName()); + children.remove(children.first()); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testChildEntriesUnmodifiable() throws IOException { + CompoundDocument document = createTestDocument(); + + Entry root = document.getRootEntry(); + + assertNotNull(root); + + SortedSet children = root.getChildEntries(); + + // Should not be allowed, as it modifies the internal structure + children.remove(children.first()); + } + + @Test + public void testReadThumbsCatalogFile() throws IOException { + CompoundDocument document = createTestDocument(); + + Entry root = document.getRootEntry(); + + assertNotNull(root); + assertEquals(25, root.getChildEntries().size()); + + Entry catalog = root.getChildEntry("Catalog"); + + assertNotNull(catalog); + assertNotNull("Input stream may not be null", catalog.getInputStream()); + } + + @Test public void testReadCatalogInputStream() throws IOException { - InputStream input = getClass().getResourceAsStream("/Thumbs-camera.db"); + InputStream input = getClass().getResourceAsStream(SAMPLE_DATA); assertNotNull("Missing test resource!", input); @@ -33,8 +118,25 @@ public class CompoundDocumentTestCase extends TestCase { assertNotNull("Input stream may not be null", catalog.getInputStream()); } + @Test + public void testReadCatalogSeekableStream() throws IOException { + InputStream input = getClass().getResourceAsStream(SAMPLE_DATA); + + assertNotNull("Missing test resource!", input); + + CompoundDocument document = new CompoundDocument(new MemoryCacheSeekableStream(input)); + Entry root = document.getRootEntry(); + assertNotNull(root); + assertEquals(25, root.getChildEntries().size()); + + Entry catalog = root.getChildEntry("Catalog"); + assertNotNull(catalog); + assertNotNull("Input stream may not be null", catalog.getInputStream()); + } + + @Test public void testReadCatalogImageInputStream() throws IOException { - InputStream input = getClass().getResourceAsStream("/Thumbs-camera.db"); + InputStream input = getClass().getResourceAsStream(SAMPLE_DATA); assertNotNull("Missing test resource!", input); @@ -53,25 +155,4 @@ public class CompoundDocumentTestCase extends TestCase { assertNotNull(catalog); assertNotNull("Input stream may not be null", catalog.getInputStream()); } - - public void testReadThumbsCatalogFile() throws IOException, URISyntaxException { - URL input = getClass().getResource("/Thumbs-camera.db"); - - assertNotNull("Missing test resource!", input); - assertEquals("Test resource not a file:// resource", "file", input.getProtocol()); - - File file = new File(input.toURI()); - - CompoundDocument document = new CompoundDocument(file); - - Entry root = document.getRootEntry(); - - assertNotNull(root); - assertEquals(25, root.getChildEntries().size()); - - Entry catalog = root.getChildEntry("Catalog"); - - assertNotNull(catalog); - assertNotNull("Input stream may not be null", catalog.getInputStream()); - } } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java new file mode 100644 index 00000000..d4e426d6 --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.io.ole2; + +import com.twelvemonkeys.io.*; +import org.junit.Test; + +import java.io.ByteArrayInputStream; + +/** + * CompoundDocument_SeekableLittleEndianDataInputStreamTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java,v 1.0 18.10.11 16:35 haraldk Exp$ + */ +public class CompoundDocument_SeekableLittleEndianDataInputStreamTestCase extends InputStreamAbstractTestCase implements SeekableInterfaceTest { + private final SeekableInterfaceTest seekableTest = new SeekableAbstractTestCase() { + @Override + protected Seekable createSeekable() { + return (Seekable) makeInputStream(); + } + }; + + @Override + protected CompoundDocument.SeekableLittleEndianDataInputStream makeInputStream(byte[] pBytes) { + return new CompoundDocument.SeekableLittleEndianDataInputStream(new MemoryCacheSeekableStream(new ByteArrayInputStream(pBytes))); + } + + @Test + public void testSeekable() { + seekableTest.testSeekable(); + } +} diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_StreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_StreamTestCase.java new file mode 100644 index 00000000..14039eae --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_StreamTestCase.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.io.ole2; + +import com.twelvemonkeys.io.InputStreamAbstractTestCase; +import com.twelvemonkeys.io.LittleEndianDataOutputStream; +import com.twelvemonkeys.io.MemoryCacheSeekableStream; +import com.twelvemonkeys.io.SeekableInputStream; +import org.junit.Test; + +import java.io.*; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * CompoundDocument_StreamTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CompoundDocument_StreamTestCase.java,v 1.0 13.10.11 12:01 haraldk Exp$ + */ +//@Ignore("Need proper in-memory creation of CompoundDocuments") +public class CompoundDocument_StreamTestCase extends InputStreamAbstractTestCase { + private static final String SAMPLE_DATA = "/Thumbs-camera.db"; + + protected final CompoundDocument createTestDocument() throws IOException { + URL input = getClass().getResource(SAMPLE_DATA); + + assertNotNull("Missing test resource!", input); + assertEquals("Test resource not a file:// resource", "file", input.getProtocol()); + + try { + return new CompoundDocument(new File(input.toURI())); + } + catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + private SeekableInputStream createRealInputStream() { + try { + Entry first = createTestDocument().getRootEntry().getChildEntries().first(); + assertNotNull(first); + return first.getInputStream(); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + protected InputStream makeInputStream(byte[] data) { + try { + // Set up fake document + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + LittleEndianDataOutputStream dataStream = new LittleEndianDataOutputStream(stream); + + dataStream.write(CompoundDocument.MAGIC); // 8 bytes magic + dataStream.write(new byte[16]); // UUID 16 bytes, all zero + dataStream.write(new byte[]{0x3E, 0, 3, 0}); // version (62), rev (3) + // 28 + dataStream.write(new byte[]{(byte) 0xfe, (byte) 0xff}); // Byte order + dataStream.write(new byte[]{9, 0, 6, 0}); // Sector size (1 << x), short sector size + dataStream.write(new byte[10]); // Reserved 10 bytes + // 44 + dataStream.writeInt(1); // SAT size (1) + dataStream.writeInt(1); // Directory SId + dataStream.write(new byte[4]); // Reserved 4 bytes + // 56 + dataStream.writeInt(4096); // Min stream size (4096) + dataStream.writeInt(3); // Short SAT SId + dataStream.writeInt(1); // Short SAT size + dataStream.writeInt(-2); // Master SAT SId (-2, end of chain) + // 72 + dataStream.writeInt(0); // Master SAT size + dataStream.writeInt(0); // Master SAT entry 0 (0) + dataStream.writeInt(128); // Master SAT entry 1 (128) + // 84 + dataStream.write(createPad(428, (byte) -1)); // Pad (until 512 bytes) + // 512 -- end header + + // SId 0 + // SAT + dataStream.writeInt(-3); // SAT entry 0 (SAT) + dataStream.writeInt(-2); // SAT entry 1 (EOS) + dataStream.write(createPad(512 - 8, (byte) -1)); // Pad (until 512 bytes) + // 1024 -- end SAT + + // SId 1 + // Directory + // 64 bytes UTF16LE ("Root Entry" + null-termination) + byte[] name = "Root Entry".getBytes(Charset.forName("UTF-16LE")); + dataStream.write(name); // Name + dataStream.write(createPad(64 - name.length, (byte) 0)); // Pad name to 64 bytes + dataStream.writeShort((short) (name.length + 2)); // 2 byte length (incl null-term) + dataStream.write(new byte[]{5, 0}); // type (root), node color + dataStream.writeInt(-1); // prevDId, -1 + dataStream.writeInt(-1); // nextDId, -1 + dataStream.writeInt(1); // rootNodeDId + dataStream.write(createPad(36, (byte) 0)); // UID + flags + 2 x long timestamps + dataStream.writeInt(2); // Start SId + dataStream.writeInt(8); // Stream size + dataStream.writeInt(0); // Reserved + + name = "data".getBytes(Charset.forName("UTF-16LE")); + dataStream.write(name); // Name + dataStream.write(createPad(64 - name.length, (byte) 0)); // Pad name to 64 bytes + dataStream.writeShort((short) (name.length + 2)); // 2 byte length (incl null-term) + dataStream.write(new byte[]{2, 0}); // type (user stream), node color + dataStream.writeInt(-1); // prevDId, -1 + dataStream.writeInt(-1); // nextDId, -1 + dataStream.writeInt(-1); // rootNodeDId + dataStream.write(createPad(36, (byte) 0)); // UID + flags + 2 x long timestamps + dataStream.writeInt(0); // Start SId + dataStream.writeInt(data.length); // Stream size + dataStream.writeInt(0); // Reserved + + dataStream.write(createPad(512 - 256, (byte) -1)); // Pad to full sector (512 bytes) + // 1536 -- end Directory + + // SId 2 + // Data + dataStream.write(data); // The data + dataStream.write(createPad(512 - data.length, (byte) -1)); // Pad to full sector (512 bytes) + // 2048 -- end Data + + // SId 3 + // Short SAT + dataStream.writeInt(2); // Short SAT entry 0 + dataStream.writeInt(-2); // Short SAT entry 1 (EOS) + dataStream.write(createPad(512 - 8, (byte) -1)); // Pad to full sector (512 bytes) + // 2560 -- end Short SAT + + + InputStream input = new ByteArrayInputStream(stream.toByteArray()); + CompoundDocument document = new CompoundDocument(new MemoryCacheSeekableStream(input)); + + Entry entry = document.getRootEntry().getChildEntries().first(); + + return entry.getInputStream(); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + private byte[] createPad(final int length, final byte val) { + byte[] pad = new byte[length]; + Arrays.fill(pad, val); + return pad; + } + +// @Ignore + @Test + public void testDev() throws IOException { + InputStream stream = makeInputStream(makeOrderedArray(32)); + + int read; + int count = 0; + while ((read = stream.read()) >= 0) { +// System.out.printf("read %02d: 0x%02x%n", count, read & 0xFF); + assertEquals(count, read); + count++; + } + + assertFalse("Short stream", count < 32); + assertFalse("Stream overrun", count > 32); + } + + @Test + public void testInputStreamSkip() throws IOException { + InputStream stream = makeInputStream(); + + // BUGFIX: Would skip and return 0 for first skip + assertTrue(stream.skip(10) > 0); + } +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java index e35aca9f..0c09176d 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/BeanUtil.java @@ -276,19 +276,19 @@ public final class BeanUtil { } try { - // If this does not throw an excption, it works + // If this does not throw an exception, it works method = pObject.getClass().getMethod(pName, pParams); } catch (Throwable t) { // Ignore } - // 2: Try any supertypes of paramType, to see if we have a match + // 2: Try any super-types of paramType, to see if we have a match if (method == null) { while ((paramType = paramType.getSuperclass()) != null) { pParams[0] = paramType; try { - // If this does not throw an excption, it works + // If this does not throw an exception, it works method = pObject.getClass().getMethod(pName, pParams); } catch (Throwable t) { @@ -365,6 +365,9 @@ public final class BeanUtil { } } + // TODO: Convert value to single-value array if needed + // TODO: Convert CSV String to string array (or potentially any type of array) + // TODO: Convert other types if (pValue instanceof String) { Converter converter = Converter.getInstance(); @@ -596,8 +599,7 @@ public final class BeanUtil { catch (NoSuchMethodException ignore) { // If invocation failed, convert lisp-style and try again if (pLispToCamel && property.indexOf('-') > 0) { - setPropertyValue(pBean, StringUtil.lispToCamel(property, false), - entry.getValue()); + setPropertyValue(pBean, StringUtil.lispToCamel(property, false), entry.getValue()); } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java index 7b9e5da3..09e9f5e9 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/DateUtil.java @@ -30,9 +30,6 @@ package com.twelvemonkeys.lang; import java.util.Date; import java.util.TimeZone; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.text.ParseException; /** * A utility class with useful date manipulation methods and constants. @@ -191,20 +188,4 @@ public final class DateUtil { int offset = pTimeZone.getOffset(pTime); return (((pTime + offset) / DAY) * DAY) - offset; } - - public static void main(String[] pArgs) throws ParseException { - DateFormat format = new SimpleDateFormat("yyyy.MM.dd HH.mm.ss S"); - - long time = pArgs.length > 0 ? format.parse(pArgs[0]).getTime() : System.currentTimeMillis(); - - System.out.println(time + ": " + format.format(new Date(time))); - time = roundToSecond(time); - System.out.println(time + ": " + format.format(new Date(time))); - time = roundToMinute(time); - System.out.println(time + ": " + format.format(new Date(time))); - time = roundToHour(time); - System.out.println(time + ": " + format.format(new Date(time))); - time = roundToDay(time); - System.out.println(time + ": " + format.format(new Date(time))); - } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java deleted file mode 100755 index fe9cd78c..00000000 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.twelvemonkeys.lang; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.reflect.UndeclaredThrowableException; -import java.sql.SQLException; - -/** - * ExceptionUtil - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java#2 $ - */ -public final class ExceptionUtil { - - /*public*/ static void launder(final Throwable pThrowable, Class... pExpectedTypes) { - if (pThrowable instanceof Error) { - throw (Error) pThrowable; - } - if (pThrowable instanceof RuntimeException) { - throw (RuntimeException) pThrowable; - } - - for (Class expectedType : pExpectedTypes) { - if (expectedType.isInstance(pThrowable)) { - throw new RuntimeException(pThrowable); - } - } - - throw new UndeclaredThrowableException(pThrowable); - } - - @SuppressWarnings({"unchecked", "UnusedDeclaration"}) - static void throwAs(final Class pType, final Throwable pThrowable) throws T { - throw (T) pThrowable; - } - - public static void throwUnchecked(final Throwable pThrowable) { - throwAs(RuntimeException.class, pThrowable); - } - - /*public*/ static void handle(final Throwable pThrowable, final ThrowableHandler... pHandler) { - handleImpl(pThrowable, pHandler); - } - - @SuppressWarnings({"unchecked"}) - private static void handleImpl(final Throwable pThrowable, final ThrowableHandler... pHandler) { - // TODO: Sort more specific throwable handlers before less specific? - for (ThrowableHandler handler : pHandler) { - if (handler.handles(pThrowable)) { - handler.handle((T) pThrowable); - return; - } - } - throwUnchecked(pThrowable); - } - - public static abstract class ThrowableHandler { - private Class[] mThrowables; - - protected ThrowableHandler(final Class... pThrowables) { - // TODO: Assert not null - mThrowables = pThrowables.clone(); - } - - final public boolean handles(final Throwable pThrowable) { - for (Class throwable : mThrowables) { - if (throwable.isAssignableFrom(pThrowable.getClass())) { - return true; - } - } - return false; - } - - public abstract void handle(T pThrowable); - } - - @SuppressWarnings({"InfiniteLoopStatement"}) - public static void main(String[] pArgs) { - while (true) { - foo(); - } - } - - private static void foo() { - try { - bar(); - } - catch (Throwable t) { - handle(t, - new ThrowableHandler(IOException.class) { - public void handle(final IOException pThrowable) { - System.out.println("IOException: " + pThrowable + " handled"); - } - }, - new ThrowableHandler(SQLException.class, NumberFormatException.class) { - public void handle(final Exception pThrowable) { - System.out.println("Exception: " + pThrowable + " handled"); - } - }, - new ThrowableHandler(Throwable.class) { - public void handle(final Throwable pThrowable) { - System.err.println("Generic throwable: " + pThrowable + " NOT handled"); - throwUnchecked(pThrowable); - } - } - ); - } - } - - private static void bar() { - baz(); - } - - @SuppressWarnings({"ThrowableInstanceNeverThrown"}) - private static void baz() { - double random = Math.random(); - if (random < (1.0 / 3.0)) { - throwUnchecked(new FileNotFoundException("FNF Boo")); - } - if (random < (2.0 / 3.0)) { - throwUnchecked(new SQLException("SQL Boo")); - } - else { - throwUnchecked(new Exception("Some Boo")); - } - } -} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/MathUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/MathUtil.java index c8a2c38e..2139b411 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/MathUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/MathUtil.java @@ -43,36 +43,6 @@ public final class MathUtil { /** */ private MathUtil() { } - - /** - * Converts an angle measured in degrees to the equivalent angle measured - * in radians. - * This method is a replacement for the Math.toRadians() method in - * Java versions < 1.2 (typically for Applets). - * - * @param pAngDeg an angle, in degrees - * @return the measurement of the angle {@code angdeg} in radians. - * - * @see java.lang.Math#toRadians(double) - */ - public static double toRadians(final double pAngDeg) { - return pAngDeg * Math.PI / 180.0; - } - - /** - * Converts an angle measured in radians to the equivalent angle measured - * in degrees. - * This method is a replacement for the Math.toDegrees() method in - * Java versions < 1.2 (typically for Applets). - * - * @param pAngRad an angle, in radians - * @return the measurement of the angle {@code angrad} in degrees. - * - * @see java.lang.Math#toDegrees(double) - */ - public static double toDegrees(final double pAngRad) { - return pAngRad * 180.0 / Math.PI; - } /** * Returns the natural logarithm (base e) of a double value. @@ -135,7 +105,7 @@ public final class MathUtil { * @see Math#abs(long) * @see Long#MIN_VALUE * - * @param pNumber + * @param pNumber a number * @return the absolute value of {@code pNumber} * * @throws ArithmeticException if {@code pNumber == Long.MIN_VALUE} @@ -144,6 +114,7 @@ public final class MathUtil { if (pNumber == Long.MIN_VALUE) { throw new ArithmeticException("long overflow: 9223372036854775808"); } + return (pNumber < 0) ? -pNumber : pNumber; } @@ -154,7 +125,7 @@ public final class MathUtil { * @see Math#abs(int) * @see Integer#MIN_VALUE * - * @param pNumber + * @param pNumber a number * @return the absolute value of {@code pNumber} * * @throws ArithmeticException if {@code pNumber == Integer.MIN_VALUE} @@ -163,6 +134,7 @@ public final class MathUtil { if (pNumber == Integer.MIN_VALUE) { throw new ArithmeticException("int overflow: 2147483648"); } + return (pNumber < 0) ? -pNumber : pNumber; } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java index 76c85eab..7fb98e4c 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/Platform.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.lang; +import java.util.Properties; + /** * Platform * @@ -39,40 +41,46 @@ public final class Platform { /** * Normalized operating system constant */ - final OperatingSystem mOS; + final OperatingSystem os; /** - * Unormalized operating system version constant (for completeness) + * Unnormalized operating system version constant (for completeness) */ - final String mVersion; + final String version; /** * Normalized system architecture constant */ - final Architecture mArchitecture; + final Architecture architecture; static final private Platform INSTANCE = new Platform(); private Platform() { - mOS = normalizeOperatingSystem(); - mVersion = System.getProperty("os.version"); - mArchitecture = normalizeArchitecture(mOS); + this(System.getProperties()); } - private static OperatingSystem normalizeOperatingSystem() { - String os = System.getProperty("os.name"); + Platform(final Properties properties) { + os = normalizeOperatingSystem(properties.getProperty("os.name")); + version = properties.getProperty("os.version"); + architecture = normalizeArchitecture(os, properties.getProperty("os.arch")); + } + + static OperatingSystem normalizeOperatingSystem(final String osName) { + String os = osName; + if (os == null) { throw new IllegalStateException("System property \"os.name\" == null"); } os = os.toLowerCase(); + if (os.startsWith("windows")) { return OperatingSystem.Windows; } else if (os.startsWith("linux")) { return OperatingSystem.Linux; } - else if (os.startsWith("mac os")) { + else if (os.startsWith("mac os") || os.startsWith("darwin")) { return OperatingSystem.MacOS; } else if (os.startsWith("solaris") || os.startsWith("sunos")) { @@ -82,15 +90,16 @@ public final class Platform { return OperatingSystem.Unknown; } - private static Architecture normalizeArchitecture(final OperatingSystem pOsName) { - String arch = System.getProperty("os.arch"); + static Architecture normalizeArchitecture(final OperatingSystem pOsName, final String osArch) { + String arch = osArch; + if (arch == null) { throw new IllegalStateException("System property \"os.arch\" == null"); } arch = arch.toLowerCase(); - if (pOsName == OperatingSystem.Windows - && (arch.startsWith("x86") || arch.startsWith("i386"))) { + + if (pOsName == OperatingSystem.Windows && (arch.startsWith("x86") || arch.startsWith("i386"))) { return Architecture.X86; // TODO: 64 bit } @@ -101,6 +110,9 @@ public final class Platform { else if (arch.startsWith("i686")) { return Architecture.I686; } + else if (arch.startsWith("power") || arch.startsWith("ppc")) { + return Architecture.PPC; + } // TODO: More Linux options? // TODO: 64 bit } @@ -108,9 +120,13 @@ public final class Platform { if (arch.startsWith("power") || arch.startsWith("ppc")) { return Architecture.PPC; } - else if (arch.startsWith("i386")) { - return Architecture.I386; + else if (arch.startsWith("x86")) { + return Architecture.X86; } + else if (arch.startsWith("i386")) { + return Architecture.X86; + } + // TODO: 64 bit } else if (pOsName == OperatingSystem.Solaris) { if (arch.startsWith("sparc")) { @@ -138,21 +154,21 @@ public final class Platform { * @return this platform's OS. */ public OperatingSystem getOS() { - return mOS; + return os; } /** * @return this platform's OS version. */ public String getVersion() { - return mVersion; + return version; } /** * @return this platform's architecture. */ public Architecture getArchitecture() { - return mArchitecture; + return architecture; } /** @@ -160,7 +176,7 @@ public final class Platform { * @return the current {@code OperatingSystem}. */ public static OperatingSystem os() { - return INSTANCE.mOS; + return INSTANCE.os; } /** @@ -168,7 +184,7 @@ public final class Platform { * @return the current OS version. */ public static String version() { - return INSTANCE.mVersion; + return INSTANCE.version; } /** @@ -176,7 +192,7 @@ public final class Platform { * @return the current {@code Architecture}. */ public static Architecture arch() { - return INSTANCE.mArchitecture; + return INSTANCE.architecture; } /** @@ -197,14 +213,14 @@ public final class Platform { Unknown(System.getProperty("os.arch")); - final String mName;// for debug only + final String name;// for debug only private Architecture(String pName) { - mName = pName; + name = pName; } public String toString() { - return mName; + return name; } } @@ -223,22 +239,26 @@ public final class Platform { Solaris("Solaris", "sun"), MacOS("Mac OS", "osx"), - Unknown(System.getProperty("os.name"), ""); + Unknown(System.getProperty("os.name"), null); - final String mId; - final String mName;// for debug only + final String id; + final String name;// for debug only private OperatingSystem(String pName, String pId) { - mName = pName; - mId = pId; + name = pName; + id = pId != null ? pId : pName.toLowerCase(); } public String getName() { - return mName; + return name; + } + + public String id() { + return id; } public String toString() { - return mId; + return String.format("%s (%s)", id, name); } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/StringUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/StringUtil.java index b7dc34e1..1d244b72 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/StringUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/StringUtil.java @@ -77,7 +77,7 @@ public final class StringUtil { } /** - * Constructs a new {@link String} by decoding the specified subarray of bytes using the specified charset. + * Constructs a new {@link String} by decoding the specified sub array of bytes using the specified charset. * Replacement for {@link String#String(byte[], int, int, String) new String(byte[], int, int, String)}, that does * not throw the checked {@link UnsupportedEncodingException}, * but instead the unchecked {@link UnsupportedCharsetException} if the character set is not supported. @@ -1580,7 +1580,7 @@ public final class StringUtil { * Converts a string array to a string separated by the given delimiter. * * @param pStringArray the string array - * @param pDelimiterString the delimter string + * @param pDelimiterString the delimiter string * @return string of delimiter separated values * @throws IllegalArgumentException if {@code pDelimiterString == null} */ diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/SystemUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/SystemUtil.java index 8f3712e4..9e79347e 100644 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/SystemUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/SystemUtil.java @@ -81,8 +81,7 @@ public final class SystemUtil { * * @return an input stream reading from the resource */ - private static InputStream getResourceAsStream(ClassLoader pClassLoader, String pName, - boolean pGuessSuffix) { + private static InputStream getResourceAsStream(ClassLoader pClassLoader, String pName, boolean pGuessSuffix) { InputStream is; if (!pGuessSuffix) { @@ -122,8 +121,7 @@ public final class SystemUtil { * * @return an input stream reading from the resource */ - private static InputStream getFileAsStream(String pName, - boolean pGuessSuffix) { + private static InputStream getFileAsStream(String pName, boolean pGuessSuffix) { InputStream is = null; File propertiesFile; @@ -206,8 +204,7 @@ public final class SystemUtil { * @todo Reconsider ever using the System ClassLoader: http://www.javaworld.com/javaworld/javaqa/2003-06/01-qa-0606-load.html * @todo Consider using Context Classloader instead? */ - public static Properties loadProperties(Class pClass, String pName) - throws IOException + public static Properties loadProperties(Class pClass, String pName) throws IOException { // Convert to name the classloader understands String name = !StringUtil.isEmpty(pName) ? pName : pClass.getName().replace('.', '/'); @@ -219,8 +216,7 @@ public final class SystemUtil { // TODO: WHAT IF MULTIPLE RESOURCES EXISTS?! // Try loading resource through the current class' classloader - if (pClass != null - && (is = getResourceAsStream(pClass.getClassLoader(), name, guessSuffix)) != null) { + if (pClass != null && (is = getResourceAsStream(pClass.getClassLoader(), name, guessSuffix)) != null) { //&& (is = getResourceAsStream(pClass, name, guessSuffix)) != null) { // Nothing to do //System.out.println(((is instanceof XMLPropertiesInputStream) ? @@ -228,9 +224,8 @@ public final class SystemUtil { // + " from Class' ClassLoader"); } // If that fails, try the system classloader - else if ((is = getResourceAsStream(ClassLoader.getSystemClassLoader(), - name, guessSuffix)) != null) { - //else if ((is = getSystemResourceAsStream(name, guessSuffix)) != null) { + else if ((is = getResourceAsStream(ClassLoader.getSystemClassLoader(), name, guessSuffix)) != null) { + //else if ((is = getSystemResourceAsStream(name, guessSuffix)) != null) { // Nothing to do //System.out.println(((is instanceof XMLPropertiesInputStream) ? // "XML-properties" : "Normal .properties") @@ -682,8 +677,8 @@ public final class SystemUtil { } private static Class getClass(String pClassName, boolean pInitialize, ClassLoader pLoader) throws ClassNotFoundException { - // NOTE: We need the context classloader, as SystemUtil's - // classloader may have a totally different classloader than + // NOTE: We need the context class loader, as SystemUtil's + // class loader may have a totally different class loader than // the original caller class (as in Class.forName(cn, false, null)). ClassLoader loader = pLoader != null ? pLoader : Thread.currentThread().getContextClassLoader(); diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/Validate.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/Validate.java index 1fe91775..16897f31 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/Validate.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/Validate.java @@ -9,13 +9,15 @@ import java.util.Map; *

* Uses type parameterized return values, thus making it possible to check * constructor arguments before - * they are passed on to {@code super} or {@code this} type constructors. + * they are passed on to {@code super} or {@code this} type constructors. * * @author Harald Kuhr * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Validate.java#1 $ */ public final class Validate { + // TODO: Make it possible to throw IllegalStateException instead of IllegalArgumentException? + private static final String UNSPECIFIED_PARAM_NAME = "method parameter"; private Validate() {} @@ -30,23 +32,34 @@ public final class Validate { if (pParameter == null) { throw new IllegalArgumentException(String.format("%s may not be null", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); } + return pParameter; } - // Not empty... + // Not empty public static T notEmpty(final T pParameter) { return notEmpty(pParameter, null); } public static T notEmpty(final T pParameter, final String pParamName) { - if (pParameter == null || pParameter.length() == 0) { - throw new IllegalArgumentException(String.format("%s may not be empty", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + if (pParameter == null || pParameter.length() == 0 || isOnlyWhiteSpace(pParameter)) { + throw new IllegalArgumentException(String.format("%s may not be blank", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); } return pParameter; } + private static boolean isOnlyWhiteSpace(T pParameter) { + for (int i = 0; i < pParameter.length(); i++) { + if (!Character.isWhitespace(pParameter.charAt(i))) { + return false; + } + } + + return true; + } + public static T[] notEmpty(final T[] pParameter) { return notEmpty(pParameter, null); } @@ -90,7 +103,7 @@ public final class Validate { } public static T[] noNullElements(final T[] pParameter, final String pParamName) { - noNullElements(Arrays.asList(pParameter), pParamName); + noNullElements(pParameter == null ? null : Arrays.asList(pParameter), pParamName); return pParameter; } @@ -99,6 +112,8 @@ public final class Validate { } public static Collection noNullElements(final Collection pParameter, final String pParamName) { + notNull(pParameter, pParamName); + for (T element : pParameter) { if (element == null) { throw new IllegalArgumentException(String.format("%s may not contain null elements", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); @@ -108,17 +123,49 @@ public final class Validate { return pParameter; } - public static Map noNullElements(final Map pParameter) { - return noNullElements(pParameter, null); + public static Map noNullValues(final Map pParameter) { + return noNullValues(pParameter, null); } - public static Map noNullElements(final Map pParameter, final String pParamName) { - for (V element : pParameter.values()) { - if (element == null) { - throw new IllegalArgumentException(String.format("%s may not contain null elements", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + public static Map noNullValues(final Map pParameter, final String pParamName) { + notNull(pParameter, pParamName); + + for (V value : pParameter.values()) { + if (value == null) { + throw new IllegalArgumentException(String.format("%s may not contain null values", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); } } return pParameter; } + + public static Map noNullKeys(final Map pParameter) { + return noNullKeys(pParameter, null); + } + + public static Map noNullKeys(final Map pParameter, final String pParamName) { + notNull(pParameter, pParamName); + + for (K key : pParameter.keySet()) { + if (key == null) { + throw new IllegalArgumentException(String.format("%s may not contain null keys", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + } + + return pParameter; + } + + // Is true + + public static boolean isTrue(final boolean pExpression, final String pMessage) { + return isTrue(pExpression, pExpression, pMessage); + } + + public static T isTrue(final boolean condition, final T value, final String message) { + if (!condition) { + throw new IllegalArgumentException(String.format(message == null ? "expression may not be %s" : message, value)); + } + + return value; + } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java index 32dd2822..ab4e4fb7 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java @@ -40,12 +40,12 @@ import java.io.Serializable; */ // TODO: The generics in this class looks suspicious.. abstract class AbstractDecoratedMap extends AbstractMap implements Map, Serializable, Cloneable { - protected Map> mEntries; - protected transient volatile int mModCount; + protected Map> entries; + protected transient volatile int modCount; - private transient volatile Set> mEntrySet = null; - private transient volatile Set mKeySet = null; - private transient volatile Collection mValues = null; + private transient volatile Set> entrySet = null; + private transient volatile Set keySet = null; + private transient volatile Collection values = null; /** * Creates a {@code Map} backed by a {@code HashMap}. @@ -104,7 +104,7 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M throw new IllegalArgumentException("backing must be empty"); } - mEntries = pBacking; + this.entries = pBacking; init(); if (pContents != null) { @@ -125,21 +125,21 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M } public int size() { - return mEntries.size(); + return entries.size(); } public void clear() { - mEntries.clear(); - mModCount++; + entries.clear(); + modCount++; init(); } public boolean isEmpty() { - return mEntries.isEmpty(); + return entries.isEmpty(); } public boolean containsKey(Object pKey) { - return mEntries.containsKey(pKey); + return entries.containsKey(pKey); } /** @@ -166,18 +166,18 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M } public Collection values() { - Collection values = mValues; - return values != null ? values : (mValues = new Values()); + Collection values = this.values; + return values != null ? values : (this.values = new Values()); } public Set> entrySet() { - Set> es = mEntrySet; - return es != null ? es : (mEntrySet = new EntrySet()); + Set> es = entrySet; + return es != null ? es : (entrySet = new EntrySet()); } public Set keySet() { - Set ks = mKeySet; - return ks != null ? ks : (mKeySet = new KeySet()); + Set ks = keySet; + return ks != null ? ks : (keySet = new KeySet()); } /** @@ -189,9 +189,9 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M protected Object clone() throws CloneNotSupportedException { AbstractDecoratedMap map = (AbstractDecoratedMap) super.clone(); - map.mValues = null; - map.mEntrySet = null; - map.mKeySet = null; + map.values = null; + map.entrySet = null; + map.keySet = null; // TODO: Implement: Need to clone the backing map... @@ -217,7 +217,7 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M } /*protected*/ Entry getEntry(K pKey) { - return mEntries.get(pKey); + return entries.get(pKey); } /** @@ -271,7 +271,7 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M Entry e = (Entry) o; //noinspection SuspiciousMethodCalls - Entry candidate = mEntries.get(e.getKey()); + Entry candidate = entries.get(e.getKey()); return candidate != null && candidate.equals(e); } @@ -284,7 +284,7 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M // NOTE: Extra cautions is taken, to only remove the entry if it // equals the entry in the map Object key = ((Entry) o).getKey(); - Entry entry = (Entry) mEntries.get(key); + Entry entry = (Entry) entries.get(key); // Same entry? if (entry != null && entry.equals(o)) { diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/BeanMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/BeanMap.java index 75d40df2..8f830c00 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/BeanMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/BeanMap.java @@ -40,20 +40,20 @@ import java.io.Serializable; /** * A {@code Map} adapter for a Java Bean. *

- * Ruhtlessly stolen from + * Ruthlessly stolen from * initDescriptors(Object pBean) throws IntrospectionException { @@ -100,7 +100,7 @@ public final class BeanMap extends AbstractMap implements Serial } public int size() { - return mDescriptors.size(); + return descriptors.size(); } private String checkKey(final Object pKey) { @@ -119,17 +119,17 @@ public final class BeanMap extends AbstractMap implements Serial private Object readResolve() throws IntrospectionException { // Initialize the property descriptors - mDescriptors = initDescriptors(mBean); + descriptors = initDescriptors(bean); return this; } private class BeanSet extends AbstractSet> { public Iterator> iterator() { - return new BeanIterator(mDescriptors.iterator()); + return new BeanIterator(descriptors.iterator()); } public int size() { - return mDescriptors.size(); + return descriptors.size(); } } @@ -173,7 +173,7 @@ public final class BeanMap extends AbstractMap implements Serial throw new UnsupportedOperationException("No getter: " + mDescriptor.getName()); } - return method.invoke(mBean); + return method.invoke(bean); } }); } @@ -188,7 +188,7 @@ public final class BeanMap extends AbstractMap implements Serial } final Object old = getValue(); - method.invoke(mBean, pValue); + method.invoke(bean, pValue); return old; } }); diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/CollectionUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/CollectionUtil.java index 0b15be9f..63e205ba 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/CollectionUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/CollectionUtil.java @@ -28,17 +28,21 @@ package com.twelvemonkeys.util; +import com.twelvemonkeys.lang.Validate; + import java.lang.reflect.Array; import java.util.*; +import static com.twelvemonkeys.lang.Validate.isTrue; +import static com.twelvemonkeys.lang.Validate.notNull; + /** * A utility class with some useful collection-related functions. * * @author Harald Kuhr * @author Eirik Torske * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/CollectionUtil.java#3 $ - * @todo move makeList and makeSet to StringUtil? + * @version $Id: com/twelvemonkeys/util/CollectionUtil.java#3 $ * @see Collections * @see Arrays */ @@ -51,8 +55,6 @@ public final class CollectionUtil { */ @SuppressWarnings({"UnusedDeclaration", "UnusedAssignment", "unchecked"}) public static void main(String[] pArgs) { - test(); - int howMany = 1000; if (pArgs.length > 0) { @@ -257,7 +259,7 @@ public final class CollectionUtil { * If the sub array is same length as the original * ({@code pStart == 0}), the original array will be returned. * - * @param pArray the origianl array + * @param pArray the original array * @param pStart the start index of the original array * @return a subset of the original array, or the original array itself, * if {@code pStart} is 0. @@ -270,16 +272,33 @@ public final class CollectionUtil { return subArray(pArray, pStart, -1); } + /** + * Creates an array containing a subset of the original array. + * If the sub array is same length as the original + * ({@code pStart == 0}), the original array will be returned. + * + * @param pArray the original array + * @param pStart the start index of the original array + * @return a subset of the original array, or the original array itself, + * if {@code pStart} is 0. + * + * @throws IllegalArgumentException if {@code pArray} is {@code null} + * @throws ArrayIndexOutOfBoundsException if {@code pStart} < 0 + */ + public static T[] subArray(T[] pArray, int pStart) { + return subArray(pArray, pStart, -1); + } + /** * Creates an array containing a subset of the original array. * If the {@code pLength} parameter is negative, it will be ignored. * If there are not {@code pLength} elements in the original array - * after {@code pStart}, the {@code pLength} paramter will be + * after {@code pStart}, the {@code pLength} parameter will be * ignored. * If the sub array is same length as the original, the original array will * be returned. * - * @param pArray the origianl array + * @param pArray the original array * @param pStart the start index of the original array * @param pLength the length of the new array * @return a subset of the original array, or the original array itself, @@ -292,9 +311,7 @@ public final class CollectionUtil { */ @SuppressWarnings({"SuspiciousSystemArraycopy"}) public static Object subArray(Object pArray, int pStart, int pLength) { - if (pArray == null) { - throw new IllegalArgumentException("array == null"); - } + Validate.notNull(pArray, "array"); // Get component type Class type; @@ -321,7 +338,7 @@ public final class CollectionUtil { Object result; if (newLength < originalLength) { - // Create subarray & copy into + // Create sub array & copy into result = Array.newInstance(type, newLength); System.arraycopy(pArray, pStart, result, 0, newLength); } @@ -335,7 +352,33 @@ public final class CollectionUtil { return result; } + /** + * Creates an array containing a subset of the original array. + * If the {@code pLength} parameter is negative, it will be ignored. + * If there are not {@code pLength} elements in the original array + * after {@code pStart}, the {@code pLength} parameter will be + * ignored. + * If the sub array is same length as the original, the original array will + * be returned. + * + * @param pArray the original array + * @param pStart the start index of the original array + * @param pLength the length of the new array + * @return a subset of the original array, or the original array itself, + * if {@code pStart} is 0 and {@code pLength} is either + * negative, or greater or equal to {@code pArray.length}. + * + * @throws IllegalArgumentException if {@code pArray} is {@code null} + * @throws ArrayIndexOutOfBoundsException if {@code pStart} < 0 + */ + @SuppressWarnings("unchecked") + public static T[] subArray(T[] pArray, int pStart, int pLength) { + return (T[]) subArray((Object) pArray, pStart, pLength); + } + public static Iterator iterator(final Enumeration pEnum) { + notNull(pEnum, "enumeration"); + return new Iterator() { public boolean hasNext() { return pEnum.hasMoreElements(); @@ -361,8 +404,8 @@ public final class CollectionUtil { * the given collection. * @throws ClassCastException class of the specified element prevents it * from being added to this collection. - * @throws NullPointerException if the specified element is null and this - * collection does not support null elements. + * @throws NullPointerException if the specified element is {@code null} and this + * collection does not support {@code null} elements. * @throws IllegalArgumentException some aspect of this element prevents * it from being added to this collection. */ @@ -372,7 +415,7 @@ public final class CollectionUtil { } } - // Is there a usecase where Arrays.asList(pArray).iterator() can't ne used? + // Is there a use case where Arrays.asList(pArray).iterator() can't ne used? /** * Creates a thin {@link Iterator} wrapper around an array. * @@ -383,7 +426,7 @@ public final class CollectionUtil { * {@code pLength > pArray.length - pStart} */ public static ListIterator iterator(final E[] pArray) { - return iterator(pArray, 0, pArray.length); + return iterator(pArray, 0, notNull(pArray).length); } /** @@ -408,7 +451,7 @@ public final class CollectionUtil { * @return a new {@code Map} of same type as {@code pSource} * @throws IllegalArgumentException if {@code pSource == null}, * or if a new map can't be instantiated, - * or if source map contains duplaicates. + * or if source map contains duplicates. * * @see #invert(java.util.Map, java.util.Map, DuplicateHandler) */ @@ -424,7 +467,7 @@ public final class CollectionUtil { * @param pResult the map used to contain the result, may be {@code null}, * in that case a new {@code Map} of same type as {@code pSource} is created. * The result map should be empty, otherwise duplicate values will need to be resolved. - * @param pHandler duplicate handler, may be {@code null} if source map don't contain dupliate values + * @param pHandler duplicate handler, may be {@code null} if source map don't contain duplicate values * @return {@code pResult}, or a new {@code Map} if {@code pResult == null} * @throws IllegalArgumentException if {@code pSource == null}, * or if result map is {@code null} and a new map can't be instantiated, @@ -476,20 +519,20 @@ public final class CollectionUtil { return result; } - public static Comparator reverseOrder(Comparator pOriginal) { + public static Comparator reverseOrder(final Comparator pOriginal) { return new ReverseComparator(pOriginal); } private static class ReverseComparator implements Comparator { - private Comparator mComparator; + private final Comparator comparator; - public ReverseComparator(Comparator pComparator) { - mComparator = pComparator; + public ReverseComparator(final Comparator pComparator) { + comparator = notNull(pComparator); } public int compare(T pLeft, T pRight) { - int result = mComparator.compare(pLeft, pRight); + int result = comparator.compare(pLeft, pRight); // We can't simply return -result, as -Integer.MIN_VALUE == Integer.MIN_VALUE. return -(result | (result >>> 1)); @@ -516,73 +559,21 @@ public final class CollectionUtil { return (T) pCollection; } - @SuppressWarnings({"UnusedDeclaration"}) - static void test() { - List list = Collections.singletonList("foo"); - @SuppressWarnings({"unchecked"}) - Set set = new HashSet(list); - - List strs0 = CollectionUtil.generify(list, String.class); - List objs0 = CollectionUtil.generify(list, String.class); -// List strs01 = CollectionUtil.generify(list, Object.class); // Not okay - try { - List strs1 = CollectionUtil.generify(set, String.class); // Not ok, runtime CCE unless set is null - } - catch (RuntimeException e) { - e.printStackTrace(); - } - - try { - ArrayList strs01 = CollectionUtil.generify(list, String.class); // Not ok, runtime CCE unless list is null - } - catch (RuntimeException e) { - e.printStackTrace(); - } - - Set setstr1 = CollectionUtil.generify(set, String.class); - Set setobj1 = CollectionUtil.generify(set, String.class); - try { - Set setobj44 = CollectionUtil.generify(list, String.class); // Not ok, runtime CCE unless list is null - } - catch (RuntimeException e) { - e.printStackTrace(); - } - - List strs2 = CollectionUtil., String>generify2(list); - List objs2 = CollectionUtil., String>generify2(list); -// List morestrs = CollectionUtil., String>generify2(list); // Not ok - try { - List strs3 = CollectionUtil., String>generify2(set); // Not ok, runtime CCE unless set is null - } - catch (RuntimeException e) { - e.printStackTrace(); - } - } - private static class ArrayIterator implements ListIterator { - private int mIndex; - private final int mStart; - private final int mLength; - private final E[] mArray; + private int next; + private final int start; + private final int length; + private final E[] array; - public ArrayIterator(E[] pArray, int pStart, int pLength) { - if (pArray == null) { - throw new IllegalArgumentException("array == null"); - } - if (pStart < 0) { - throw new IllegalArgumentException("start < 0"); - } - if (pLength > pArray.length - pStart) { - throw new IllegalArgumentException("length > array.length - start"); - } - mArray = pArray; - mStart = pStart; - mLength = pLength; - mIndex = mStart; + public ArrayIterator(final E[] pArray, final int pStart, final int pLength) { + array = notNull(pArray, "array"); + start = isTrue(pStart >= 0, pStart, "start < 0: %d"); + length = isTrue(pLength <= pArray.length - pStart, pLength, "length > array.length - start: %d"); + next = start; } public boolean hasNext() { - return mIndex < mLength + mStart; + return next < length + start; } public E next() { @@ -591,7 +582,7 @@ public final class CollectionUtil { } try { - return mArray[mIndex++]; + return array[next++]; } catch (ArrayIndexOutOfBoundsException e) { NoSuchElementException nse = new NoSuchElementException(e.getMessage()); @@ -609,11 +600,11 @@ public final class CollectionUtil { } public boolean hasPrevious() { - return mIndex > mStart; + return next > start; } public int nextIndex() { - return mIndex + 1; + return next - start; } public E previous() { @@ -622,7 +613,7 @@ public final class CollectionUtil { } try { - return mArray[mIndex--]; + return array[--next]; } catch (ArrayIndexOutOfBoundsException e) { NoSuchElementException nse = new NoSuchElementException(e.getMessage()); @@ -632,11 +623,11 @@ public final class CollectionUtil { } public int previousIndex() { - return mIndex - 1; + return nextIndex() - 1; } public void set(E pElement) { - mArray[mIndex] = pElement; + array[next - 1] = pElement; } } } \ No newline at end of file diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/FilterIterator.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/FilterIterator.java index db9d4131..0e5e8586 100644 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/FilterIterator.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/FilterIterator.java @@ -49,11 +49,11 @@ import java.util.NoSuchElementException; */ public class FilterIterator implements Iterator { - protected final Filter mFilter; - protected final Iterator mIterator; + protected final Filter filter; + protected final Iterator iterator; - private E mNext = null; - private E mCurrent = null; + private E next = null; + private E current = null; /** * Creates a {@code FilterIterator} that wraps the {@code Iterator}. Each @@ -72,8 +72,8 @@ public class FilterIterator implements Iterator { throw new IllegalArgumentException("filter == null"); } - mIterator = pIterator; - mFilter = pFilter; + iterator = pIterator; + filter = pFilter; } /** @@ -85,16 +85,16 @@ public class FilterIterator implements Iterator { * @see FilterIterator.Filter#accept */ public boolean hasNext() { - while (mNext == null && mIterator.hasNext()) { - E element = mIterator.next(); + while (next == null && iterator.hasNext()) { + E element = iterator.next(); - if (mFilter.accept(element)) { - mNext = element; + if (filter.accept(element)) { + next = element; break; } } - return mNext != null; + return next != null; } /** @@ -105,11 +105,11 @@ public class FilterIterator implements Iterator { */ public E next() { if (hasNext()) { - mCurrent = mNext; + current = next; // Make sure we advance next time - mNext = null; - return mCurrent; + next = null; + return current; } else { throw new NoSuchElementException("Iteration has no more elements."); @@ -124,8 +124,8 @@ public class FilterIterator implements Iterator { * progress in any way other than by calling this method. */ public void remove() { - if (mCurrent != null) { - mIterator.remove(); + if (current != null) { + iterator.remove(); } else { throw new IllegalStateException("Iteration has no current element."); diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java index df606ff4..4258d723 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java @@ -107,7 +107,7 @@ public class IgnoreCaseMap extends AbstractDecoratedMap implements */ public V put(String pKey, V pValue) { String key = (String) toUpper(pKey); - return unwrap(mEntries.put(key, new BasicEntry(key, pValue))); + return unwrap(entries.put(key, new BasicEntry(key, pValue))); } private V unwrap(Entry pEntry) { @@ -124,7 +124,7 @@ public class IgnoreCaseMap extends AbstractDecoratedMap implements * the key is not mapped to any value in this map. */ public V get(Object pKey) { - return unwrap(mEntries.get(toUpper(pKey))); + return unwrap(entries.get(toUpper(pKey))); } /** @@ -137,7 +137,7 @@ public class IgnoreCaseMap extends AbstractDecoratedMap implements * or null if the key did not have a mapping. */ public V remove(Object pKey) { - return unwrap(mEntries.remove(toUpper(pKey))); + return unwrap(entries.remove(toUpper(pKey))); } /** @@ -149,7 +149,7 @@ public class IgnoreCaseMap extends AbstractDecoratedMap implements * map, as determined by the equals method; false otherwise. */ public boolean containsKey(Object pKey) { - return mEntries.containsKey(toUpper(pKey)); + return entries.containsKey(toUpper(pKey)); } /** @@ -163,14 +163,14 @@ public class IgnoreCaseMap extends AbstractDecoratedMap implements } protected Iterator> newEntryIterator() { - return (Iterator) mEntries.entrySet().iterator(); + return (Iterator) entries.entrySet().iterator(); } protected Iterator newKeyIterator() { - return mEntries.keySet().iterator(); + return entries.keySet().iterator(); } protected Iterator newValueIterator() { - return (Iterator) mEntries.values().iterator(); + return (Iterator) entries.values().iterator(); } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java index 39779eb6..cbaa015f 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUHashMap.java @@ -51,8 +51,8 @@ import java.util.Iterator; */ public class LRUHashMap extends LinkedHashMap implements ExpiringMap { - private int mMaxSize = 1000; - private float mTrimFactor = 0.01f; + private int maxSize = 1000; + private float trimFactor = 0.01f; /** * Creates an LRUHashMap with default max size (1000 entries). @@ -113,7 +113,7 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap * @return the size limit */ public int getMaxSize() { - return mMaxSize; + return maxSize; } /** @@ -131,9 +131,9 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap throw new IllegalArgumentException("max size must be positive"); } - mMaxSize = pMaxSize; + maxSize = pMaxSize; - while(size() > mMaxSize) { + while(size() > maxSize) { removeLRU(); } } @@ -148,7 +148,7 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap * @return the current trim factor */ public float getTrimFactor() { - return mTrimFactor; + return trimFactor; } /** @@ -168,7 +168,7 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap throw new IllegalArgumentException("trim factor must be between 0 and 1"); } - mTrimFactor = pTrimFactor; + trimFactor = pTrimFactor; } /** @@ -178,7 +178,7 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap protected boolean removeEldestEntry(Map.Entry pEldest) { // NOTE: As removeLRU() may remove more than one entry, this is better // than simply removing the eldest entry. - if (size() >= mMaxSize) { + if (size() >= maxSize) { removeLRU(); } return false; @@ -204,7 +204,7 @@ public class LRUHashMap extends LinkedHashMap implements ExpiringMap * @see #getTrimFactor() */ public void removeLRU() { - int removeCount = (int) Math.max((size() * mTrimFactor), 1); + int removeCount = (int) Math.max((size() * trimFactor), 1); Iterator> entries = entrySet().iterator(); while ((removeCount--) > 0 && entries.hasNext()) { entries.next(); diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java index bad6b61e..d5057046 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LRUMap.java @@ -49,8 +49,8 @@ import java.util.Map; */ public class LRUMap extends LinkedMap implements ExpiringMap { - private int mMaxSize = 1000; - private float mTrimFactor = 0.01f; + private int maxSize = 1000; + private float trimFactor = 0.01f; /** * Creates an LRUMap with default max size (1000 entries). @@ -124,7 +124,7 @@ public class LRUMap extends LinkedMap implements ExpiringMap { * @return the size limit */ public int getMaxSize() { - return mMaxSize; + return maxSize; } /** @@ -142,9 +142,9 @@ public class LRUMap extends LinkedMap implements ExpiringMap { throw new IllegalArgumentException("max size must be positive"); } - mMaxSize = pMaxSize; + maxSize = pMaxSize; - while(size() > mMaxSize) { + while(size() > maxSize) { removeLRU(); } } @@ -159,7 +159,7 @@ public class LRUMap extends LinkedMap implements ExpiringMap { * @return the current trim factor */ public float getTrimFactor() { - return mTrimFactor; + return trimFactor; } /** @@ -179,7 +179,7 @@ public class LRUMap extends LinkedMap implements ExpiringMap { throw new IllegalArgumentException("trim factor must be between 0 and 1"); } - mTrimFactor = pTrimFactor; + trimFactor = pTrimFactor; } /** @@ -189,7 +189,7 @@ public class LRUMap extends LinkedMap implements ExpiringMap { protected boolean removeEldestEntry(Entry pEldest) { // NOTE: As removeLRU() may remove more than one entry, this is better // than simply removing the eldest entry. - if (size() >= mMaxSize) { + if (size() >= maxSize) { removeLRU(); } return false; @@ -221,9 +221,9 @@ public class LRUMap extends LinkedMap implements ExpiringMap { * @see #getTrimFactor() */ public void removeLRU() { - int removeCount = (int) Math.max((size() * mTrimFactor), 1); + int removeCount = (int) Math.max((size() * trimFactor), 1); while ((removeCount--) > 0) { - removeEntry(mHead.mNext); + removeEntry(head.mNext); } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java index fef6b51e..b493ff9d 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedMap.java @@ -72,8 +72,8 @@ import java.io.Serializable; */ public class LinkedMap extends AbstractDecoratedMap implements Serializable { - transient LinkedEntry mHead; - protected final boolean mAccessOrder; + transient LinkedEntry head; + protected final boolean accessOrder; /** * Creates a {@code LinkedMap} backed by a {@code HashMap}, with default @@ -118,7 +118,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria */ public LinkedMap(Map pContents, boolean pAccessOrder) { super(pContents); - mAccessOrder = pAccessOrder; + accessOrder = pAccessOrder; } /** @@ -148,11 +148,11 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria */ public LinkedMap(Map> pBacking, Map pContents, boolean pAccessOrder) { super(pBacking, pContents); - mAccessOrder = pAccessOrder; + accessOrder = pAccessOrder; } protected void init() { - mHead = new LinkedEntry(null, null, null) { + head = new LinkedEntry(null, null, null) { void addBefore(LinkedEntry pExisting) { throw new Error(); } @@ -181,19 +181,19 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria return "head"; } }; - mHead.mPrevious = mHead.mNext = mHead; + head.mPrevious = head.mNext = head; } public boolean containsValue(Object pValue) { // Overridden to take advantage of faster iterator if (pValue == null) { - for (LinkedEntry e = mHead.mNext; e != mHead; e = e.mNext) { + for (LinkedEntry e = head.mNext; e != head; e = e.mNext) { if (e.mValue == null) { return true; } } } else { - for (LinkedEntry e = mHead.mNext; e != mHead; e = e.mNext) { + for (LinkedEntry e = head.mNext; e != head; e = e.mNext) { if (pValue.equals(e.mValue)) { return true; } @@ -215,7 +215,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria } private abstract class LinkedMapIterator implements Iterator { - LinkedEntry mNextEntry = mHead.mNext; + LinkedEntry mNextEntry = head.mNext; LinkedEntry mLastReturned = null; /** @@ -223,10 +223,10 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria * List should have. If this expectation is violated, the iterator * has detected concurrent modification. */ - int mExpectedModCount = mModCount; + int mExpectedModCount = modCount; public boolean hasNext() { - return mNextEntry != mHead; + return mNextEntry != head; } public void remove() { @@ -234,22 +234,22 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria throw new IllegalStateException(); } - if (mModCount != mExpectedModCount) { + if (modCount != mExpectedModCount) { throw new ConcurrentModificationException(); } LinkedMap.this.remove(mLastReturned.mKey); mLastReturned = null; - mExpectedModCount = mModCount; + mExpectedModCount = modCount; } LinkedEntry nextEntry() { - if (mModCount != mExpectedModCount) { + if (modCount != mExpectedModCount) { throw new ConcurrentModificationException(); } - if (mNextEntry == mHead) { + if (mNextEntry == head) { throw new NoSuchElementException(); } @@ -279,7 +279,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria } public V get(Object pKey) { - LinkedEntry entry = (LinkedEntry) mEntries.get(pKey); + LinkedEntry entry = (LinkedEntry) entries.get(pKey); if (entry != null) { entry.recordAccess(this); @@ -290,11 +290,11 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria } public V remove(Object pKey) { - LinkedEntry entry = (LinkedEntry) mEntries.remove(pKey); + LinkedEntry entry = (LinkedEntry) entries.remove(pKey); if (entry != null) { entry.remove(); - mModCount++; + modCount++; return entry.mValue; } @@ -302,22 +302,22 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria } public V put(K pKey, V pValue) { - LinkedEntry entry = (LinkedEntry) mEntries.get(pKey); + LinkedEntry entry = (LinkedEntry) entries.get(pKey); V oldValue; if (entry == null) { oldValue = null; // Remove eldest entry if instructed, else grow capacity if appropriate - LinkedEntry eldest = mHead.mNext; + LinkedEntry eldest = head.mNext; if (removeEldestEntry(eldest)) { removeEntry(eldest); } entry = createEntry(pKey, pValue); - entry.addBefore(mHead); + entry.addBefore(head); - mEntries.put(pKey, entry); + entries.put(pKey, entry); } else { oldValue = entry.mValue; @@ -326,7 +326,7 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria entry.recordAccess(this); } - mModCount++; + modCount++; return oldValue; } @@ -446,10 +446,10 @@ public class LinkedMap extends AbstractDecoratedMap implements Seria */ protected void recordAccess(Map pMap) { LinkedMap linkedMap = (LinkedMap) pMap; - if (linkedMap.mAccessOrder) { - linkedMap.mModCount++; + if (linkedMap.accessOrder) { + linkedMap.modCount++; remove(); - addBefore(linkedMap.mHead); + addBefore(linkedMap.head); } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedSet.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedSet.java index 511acbb7..8a56b367 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedSet.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/LinkedSet.java @@ -47,10 +47,10 @@ public class LinkedSet extends AbstractSet implements Set, Cloneable, S private final static Object DUMMY = new Object(); - private final Map mMap; + private final Map map; public LinkedSet() { - mMap = new LinkedMap(); + map = new LinkedMap(); } public LinkedSet(Collection pCollection) { @@ -69,14 +69,14 @@ public class LinkedSet extends AbstractSet implements Set, Cloneable, S } public boolean add(E pValue) { - return mMap.put(pValue, DUMMY) == null; + return map.put(pValue, DUMMY) == null; } public int size() { - return mMap.size(); + return map.size(); } public Iterator iterator() { - return mMap.keySet().iterator(); + return map.keySet().iterator(); } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/NullMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/NullMap.java index 9a3a1968..eb538915 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/NullMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/NullMap.java @@ -32,8 +32,8 @@ import java.util.*; import java.io.Serializable; /** - * An (immutable) empty {@link Map}, that supports all {@code Map} operations - * without throwing expcetions (in contrast to {@link Collections#EMPTY_MAP} + * An (immutable) empty {@link Map}, that supports all {@code Map} operations + * without throwing exceptions (in contrast to {@link Collections#EMPTY_MAP} * that will throw exceptions on {@code put}/{@code remove}). *

* NOTE: This is not a general purpose {@code Map} implementation, @@ -41,7 +41,7 @@ import java.io.Serializable; * Instances of this class will always be an empty map. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/NullMap.java#2 $ + * @version $Id: com/twelvemonkeys/util/NullMap.java#2 $ */ public final class NullMap implements Map, Serializable { public final int size() { diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java index 433f2961..dc95c5d6 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java @@ -51,15 +51,15 @@ import java.util.NoSuchElementException; */ public class StringTokenIterator extends AbstractTokenIterator { - private final String mString; - private final char[] mDelimiters; - private int mPosition; - private final int mMaxPosition; - private String mNext; - private String mNextDelimiter; - private final boolean mIncludeDelimiters; - private final boolean mIncludeEmpty; - private final boolean mReverse; + private final String string; + private final char[] delimiters; + private int position; + private final int maxPosition; + private String next; + private String nextDelimiter; + private final boolean includeDelimiters; + private final boolean includeEmpty; + private final boolean reverse; public final static int FORWARD = 1; public final static int REVERSE = -1; @@ -68,7 +68,7 @@ public class StringTokenIterator extends AbstractTokenIterator { * Stores the value of the delimiter character with the highest value. * It is used to optimize the detection of delimiter characters. */ - private final char mMaxDelimiter; + private final char maxDelimiter; /** * Creates a StringTokenIterator @@ -141,13 +141,13 @@ public class StringTokenIterator extends AbstractTokenIterator { throw new IllegalArgumentException("string == null"); } - mString = pString; - mMaxPosition = pString.length(); - mDelimiters = pDelimiters; - mIncludeDelimiters = pIncludeDelimiters; - mReverse = (pDirection == REVERSE); - mIncludeEmpty = pIncludeEmpty; - mMaxDelimiter = initMaxDelimiter(pDelimiters); + string = pString; + maxPosition = pString.length(); + delimiters = pDelimiters; + includeDelimiters = pIncludeDelimiters; + reverse = (pDirection == REVERSE); + includeEmpty = pIncludeEmpty; + maxDelimiter = initMaxDelimiter(pDelimiters); reset(); } @@ -184,9 +184,9 @@ public class StringTokenIterator extends AbstractTokenIterator { * */ public void reset() { - mPosition = 0; - mNext = null; - mNextDelimiter = null; + position = 0; + next = null; + nextDelimiter = null; } /** @@ -197,23 +197,23 @@ public class StringTokenIterator extends AbstractTokenIterator { * @return {@code true} if the iterator has more elements. */ public boolean hasNext() { - return (mNext != null || fetchNext() != null); + return (next != null || fetchNext() != null); } private String fetchNext() { // If next is delimiter, return fast - if (mNextDelimiter != null) { - mNext = mNextDelimiter; - mNextDelimiter = null; - return mNext; + if (nextDelimiter != null) { + next = nextDelimiter; + nextDelimiter = null; + return next; } // If no more chars, return null - if (mPosition >= mMaxPosition) { + if (position >= maxPosition) { return null; } - return mReverse ? fetchReverse() : fetchForward(); + return reverse ? fetchReverse() : fetchForward(); } @@ -222,20 +222,20 @@ public class StringTokenIterator extends AbstractTokenIterator { int prevPos = scanForPrev(); // Store next string - mNext = mString.substring(prevPos + 1, mMaxPosition - mPosition); + next = string.substring(prevPos + 1, maxPosition - position); - if (mIncludeDelimiters && prevPos >= 0 && prevPos < mMaxPosition) { - mNextDelimiter = mString.substring(prevPos, prevPos + 1); + if (includeDelimiters && prevPos >= 0 && prevPos < maxPosition) { + nextDelimiter = string.substring(prevPos, prevPos + 1); } - mPosition = mMaxPosition - prevPos; + position = maxPosition - prevPos; // Skip empty - if (mNext.length() == 0 && !mIncludeEmpty) { + if (next.length() == 0 && !includeEmpty) { return fetchNext(); } - return mNext; + return next; } private String fetchForward() { @@ -243,33 +243,33 @@ public class StringTokenIterator extends AbstractTokenIterator { int nextPos = scanForNext(); // Store next string - mNext = mString.substring(mPosition, nextPos); + next = string.substring(position, nextPos); - if (mIncludeDelimiters && nextPos >= 0 && nextPos < mMaxPosition) { - mNextDelimiter = mString.substring(nextPos, nextPos + 1); + if (includeDelimiters && nextPos >= 0 && nextPos < maxPosition) { + nextDelimiter = string.substring(nextPos, nextPos + 1); } - mPosition = ++nextPos; + position = ++nextPos; // Skip empty - if (mNext.length() == 0 && !mIncludeEmpty) { + if (next.length() == 0 && !includeEmpty) { return fetchNext(); } - return mNext; + return next; } private int scanForNext() { - int position = mPosition; + int position = this.position; - while (position < mMaxPosition) { + while (position < maxPosition) { // Find next match, using all delimiters - char c = mString.charAt(position); + char c = string.charAt(position); - if (c <= mMaxDelimiter) { + if (c <= maxDelimiter) { // Find first delimiter match - for (char delimiter : mDelimiters) { + for (char delimiter : delimiters) { if (c == delimiter) { return position;// Return if match } @@ -285,16 +285,16 @@ public class StringTokenIterator extends AbstractTokenIterator { } private int scanForPrev() { - int position = (mMaxPosition - 1) - mPosition; + int position = (maxPosition - 1) - this.position; while (position >= 0) { // Find next match, using all delimiters - char c = mString.charAt(position); + char c = string.charAt(position); - if (c <= mMaxDelimiter) { + if (c <= maxDelimiter) { // Find first delimiter match - for (char delimiter : mDelimiters) { + for (char delimiter : delimiters) { if (c == delimiter) { return position;// Return if match } @@ -320,8 +320,8 @@ public class StringTokenIterator extends AbstractTokenIterator { throw new NoSuchElementException(); } - String next = mNext; - mNext = fetchNext(); + String next = this.next; + this.next = fetchNext(); return next; } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/Time.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/Time.java index 49368127..c7be02a7 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/Time.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/Time.java @@ -37,7 +37,7 @@ package com.twelvemonkeys.util; */ public class Time { - private int mTime = -1; + private int time = -1; public final static int SECONDS_IN_MINUTE = 60; /** @@ -61,14 +61,14 @@ public class Time { if (pTime < 0) { throw new IllegalArgumentException("Time argument must be 0 or positive!"); } - mTime = pTime; + time = pTime; } /** * Gets the full time in seconds. */ public int getTime() { - return mTime; + return time; } /** @@ -78,7 +78,7 @@ public class Time { * @see java.util.Date#setTime(long) */ public long getTimeInMillis() { - return (long) mTime * 1000L; + return (long) time * 1000L; } /** @@ -88,7 +88,7 @@ public class Time { * @param pSeconds an integer that should be between 0 and 59. */ public void setSeconds(int pSeconds) { - mTime = getMinutes() * SECONDS_IN_MINUTE + pSeconds; + time = getMinutes() * SECONDS_IN_MINUTE + pSeconds; } /** @@ -97,7 +97,7 @@ public class Time { * @return an integer between 0 and 59 */ public int getSeconds() { - return mTime % SECONDS_IN_MINUTE; + return time % SECONDS_IN_MINUTE; } /** @@ -106,7 +106,7 @@ public class Time { * @param pMinutes an integer */ public void setMinutes(int pMinutes) { - mTime = pMinutes * SECONDS_IN_MINUTE + getSeconds(); + time = pMinutes * SECONDS_IN_MINUTE + getSeconds(); } /** @@ -115,7 +115,7 @@ public class Time { * @return an integer */ public int getMinutes() { - return mTime / SECONDS_IN_MINUTE; + return time / SECONDS_IN_MINUTE; } /** diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeFormat.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeFormat.java index d2e9405b..68fb1d33 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeFormat.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeFormat.java @@ -29,7 +29,10 @@ package com.twelvemonkeys.util; +import com.twelvemonkeys.lang.StringUtil; + import java.text.FieldPosition; +import java.text.Format; import java.text.ParsePosition; import java.util.StringTokenizer; import java.util.Vector; @@ -72,8 +75,7 @@ import java.util.Vector; * * @author Harald Kuhr */ - -public class TimeFormat extends java.text.Format { +public class TimeFormat extends Format { final static String MINUTE = "m"; final static String SECOND = "s"; final static String TIME = "S"; @@ -84,7 +86,7 @@ public class TimeFormat extends java.text.Format { */ private final static TimeFormat DEFAULT_FORMAT = new TimeFormat("m:ss"); - protected String mFormatString = null; + protected String formatString = null; /** * Main method for testing ONLY @@ -114,14 +116,14 @@ public class TimeFormat extends java.text.Format { if (argv.length >= 1) { System.out.println("Parsing: \"" + argv[0] + "\" with format \"" - + in.mFormatString + "\""); + + in.formatString + "\""); time = in.parse(argv[0]); } else time = new Time(); System.out.println("Time is \"" + out.format(time) + - "\" according to format \"" + out.mFormatString + "\""); + "\" according to format \"" + out.formatString + "\""); } @@ -129,14 +131,14 @@ public class TimeFormat extends java.text.Format { * The formatter array. */ - protected TimeFormatter[] mFormatter; + protected TimeFormatter[] formatter; /** * Creates a new TimeFormat with the given formatString, */ public TimeFormat(String pStr) { - mFormatString = pStr; + formatString = pStr; Vector formatter = new Vector(); StringTokenizer tok = new StringTokenizer(pStr, "\\msS", true); @@ -195,10 +197,10 @@ public class TimeFormat extends java.text.Format { /* for (int i = 0; i < formatter.size(); i++) { System.out.println("Formatter " + formatter.get(i).getClass() - + ": length=" + ((TimeFormatter) formatter.get(i)).mDigits); + + ": length=" + ((TimeFormatter) formatter.get(i)).digits); } */ - mFormatter = (TimeFormatter[]) + this.formatter = (TimeFormatter[]) formatter.toArray(new TimeFormatter[formatter.size()]); } @@ -228,7 +230,7 @@ public class TimeFormat extends java.text.Format { /** Gets the format string. */ public String getFormatString() { - return mFormatString; + return formatString; } /** DUMMY IMPLEMENTATION!! */ @@ -247,8 +249,8 @@ public class TimeFormat extends java.text.Format { public String format(Time pTime) { StringBuilder buf = new StringBuilder(); - for (int i = 0; i < mFormatter.length; i++) { - buf.append(mFormatter[i].format(pTime)); + for (int i = 0; i < formatter.length; i++) { + buf.append(formatter[i].format(pTime)); } return buf.toString(); } @@ -279,45 +281,45 @@ public class TimeFormat extends java.text.Format { boolean onlyUseSeconds = false; - for (int i = 0; (i < mFormatter.length) + for (int i = 0; (i < formatter.length) && (pos + skip < pStr.length()) ; i++) { // Go to next offset pos += skip; - if (mFormatter[i] instanceof MinutesFormatter) { + if (formatter[i] instanceof MinutesFormatter) { // Parse MINUTES - if ((i + 1) < mFormatter.length - && mFormatter[i + 1] instanceof TextFormatter) { + if ((i + 1) < formatter.length + && formatter[i + 1] instanceof TextFormatter) { // Skip until next format element - skip = pStr.indexOf(((TextFormatter) mFormatter[i + 1]).mText, pos); + skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); // Error in format, try parsing to end if (skip < 0) skip = pStr.length(); } - else if ((i + 1) >= mFormatter.length) { + else if ((i + 1) >= formatter.length) { // Skip until end of string skip = pStr.length(); } else { // Hope this is correct... - skip = mFormatter[i].mDigits; + skip = formatter[i].digits; } // May be first char if (skip > pos) min = Integer.parseInt(pStr.substring(pos, skip)); } - else if (mFormatter[i] instanceof SecondsFormatter) { + else if (formatter[i] instanceof SecondsFormatter) { // Parse SECONDS - if (mFormatter[i].mDigits == -1) { + if (formatter[i].digits == -1) { // Only seconds (or full TIME) - if ((i + 1) < mFormatter.length - && mFormatter[i + 1] instanceof TextFormatter) { + if ((i + 1) < formatter.length + && formatter[i + 1] instanceof TextFormatter) { // Skip until next format element - skip = pStr.indexOf(((TextFormatter) mFormatter[i + 1]).mText, pos); + skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); } - else if ((i + 1) >= mFormatter.length) { + else if ((i + 1) >= formatter.length) { // Skip until end of string skip = pStr.length(); } @@ -336,25 +338,25 @@ public class TimeFormat extends java.text.Format { } else { // Normal SECONDS - if ((i + 1) < mFormatter.length - && mFormatter[i + 1] instanceof TextFormatter) { + if ((i + 1) < formatter.length + && formatter[i + 1] instanceof TextFormatter) { // Skip until next format element - skip = pStr.indexOf(((TextFormatter) mFormatter[i + 1]).mText, pos); + skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); } - else if ((i + 1) >= mFormatter.length) { + else if ((i + 1) >= formatter.length) { // Skip until end of string skip = pStr.length(); } else { - skip = mFormatter[i].mDigits; + skip = formatter[i].digits; } // Get seconds sec = Integer.parseInt(pStr.substring(pos, skip)); } } - else if (mFormatter[i] instanceof TextFormatter) { - skip = mFormatter[i].mDigits; + else if (formatter[i] instanceof TextFormatter) { + skip = formatter[i].digits; } } @@ -374,7 +376,7 @@ public class TimeFormat extends java.text.Format { * The base class of TimeFormatters */ abstract class TimeFormatter { - int mDigits = 0; + int digits = 0; abstract String format(Time t); } @@ -385,23 +387,23 @@ abstract class TimeFormatter { class SecondsFormatter extends TimeFormatter { SecondsFormatter(int pDigits) { - mDigits = pDigits; + digits = pDigits; } String format(Time t) { // Negative number of digits, means all seconds, no padding - if (mDigits < 0) { + if (digits < 0) { return Integer.toString(t.getTime()); } - // If seconds is more than mDigits long, simply return it - if (t.getSeconds() >= Math.pow(10, mDigits)) { + // If seconds is more than digits long, simply return it + if (t.getSeconds() >= Math.pow(10, digits)) { return Integer.toString(t.getSeconds()); } // Else return it with leading 0's - //return StringUtil.formatNumber(t.getSeconds(), mDigits); - return com.twelvemonkeys.lang.StringUtil.pad("" + t.getSeconds(), mDigits, "0", true); + //return StringUtil.formatNumber(t.getSeconds(), digits); + return StringUtil.pad("" + t.getSeconds(), digits, "0", true); } } @@ -411,18 +413,18 @@ class SecondsFormatter extends TimeFormatter { class MinutesFormatter extends TimeFormatter { MinutesFormatter(int pDigits) { - mDigits = pDigits; + digits = pDigits; } String format(Time t) { - // If minutes is more than mDigits long, simply return it - if (t.getMinutes() >= Math.pow(10, mDigits)) { + // If minutes is more than digits long, simply return it + if (t.getMinutes() >= Math.pow(10, digits)) { return Integer.toString(t.getMinutes()); } // Else return it with leading 0's - //return StringUtil.formatNumber(t.getMinutes(), mDigits); - return com.twelvemonkeys.lang.StringUtil.pad("" + t.getMinutes(), mDigits, "0", true); + //return StringUtil.formatNumber(t.getMinutes(), digits); + return StringUtil.pad("" + t.getMinutes(), digits, "0", true); } } @@ -430,20 +432,20 @@ class MinutesFormatter extends TimeFormatter { * Formats text constant part of the Time */ class TextFormatter extends TimeFormatter { - String mText = null; + String text = null; TextFormatter(String pText) { - mText = pText; + text = pText; // Just to be able to skip over if (pText != null) { - mDigits = pText.length(); + digits = pText.length(); } } String format(Time t) { // Simply return the text - return mText; + return text; } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java index dff22ded..800ce922 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java @@ -62,10 +62,10 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi /** * Expiry time */ - protected long mExpiryTime = 60000L; // 1 minute + protected long expiryTime = 60000L; // 1 minute ////////////////////// - private volatile long mNextExpiryTime; + private volatile long nextExpiryTime; ////////////////////// /** @@ -107,7 +107,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi */ public TimeoutMap(long pExpiryTime) { this(); - mExpiryTime = pExpiryTime; + expiryTime = pExpiryTime; } /** @@ -125,7 +125,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi */ public TimeoutMap(Map> pBacking, Map pContents, long pExpiryTime) { super(pBacking, pContents); - mExpiryTime = pExpiryTime; + expiryTime = pExpiryTime; } /** @@ -134,7 +134,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * @return the expiry time */ public long getExpiryTime() { - return mExpiryTime; + return expiryTime; } /** @@ -144,13 +144,13 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * @param pExpiryTime the expiry time (time to live) for elements in this map */ public void setExpiryTime(long pExpiryTime) { - long oldEexpiryTime = mExpiryTime; + long oldEexpiryTime = expiryTime; - mExpiryTime = pExpiryTime; + expiryTime = pExpiryTime; - if (mExpiryTime < oldEexpiryTime) { + if (expiryTime < oldEexpiryTime) { // Expire now - mNextExpiryTime = 0; + nextExpiryTime = 0; removeExpiredEntries(); } } @@ -164,7 +164,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi */ public int size() { removeExpiredEntries(); - return mEntries.size(); + return entries.size(); } /** @@ -186,7 +186,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi */ public boolean containsKey(Object pKey) { removeExpiredEntries(); - return mEntries.containsKey(pKey); + return entries.containsKey(pKey); } /** @@ -203,14 +203,14 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * @see #containsKey(java.lang.Object) */ public V get(Object pKey) { - TimedEntry entry = (TimedEntry) mEntries.get(pKey); + TimedEntry entry = (TimedEntry) entries.get(pKey); if (entry == null) { return null; } else if (entry.isExpired()) { //noinspection SuspiciousMethodCalls - mEntries.remove(pKey); + entries.remove(pKey); processRemoved(entry); return null; } @@ -231,7 +231,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * {@code null} values. */ public V put(K pKey, V pValue) { - TimedEntry entry = (TimedEntry) mEntries.get(pKey); + TimedEntry entry = (TimedEntry) entries.get(pKey); V oldValue; if (entry == null) { @@ -239,7 +239,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi entry = createEntry(pKey, pValue); - mEntries.put(pKey, entry); + entries.put(pKey, entry); } else { oldValue = entry.mValue; @@ -250,7 +250,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi // Need to remove expired objects every now and then // We do it in the put method, to avoid resource leaks over time. removeExpiredEntries(); - mModCount++; + modCount++; return oldValue; } @@ -267,7 +267,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * {@code null} values. */ public V remove(Object pKey) { - TimedEntry entry = (TimedEntry) mEntries.remove(pKey); + TimedEntry entry = (TimedEntry) entries.remove(pKey); return (entry != null) ? entry.getValue() : null; } @@ -275,7 +275,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * Removes all mappings from this map. */ public void clear() { - mEntries.clear(); // Finally something straightforward.. :-) + entries.clear(); // Finally something straightforward.. :-) init(); } @@ -290,7 +290,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi protected void removeExpiredEntries() { // Remove any expired elements long now = System.currentTimeMillis(); - if (now > mNextExpiryTime) { + if (now > nextExpiryTime) { removeExpiredEntriesSynced(now); } } @@ -303,10 +303,10 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * @param pTime now */ private synchronized void removeExpiredEntriesSynced(long pTime) { - if (pTime > mNextExpiryTime) { + if (pTime > nextExpiryTime) { //// long next = Long.MAX_VALUE; - mNextExpiryTime = next; // Avoid multiple runs... + nextExpiryTime = next; // Avoid multiple runs... for (Iterator> iterator = new EntryIterator(); iterator.hasNext();) { TimedEntry entry = (TimedEntry) iterator.next(); //// @@ -317,7 +317,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi //// } //// - mNextExpiryTime = next; + nextExpiryTime = next; } } @@ -356,7 +356,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * Note: Iterating through this iterator will remove any expired values. */ private abstract class TimeoutMapIterator implements Iterator { - Iterator>> mIterator = mEntries.entrySet().iterator(); + Iterator>> mIterator = entries.entrySet().iterator(); BasicEntry mNext; long mNow = System.currentTimeMillis(); @@ -443,7 +443,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi } final long expires() { - return mTimestamp + mExpiryTime; + return mTimestamp + expiryTime; } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java index c861d8b9..d9737e92 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java @@ -135,46 +135,46 @@ public class WeakWeakMap extends WeakHashMap { public Iterator> iterator() { return new Iterator>() { @SuppressWarnings({"unchecked"}) - final Iterator>> mIterator = (Iterator) WeakWeakMap.super.entrySet().iterator(); + final Iterator>> iterator = (Iterator) WeakWeakMap.super.entrySet().iterator(); public boolean hasNext() { - return mIterator.hasNext(); + return iterator.hasNext(); } public Map.Entry next() { return new Map.Entry() { - final Map.Entry> mEntry = mIterator.next(); + final Map.Entry> entry = iterator.next(); public K getKey() { - return mEntry.getKey(); + return entry.getKey(); } public V getValue() { - WeakReference ref = mEntry.getValue(); + WeakReference ref = entry.getValue(); return ref.get(); } public V setValue(V pValue) { - WeakReference ref = mEntry.setValue(new WeakReference(pValue)); + WeakReference ref = entry.setValue(new WeakReference(pValue)); return ref != null ? ref.get() : null; } public boolean equals(Object obj) { - return mEntry.equals(obj); + return entry.equals(obj); } public int hashCode() { - return mEntry.hashCode(); + return entry.hashCode(); } public String toString() { - return mEntry.toString(); + return entry.toString(); } }; } public void remove() { - mIterator.remove(); + iterator.remove(); } }; } @@ -191,19 +191,19 @@ public class WeakWeakMap extends WeakHashMap { public Iterator iterator() { return new Iterator() { @SuppressWarnings({"unchecked"}) - Iterator> mIterator = (Iterator>) WeakWeakMap.super.values().iterator(); + Iterator> iterator = (Iterator>) WeakWeakMap.super.values().iterator(); public boolean hasNext() { - return mIterator.hasNext(); + return iterator.hasNext(); } public V next() { - WeakReference ref = mIterator.next(); + WeakReference ref = iterator.next(); return ref.get(); } public void remove() { - mIterator.remove(); + iterator.remove(); } }; } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java index 0e139eb5..06bf09ab 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java @@ -37,60 +37,31 @@ package com.twelvemonkeys.util.convert; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java#1 $ */ public class ConversionException extends IllegalArgumentException { - protected Throwable mCause = this; - /** * Creates a {@code ConversionException} with the given error message. * * @param pMessage the error message */ - public ConversionException(String pMessage) { + public ConversionException(final String pMessage) { super(pMessage); } /** * Creates a {@code ConversionException} with the given cause. * - * @param pCause The Throwable that caused this exception + * @param pCause The {@link Throwable} that caused this exception */ - public ConversionException(Throwable pCause) { - super(pCause == null ? null : pCause.getMessage()); - initCause(pCause); + public ConversionException(final Throwable pCause) { + super(pCause != null ? pCause.getMessage() : null, pCause); } /** - * Returns the cause of this {@code Throwable} or {@code null} if the - * cause is nonexistent or unknown. + * Creates a {@code ConversionException} with the given message and cause. * - * @return the cause of this {@code Throwable} or {@code null} if the - * cause is nonexistent or unknown (the cause is the throwable that caused - * this throwable to get thrown). + * @param pMessage the error message + * @param pCause The {@link Throwable} that caused this exception */ - public Throwable getCause() { - if (mCause == this) { - return null; - } - return mCause; - } - - /** - * Initializes this ConversionException with the given cause. - * - * @param pCause The Throwable that caused this exception - * - * @throws IllegalStateException if cause is allready set - * @throws IllegalArgumentException if {@code pCause == this} - */ - public Throwable initCause(Throwable pCause) { - if (mCause != this) { - throw new IllegalStateException("Can't overwrite cause"); - } - if (pCause == this) { - throw new IllegalArgumentException("Can't be caused by self"); - } - - mCause = pCause; - - return this; + public ConversionException(final String pMessage, final Throwable pCause) { + super(pMessage, pCause); } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java index b95d82d3..aa4015ef 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/Converter.java @@ -56,13 +56,14 @@ import java.util.Map; // Maybe have BeanUtil act as a "proxy", and hide this class alltogheter? // TODO: ServiceRegistry for registering 3rd party converters // TODO: URI scheme, for implicit typing? Is that a good idea? +// TODO: Array converters? public abstract class Converter implements PropertyConverter { /** Our singleton instance */ protected static Converter sInstance = new ConverterImpl(); // Thread safe & EASY /** The conveters Map */ - protected Map mConverters = new Hashtable(); + protected Map converters = new Hashtable(); // Register our predefined converters static { @@ -115,7 +116,7 @@ public abstract class Converter implements PropertyConverter { * @see #unregisterConverter(Class) */ public static void registerConverter(Class pType, PropertyConverter pConverter) { - getInstance().mConverters.put(pType, pConverter); + getInstance().converters.put(pType, pConverter); } /** @@ -128,7 +129,7 @@ public abstract class Converter implements PropertyConverter { * @see #registerConverter(Class,PropertyConverter) */ public static void unregisterConverter(Class pType) { - getInstance().mConverters.remove(pType); + getInstance().converters.remove(pType); } /** diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java index dba48dc5..321c6ee1 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java @@ -59,7 +59,7 @@ class ConverterImpl extends Converter { // Loop until we find a suitable converter do { // Have a match, return converter - if ((converter = getInstance().mConverters.get(cl)) != null) { + if ((converter = getInstance().converters.get(cl)) != null) { return (PropertyConverter) converter; } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java index ca94503e..9183fbb7 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java @@ -38,7 +38,7 @@ import java.lang.reflect.InvocationTargetException; * Converts strings to dates and back. *

* This class has a static cache of {@code DateFormats}, to avoid - * creation and parsing of dateformats every time one is used. + * creation and parsing of date formats every time one is used. * * @author Harald Kuhr * @author last modified by $Author: haku $ @@ -68,8 +68,7 @@ public class DateConverter extends NumberConverter { * * @throws ConversionException */ - public Object toObject(String pString, Class pType, String pFormat) - throws ConversionException { + public Object toObject(String pString, Class pType, String pFormat) throws ConversionException { if (StringUtil.isEmpty(pString)) return null; @@ -123,13 +122,13 @@ public class DateConverter extends NumberConverter { * @see Date * @see java.text.DateFormat */ - public String toString(Object pObject, String pFormat) - throws ConversionException { + public String toString(Object pObject, String pFormat) throws ConversionException { if (pObject == null) return null; - if (!(pObject instanceof Date)) + if (!(pObject instanceof Date)) { throw new TypeMismathException(pObject.getClass()); + } try { // Convert to string, default way @@ -139,6 +138,7 @@ public class DateConverter extends NumberConverter { // Convert to string, using format DateFormat format = getDateFormat(pFormat); + return format.format(pObject); } catch (RuntimeException rte) { @@ -147,6 +147,6 @@ public class DateConverter extends NumberConverter { } private DateFormat getDateFormat(String pFormat) { - return (DateFormat) getFormat(SimpleDateFormat.class, pFormat); + return (DateFormat) getFormat(SimpleDateFormat.class, pFormat, Locale.US); } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java index c80f6cdb..81e4d589 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java @@ -28,9 +28,11 @@ package com.twelvemonkeys.util.convert; -import com.twelvemonkeys.lang.*; +import com.twelvemonkeys.lang.BeanUtil; +import com.twelvemonkeys.lang.StringUtil; -import java.lang.reflect.*; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; /** * Converts strings to objects and back. @@ -67,9 +69,7 @@ public final class DefaultConverter implements PropertyConverter { * be converted into the given type, using a string constructor or static * {@code valueof} method. */ - public Object toObject(String pString, final Class pType, String pFormat) - throws ConversionException { - + public Object toObject(String pString, final Class pType, String pFormat) throws ConversionException { if (pString == null) { return null; } @@ -78,6 +78,14 @@ public final class DefaultConverter implements PropertyConverter { throw new MissingTypeException(); } + if (pType.isArray()) { + return toArray(pString, pType, pFormat); + } + + // TODO: Separate CollectionConverter? + // should however, be simple to wrap array using Arrays.asList + // But what about generic type?! It's erased... + // Primitive -> wrapper Class type; if (pType == Boolean.TYPE) { @@ -113,6 +121,31 @@ public final class DefaultConverter implements PropertyConverter { } } + private Object toArray(String pString, Class pType, String pFormat) { + String[] strings = StringUtil.toStringArray(pString, pFormat != null ? pFormat : StringUtil.DELIMITER_STRING); + Class type = pType.getComponentType(); + if (type == String.class) { + return strings; + } + + Object array = Array.newInstance(type, strings.length); + try { + for (int i = 0; i < strings.length; i++) { + Array.set(array, i, Converter.getInstance().toObject(strings[i], type)); + } + } + catch (ConversionException e) { + if (pFormat != null) { + throw new ConversionException(String.format("%s for string \"%s\" with format \"%s\"", e.getMessage(), pString, pFormat), e); + } + else { + throw new ConversionException(String.format("%s for string \"%s\"", e.getMessage(), pString), e); + } + } + + return array; + } + /** * Converts the object to a string, using {@code pObject.toString()}. * @@ -126,10 +159,77 @@ public final class DefaultConverter implements PropertyConverter { throws ConversionException { try { - return (pObject != null ? pObject.toString() : null); + return pObject == null ? null : pObject.getClass().isArray() ? arrayToString(toObjectArray(pObject), pFormat) : pObject.toString(); } catch (RuntimeException rte) { throw new ConversionException(rte); } } + + private String arrayToString(final Object[] pArray, final String pFormat) { + return pFormat == null ? StringUtil.toCSVString(pArray) : StringUtil.toCSVString(pArray, pFormat); + } + + private Object[] toObjectArray(Object pObject) { + // TODO: Extract util method for wrapping/unwrapping native arrays? + Object[] array; + Class componentType = pObject.getClass().getComponentType(); + if (componentType.isPrimitive()) { + if (int.class == componentType) { + array = new Integer[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (short.class == componentType) { + array = new Short[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (long.class == componentType) { + array = new Long[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (float.class == componentType) { + array = new Float[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (double.class == componentType) { + array = new Double[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (boolean.class == componentType) { + array = new Boolean[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (byte.class == componentType) { + array = new Byte[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else if (char.class == componentType) { + array = new Character[Array.getLength(pObject)]; + for (int i = 0; i < array.length; i++) { + Array.set(array, i, Array.get(pObject, i)); + } + } + else { + throw new IllegalArgumentException("Unknown type " + componentType); + } + } + else { + array = (Object[]) pObject; + } + return array; + } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java index 8a3b586b..13c7e322 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java @@ -39,14 +39,14 @@ import java.text.*; * Converts strings to numbers and back. *

* This class has a static cache of {@code NumberFormats}, to avoid - * creation and parsing of numberformats every time one is used. + * creation and parsing of number formats every time one is used. * * @author Harald Kuhr * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java#2 $ */ public class NumberConverter implements PropertyConverter { - // TODO: Need to either make this non-local aware, or document that it is... + // TODO: Need to either make this non-locale aware, or document that it is... private static final DecimalFormatSymbols SYMBOLS = new DecimalFormatSymbols(Locale.US); private static final NumberFormat sDefaultFormat = new DecimalFormat("#0.#", SYMBOLS); @@ -91,7 +91,6 @@ public class NumberConverter implements PropertyConverter { if (pFormat == null) { // Use system default format, using default locale -// format = NumberFormat.getNumberInstance(); format = sDefaultFormat; } else { @@ -160,7 +159,6 @@ public class NumberConverter implements PropertyConverter { try { // Convert to string, default way if (StringUtil.isEmpty(pFormat)) { -// return NumberFormat.getNumberInstance().format(pObject); return sDefaultFormat.format(pObject); } @@ -196,13 +194,11 @@ public class NumberConverter implements PropertyConverter { return null; } - // ...and store in cache sFormats.put(key, format); } return format; } - } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java index 0dbfa61b..ce1219f9 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java @@ -45,8 +45,8 @@ import java.util.regex.PatternSyntaxException; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java#1 $ */ public class RegExTokenIterator extends AbstractTokenIterator { - private final Matcher mMatcher; - private boolean mNext = false; + private final Matcher matcher; + private boolean next = false; /** * Creates a {@code RegExTokenIterator}. @@ -80,7 +80,7 @@ public class RegExTokenIterator extends AbstractTokenIterator { throw new IllegalArgumentException("pattern == null"); } - mMatcher = Pattern.compile(pPattern).matcher(pString); + matcher = Pattern.compile(pPattern).matcher(pString); } /** @@ -88,18 +88,18 @@ public class RegExTokenIterator extends AbstractTokenIterator { * */ public void reset() { - mMatcher.reset(); + matcher.reset(); } public boolean hasNext() { - return mNext || (mNext = mMatcher.find()); + return next || (next = matcher.find()); } public String next() { if (!hasNext()) { throw new NoSuchElementException(); } - mNext = false; - return mMatcher.group(); + next = false; + return matcher.group(); } } \ No newline at end of file diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java index 62dbeb5f..5a40f96e 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java @@ -66,7 +66,7 @@ import java.io.PrintStream; * For each character in the mask a state representing that character is created. * The number of states therefore coincides with the length of the mask. *

  • An alphabet consisting of all legal filename characters - included the two wildcard characters '*' and '?'. - * This alphabet is hard-coded in this class. It contains {a .. å}, {A .. Å}, {0 .. 9}, {.}, {_}, {-}, {*} and {?}. + * This alphabet is hard-coded in this class. It contains {a .. �}, {A .. �}, {0 .. 9}, {.}, {_}, {-}, {*} and {?}. *
  • A finite set of initial states, here only consisting of the state corresponding to the first character in the mask. *
  • A finite set of final states, here only consisting of the state corresponding to the last character in the mask. *
  • A transition relation that is a finite set of transitions satisfying some formal rules.
    @@ -114,14 +114,15 @@ import java.io.PrintStream; * @deprecated Will probably be removed in the near future */ public class WildcardStringParser { + // TODO: Get rid of this class // Constants /** Field ALPHABET */ public static final char[] ALPHABET = { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'æ', - 'ø', 'å', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'N', 'M', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', - 'Z', 'Æ', 'Ø', 'Å', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-' + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\u00e6', + '\u00f8', '\u00e5', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'N', 'M', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', '\u00c6', '\u00d8', '\u00c5', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-' }; /** Field FREE_RANGE_CHARACTER */ @@ -131,11 +132,11 @@ public class WildcardStringParser { public static final char FREE_PASS_CHARACTER = '?'; // Members - boolean mInitialized; - String mStringMask; - WildcardStringParserState mInitialState; - int mTotalNumberOfStringsParsed; - boolean mDebugging; + boolean initialized; + String stringMask; + WildcardStringParserState initialState; + int totalNumberOfStringsParsed; + boolean debugging; PrintStream out; // Properties @@ -171,11 +172,10 @@ public class WildcardStringParser { * @param pDebuggingPrintStream the {@code java.io.PrintStream} to which the debug messages will be emitted. */ public WildcardStringParser(final String pStringMask, final boolean pDebugging, final PrintStream pDebuggingPrintStream) { - - this.mStringMask = pStringMask; - this.mDebugging = pDebugging; + this.stringMask = pStringMask; + this.debugging = pDebugging; this.out = pDebuggingPrintStream; - mInitialized = buildAutomaton(); + initialized = buildAutomaton(); } // Methods @@ -183,12 +183,12 @@ public class WildcardStringParser { WildcardStringParserState runnerState = pState; - while (runnerState.mPreviousState != null) { - runnerState = runnerState.mPreviousState; - if (isFreeRangeCharacter(runnerState.mChar)) { + while (runnerState.previousState != null) { + runnerState = runnerState.previousState; + if (isFreeRangeCharacter(runnerState.character)) { return true; } - if (!isFreePassCharacter(runnerState.mChar)) { + if (!isFreePassCharacter(runnerState.character)) { return false; } // If free-pass char '?' - move on } @@ -197,10 +197,10 @@ public class WildcardStringParser { private boolean checkIfLastFreeRangeState(WildcardStringParserState pState) { - if (isFreeRangeCharacter(pState.mChar)) { + if (isFreeRangeCharacter(pState.character)) { return true; } - if (isFreePassCharacter(pState.mChar)) { + if (isFreePassCharacter(pState.character)) { if (checkIfStateInWildcardRange(pState)) { return true; } @@ -211,8 +211,8 @@ public class WildcardStringParser { /** @return {@code true} if and only if the string mask only consists of free-range wildcard character(s). */ private boolean isTrivialAutomaton() { - for (int i = 0; i < mStringMask.length(); i++) { - if (!isFreeRangeCharacter(mStringMask.charAt(i))) { + for (int i = 0; i < stringMask.length(); i++) { + if (!isFreeRangeCharacter(stringMask.charAt(i))) { return false; } } @@ -227,16 +227,16 @@ public class WildcardStringParser { WildcardStringParserState lastFreeRangeState = null; // Create the initial state of the automaton - if ((mStringMask != null) && (mStringMask.length() > 0)) { - newState = new WildcardStringParserState(mStringMask.charAt(0)); - newState.mAutomatonStateNumber = 0; - newState.mPreviousState = null; + if ((stringMask != null) && (stringMask.length() > 0)) { + newState = new WildcardStringParserState(stringMask.charAt(0)); + newState.automatonStateNumber = 0; + newState.previousState = null; if (checkIfLastFreeRangeState(newState)) { lastFreeRangeState = newState; } runnerState = newState; - mInitialState = runnerState; - mInitialState.mAutomatonStateNumber = 0; + initialState = runnerState; + initialState.automatonStateNumber = 0; } else { System.err.println("string mask provided are null or empty - aborting!"); @@ -244,8 +244,8 @@ public class WildcardStringParser { } // Create the rest of the automaton - for (int i = 1; i < mStringMask.length(); i++) { - activeChar = mStringMask.charAt(i); + for (int i = 1; i < stringMask.length(); i++) { + activeChar = stringMask.charAt(i); // Check if the char is an element in the alphabet or is a wildcard character if (!((isInAlphabet(activeChar)) || (isWildcardCharacter(activeChar)))) { @@ -254,12 +254,12 @@ public class WildcardStringParser { } // Set last free-range state before creating/checking the next state - runnerState.mLastFreeRangeState = lastFreeRangeState; + runnerState.lastFreeRangeState = lastFreeRangeState; // Create next state, check if free-range state, set the state number and preceeding state newState = new WildcardStringParserState(activeChar); - newState.mAutomatonStateNumber = i; - newState.mPreviousState = runnerState; + newState.automatonStateNumber = i; + newState.previousState = runnerState; // Special check if the state represents an '*' or '?' with only preceeding states representing '?' and '*' if (checkIfLastFreeRangeState(newState)) { @@ -267,19 +267,19 @@ public class WildcardStringParser { } // Set the succeding state before moving to the next state - runnerState.mNextState = newState; + runnerState.nextState = newState; // Move to the next state runnerState = newState; // Special setting of the last free-range state for the last element - if (runnerState.mAutomatonStateNumber == mStringMask.length() - 1) { - runnerState.mLastFreeRangeState = lastFreeRangeState; + if (runnerState.automatonStateNumber == stringMask.length() - 1) { + runnerState.lastFreeRangeState = lastFreeRangeState; } } // Initiate some statistics - mTotalNumberOfStringsParsed = 0; + totalNumberOfStringsParsed = 0; return true; } @@ -316,7 +316,7 @@ public class WildcardStringParser { * @return the string mask used for building the parser automaton. */ public String getStringMask() { - return mStringMask; + return stringMask; } /** @@ -328,16 +328,16 @@ public class WildcardStringParser { */ public boolean parseString(final String pStringToParse) { - if (mDebugging) { + if (debugging) { out.println("parsing \"" + pStringToParse + "\"..."); } // Update statistics - mTotalNumberOfStringsParsed++; + totalNumberOfStringsParsed++; // Check string to be parsed for nullness if (pStringToParse == null) { - if (mDebugging) { + if (debugging) { out.println("string to be parsed is null - rejection!"); } return false; @@ -348,21 +348,21 @@ public class WildcardStringParser { // Check string to be parsed if (!parsableString.checkString()) { - if (mDebugging) { + if (debugging) { out.println("one or more characters in string to be parsed are not legal characters - rejection!"); } return false; } // Check if automaton is correctly initialized - if (!mInitialized) { + if (!initialized) { System.err.println("automaton is not initialized - rejection!"); return false; } // Check if automaton is trivial (accepts all strings) if (isTrivialAutomaton()) { - if (mDebugging) { + if (debugging) { out.println("automaton represents a trivial string mask (accepts all strings) - acceptance!"); } return true; @@ -370,7 +370,7 @@ public class WildcardStringParser { // Check if string to be parsed is empty if (parsableString.isEmpty()) { - if (mDebugging) { + if (debugging) { out.println("string to be parsed is empty and not trivial automaton - rejection!"); } return false; @@ -383,59 +383,59 @@ public class WildcardStringParser { WildcardStringParserState runnerState = null; // Accepted by the first state? - if ((parsableString.mCharArray[0] == mInitialState.mChar) || isWildcardCharacter(mInitialState.mChar)) { - runnerState = mInitialState; - parsableString.mIndex = 0; + if ((parsableString.charArray[0] == initialState.character) || isWildcardCharacter(initialState.character)) { + runnerState = initialState; + parsableString.index = 0; } else { - if (mDebugging) { + if (debugging) { out.println("cannot enter first automaton state - rejection!"); } return false; } // Initialize the free-pass character state visited count - if (isFreePassCharacter(runnerState.mChar)) { + if (isFreePassCharacter(runnerState.character)) { numberOfFreePassCharactersRead_SinceLastFreePassState++; } // Perform parsing according to the rules above for (int i = 0; i < parsableString.length(); i++) { - if (mDebugging) { + if (debugging) { out.println(); } - if (mDebugging) { + if (debugging) { out.println("parsing - index number " + i + ", active char: '" - + parsableString.getActiveChar() + "' char string index: " + parsableString.mIndex + + parsableString.getActiveChar() + "' char string index: " + parsableString.index + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); } - if (mDebugging) { - out.println("parsing - state: " + runnerState.mAutomatonStateNumber + " '" - + runnerState.mChar + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); + if (debugging) { + out.println("parsing - state: " + runnerState.automatonStateNumber + " '" + + runnerState.character + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); } - if (mDebugging) { + if (debugging) { out.println("parsing - hasPerformedFreeRangeMovement: " + hasPerformedFreeRangeMovement); } - if (runnerState.mNextState == null) { - if (mDebugging) { - out.println("parsing - runnerState.mNextState == null"); + if (runnerState.nextState == null) { + if (debugging) { + out.println("parsing - runnerState.nextState == null"); } // If there are no subsequent state (final state) and the state represents '*' - acceptance! - if (isFreeRangeCharacter(runnerState.mChar)) { + if (isFreeRangeCharacter(runnerState.character)) { // Special free-range skipping check if (hasPerformedFreeRangeMovement) { if (parsableString.reachedEndOfString()) { if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { - if (mDebugging) { + if (debugging) { out.println( "no subsequent state (final state) and the state represents '*' - end of parsing string, but not enough characters read - rejection!"); } return false; } else { - if (mDebugging) { + if (debugging) { out.println( "no subsequent state (final state) and the state represents '*' - end of parsing string and enough characters read - acceptance!"); } @@ -444,15 +444,15 @@ public class WildcardStringParser { } else { if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { - if (mDebugging) { + if (debugging) { out.println( "no subsequent state (final state) and the state represents '*' - not the end of parsing string and not enough characters read - read next character"); } - parsableString.mIndex++; + parsableString.index++; numberOfParsedCharactersRead_SinceLastFreePassState++; } else { - if (mDebugging) { + if (debugging) { out.println( "no subsequent state (final state) and the state represents '*' - not the end of parsing string, but enough characters read - acceptance!"); } @@ -461,7 +461,7 @@ public class WildcardStringParser { } } else { - if (mDebugging) { + if (debugging) { out.println("no subsequent state (final state) and the state represents '*' - no skipping performed - acceptance!"); } return true; @@ -474,58 +474,58 @@ public class WildcardStringParser { // Special free-range skipping check if ((hasPerformedFreeRangeMovement) && (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState)) { - if (mDebugging) { + if (debugging) { out.println( "no subsequent state (final state) and skipping has been performed and end of parsing string, but not enough characters read - rejection!"); } return false; } - if (mDebugging) { + if (debugging) { out.println("no subsequent state (final state) and the end of the string to test is reached - acceptance!"); } return true; } else { - if (mDebugging) { + if (debugging) { out.println("parsing - escaping process..."); } } } else { - if (mDebugging) { - out.println("parsing - runnerState.mNextState != null"); + if (debugging) { + out.println("parsing - runnerState.nextState != null"); } // Special Case: // If this state represents '*' - go to the rightmost state representing '?'. // This state will act as an '*' - except that you only can go to the next state or accept the string, if and only if the number of '?' read are equal or less than the number of character read from the parsing string. - if (isFreeRangeCharacter(runnerState.mChar)) { + if (isFreeRangeCharacter(runnerState.character)) { numberOfFreePassCharactersRead_SinceLastFreePassState = 0; numberOfParsedCharactersRead_SinceLastFreePassState = 0; - WildcardStringParserState freeRangeRunnerState = runnerState.mNextState; + WildcardStringParserState freeRangeRunnerState = runnerState.nextState; - while ((freeRangeRunnerState != null) && (isFreePassCharacter(freeRangeRunnerState.mChar))) { + while ((freeRangeRunnerState != null) && (isFreePassCharacter(freeRangeRunnerState.character))) { runnerState = freeRangeRunnerState; hasPerformedFreeRangeMovement = true; numberOfFreePassCharactersRead_SinceLastFreePassState++; - freeRangeRunnerState = freeRangeRunnerState.mNextState; + freeRangeRunnerState = freeRangeRunnerState.nextState; } // Special Case: if the mask is at the end - if (runnerState.mNextState == null) { - if (mDebugging) { + if (runnerState.nextState == null) { + if (debugging) { out.println(); } - if (mDebugging) { + if (debugging) { out.println("parsing - index number " + i + ", active char: '" - + parsableString.getActiveChar() + "' char string index: " + parsableString.mIndex + + parsableString.getActiveChar() + "' char string index: " + parsableString.index + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); } - if (mDebugging) { - out.println("parsing - state: " + runnerState.mAutomatonStateNumber + " '" - + runnerState.mChar + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); + if (debugging) { + out.println("parsing - state: " + runnerState.automatonStateNumber + " '" + + runnerState.character + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); } - if (mDebugging) { + if (debugging) { out.println("parsing - hasPerformedFreeRangeMovement: " + hasPerformedFreeRangeMovement); } @@ -540,42 +540,42 @@ public class WildcardStringParser { } // If the next state represents '*' - go to this next state - if (isFreeRangeCharacter(runnerState.mNextState.mChar)) { - runnerState = runnerState.mNextState; - parsableString.mIndex++; + if (isFreeRangeCharacter(runnerState.nextState.character)) { + runnerState = runnerState.nextState; + parsableString.index++; numberOfParsedCharactersRead_SinceLastFreePassState++; } // If the next state represents '?' - go to this next state - else if (isFreePassCharacter(runnerState.mNextState.mChar)) { - runnerState = runnerState.mNextState; - parsableString.mIndex++; + else if (isFreePassCharacter(runnerState.nextState.character)) { + runnerState = runnerState.nextState; + parsableString.index++; numberOfFreePassCharactersRead_SinceLastFreePassState++; numberOfParsedCharactersRead_SinceLastFreePassState++; } // If the next state represents the same character as the next character in the string to test - go to this next state - else if ((!parsableString.reachedEndOfString()) && (runnerState.mNextState.mChar == parsableString.getSubsequentChar())) { - runnerState = runnerState.mNextState; - parsableString.mIndex++; + else if ((!parsableString.reachedEndOfString()) && (runnerState.nextState.character == parsableString.getSubsequentChar())) { + runnerState = runnerState.nextState; + parsableString.index++; numberOfParsedCharactersRead_SinceLastFreePassState++; } // If the next character in the string to test does not coincide with the next state - go to the last state representing '*'. If there are none - rejection! - else if (runnerState.mLastFreeRangeState != null) { - runnerState = runnerState.mLastFreeRangeState; - parsableString.mIndex++; + else if (runnerState.lastFreeRangeState != null) { + runnerState = runnerState.lastFreeRangeState; + parsableString.index++; numberOfParsedCharactersRead_SinceLastFreePassState++; } else { - if (mDebugging) { + if (debugging) { out.println("the next state does not represent the same character as the next character in the string to test, and there are no last-free-range-state - rejection!"); } return false; } } } - if (mDebugging) { + if (debugging) { out.println("finished reading parsing string and not at any final state - rejection!"); } return false; @@ -594,42 +594,42 @@ public class WildcardStringParser { StringBuilder buffer = new StringBuilder(); - if (!mInitialized) { + if (!initialized) { buffer.append(getClass().getName()); buffer.append(": Not initialized properly!"); buffer.append("\n"); buffer.append("\n"); } else { - WildcardStringParserState runnerState = mInitialState; + WildcardStringParserState runnerState = initialState; buffer.append(getClass().getName()); buffer.append(": String mask "); - buffer.append(mStringMask); + buffer.append(stringMask); buffer.append("\n"); buffer.append("\n"); buffer.append(" Automaton: "); while (runnerState != null) { - buffer.append(runnerState.mAutomatonStateNumber); + buffer.append(runnerState.automatonStateNumber); buffer.append(": "); - buffer.append(runnerState.mChar); + buffer.append(runnerState.character); buffer.append(" ("); - if (runnerState.mLastFreeRangeState != null) { - buffer.append(runnerState.mLastFreeRangeState.mAutomatonStateNumber); + if (runnerState.lastFreeRangeState != null) { + buffer.append(runnerState.lastFreeRangeState.automatonStateNumber); } else { buffer.append("-"); } buffer.append(")"); - if (runnerState.mNextState != null) { + if (runnerState.nextState != null) { buffer.append(" --> "); } - runnerState = runnerState.mNextState; + runnerState = runnerState.nextState; } buffer.append("\n"); buffer.append(" Format: : ()"); buffer.append("\n"); - buffer.append(" Number of strings parsed: " + mTotalNumberOfStringsParsed); + buffer.append(" Number of strings parsed: " + totalNumberOfStringsParsed); buffer.append("\n"); } return buffer.toString(); @@ -646,7 +646,7 @@ public class WildcardStringParser { if (pObject instanceof WildcardStringParser) { WildcardStringParser externalParser = (WildcardStringParser) pObject; - return ((externalParser.mInitialized == this.mInitialized) && (externalParser.mStringMask == this.mStringMask)); + return ((externalParser.initialized == this.initialized) && (externalParser.stringMask == this.stringMask)); } return super.equals(pObject); } @@ -664,8 +664,8 @@ public class WildcardStringParser { protected Object clone() throws CloneNotSupportedException { - if (mInitialized) { - return new WildcardStringParser(mStringMask); + if (initialized) { + return new WildcardStringParser(stringMask); } return null; } @@ -679,11 +679,11 @@ public class WildcardStringParser { // Constants // Members - int mAutomatonStateNumber; - char mChar; - WildcardStringParserState mPreviousState; - WildcardStringParserState mNextState; - WildcardStringParserState mLastFreeRangeState; + int automatonStateNumber; + char character; + WildcardStringParserState previousState; + WildcardStringParserState nextState; + WildcardStringParserState lastFreeRangeState; // Constructors @@ -693,7 +693,7 @@ public class WildcardStringParser { * @param pChar */ public WildcardStringParserState(final char pChar) { - this.mChar = pChar; + this.character = pChar; } // Methods @@ -705,34 +705,34 @@ public class WildcardStringParser { // Constants // Members - char[] mCharArray; - int mIndex; + char[] charArray; + int index; // Constructors ParsableString(final String pStringToParse) { if (pStringToParse != null) { - mCharArray = pStringToParse.toCharArray(); + charArray = pStringToParse.toCharArray(); } - mIndex = -1; + index = -1; } // Methods boolean reachedEndOfString() { - //System.out.println(DebugUtil.DEBUG + DebugUtil.getClassName(this) + ": mIndex :" + mIndex); - //System.out.println(DebugUtil.DEBUG + DebugUtil.getClassName(this) + ": mCharArray.length :" + mCharArray.length); - return mIndex == mCharArray.length - 1; + //System.out.println(DebugUtil.DEBUG + DebugUtil.getClassName(this) + ": index :" + index); + //System.out.println(DebugUtil.DEBUG + DebugUtil.getClassName(this) + ": charArray.length :" + charArray.length); + return index == charArray.length - 1; } int length() { - return mCharArray.length; + return charArray.length; } char getActiveChar() { - if ((mIndex > -1) && (mIndex < mCharArray.length)) { - return mCharArray[mIndex]; + if ((index > -1) && (index < charArray.length)) { + return charArray[index]; } System.err.println(getClass().getName() + ": trying to access character outside character array!"); return ' '; @@ -740,8 +740,8 @@ public class WildcardStringParser { char getSubsequentChar() { - if ((mIndex > -1) && (mIndex + 1 < mCharArray.length)) { - return mCharArray[mIndex + 1]; + if ((index > -1) && (index + 1 < charArray.length)) { + return charArray[index + 1]; } System.err.println(getClass().getName() + ": trying to access character outside character array!"); return ' '; @@ -752,8 +752,8 @@ public class WildcardStringParser { if (!isEmpty()) { // Check if the string only contains chars that are elements in the alphabet - for (int i = 0; i < mCharArray.length; i++) { - if (!WildcardStringParser.isInAlphabet(mCharArray[i])) { + for (int i = 0; i < charArray.length; i++) { + if (!WildcardStringParser.isInAlphabet(charArray[i])) { return false; } } @@ -762,7 +762,7 @@ public class WildcardStringParser { } boolean isEmpty() { - return ((mCharArray == null) || (mCharArray.length == 0)); + return ((charArray == null) || (charArray.length == 0)); } /** @@ -771,7 +771,7 @@ public class WildcardStringParser { * @return */ public String toString() { - return new String(mCharArray); + return new String(charArray); } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java index e6ce626d..c375d955 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java @@ -59,13 +59,13 @@ import java.util.*; * * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java#2 $ + * @version $Id: com/twelvemonkeys/util/service/ServiceRegistry.java#2 $ * @see RegisterableService * @see JAR File Specification */ public class ServiceRegistry { // TODO: Security issues? - // TODO: Application contexts? + // TODO: Application contexts? Probably use instance per thread group.. /** * "META-INF/services/" @@ -73,7 +73,7 @@ public class ServiceRegistry { public static final String SERVICES = "META-INF/services/"; // Class to CategoryRegistry mapping - private final Map, CategoryRegistry> mCategoryMap; + private final Map, CategoryRegistry> categoryMap; /** * Creates a {@code ServiceRegistry} instance with a set of categories @@ -98,7 +98,7 @@ public class ServiceRegistry { } // NOTE: Categories are constant for the lifetime of a registry - mCategoryMap = Collections.unmodifiableMap(map); + categoryMap = Collections.unmodifiableMap(map); } private void putCategory(Map, CategoryRegistry> pMap, Class pCategory) { @@ -110,7 +110,7 @@ public class ServiceRegistry { * Registers all provider implementations for this {@code ServiceRegistry} * found in the application classpath. * - * @throws ServiceConfigurationError if an error occured during registration + * @throws ServiceConfigurationError if an error occurred during registration */ public void registerApplicationClasspathSPIs() { ClassLoader loader = Thread.currentThread().getContextClassLoader(); @@ -140,7 +140,7 @@ public class ServiceRegistry { * * @param pResource the resource to load SPIs from * @param pCategory the category class - * @param pLoader the classloader to use + * @param pLoader the class loader to use */ void registerSPIs(final URL pResource, final Class pCategory, final ClassLoader pLoader) { Properties classNames = new Properties(); @@ -154,7 +154,7 @@ public class ServiceRegistry { if (!classNames.isEmpty()) { @SuppressWarnings({"unchecked"}) - CategoryRegistry registry = mCategoryMap.get(pCategory); + CategoryRegistry registry = categoryMap.get(pCategory); Set providerClassNames = classNames.keySet(); @@ -213,7 +213,7 @@ public class ServiceRegistry { * @return an {@code Iterator} containing all categories in this registry. */ protected Iterator> categories() { - return mCategoryMap.keySet().iterator(); + return categoryMap.keySet().iterator(); } /** @@ -242,7 +242,7 @@ public class ServiceRegistry { * The iterator supports removal. *

    * - * NOTE: Removing a category from the iterator, deregisters + * NOTE: Removing a category from the iterator, de-registers * {@code pProvider} from the current category (as returned by the last * invocation of {@code next()}), it does not remove the category * itself from the registry. @@ -257,21 +257,22 @@ public class ServiceRegistry { return new FilterIterator>(categories(), new FilterIterator.Filter>() { public boolean accept(Class pElement) { - return getRegistry(pElement).contatins(pProvider); + return getRegistry(pElement).contains(pProvider); } }) { - Class mCurrent; + Class current; public Class next() { - return (mCurrent = super.next()); + return (current = super.next()); } public void remove() { - if (mCurrent == null) { + if (current == null) { throw new IllegalStateException("No current element"); } - getRegistry(mCurrent).deregister(pProvider); - mCurrent = null; + + getRegistry(current).deregister(pProvider); + current = null; } }; } @@ -284,7 +285,7 @@ public class ServiceRegistry { */ private CategoryRegistry getRegistry(final Class pCategory) { @SuppressWarnings({"unchecked"}) - CategoryRegistry registry = mCategoryMap.get(pCategory); + CategoryRegistry registry = categoryMap.get(pCategory); if (registry == null) { throw new IllegalArgumentException("No such category: " + pCategory.getName()); } @@ -296,7 +297,7 @@ public class ServiceRegistry { * * @param pProvider the provider instance * @return {@code true} if {@code pProvider} is now registered in - * one or more categories + * one or more categories it was not registered in before. * @see #compatibleCategories(Object) */ public boolean register(final Object pProvider) { @@ -328,12 +329,12 @@ public class ServiceRegistry { } /** - * Deregisters the given provider from all categories it's currently + * De-registers the given provider from all categories it's currently * registered in. * * @param pProvider the provider instance * @return {@code true} if {@code pProvider} was previously registered in - * any category + * any category and is now de-registered. * @see #containingCategories(Object) */ public boolean deregister(final Object pProvider) { @@ -366,27 +367,26 @@ public class ServiceRegistry { * Keeps track of each individual category. */ class CategoryRegistry { - private final Class mCategory; - private final Map mProviders = new LinkedHashMap(); + private final Class category; + private final Map providers = new LinkedHashMap(); CategoryRegistry(Class pCategory) { Validate.notNull(pCategory, "category"); - mCategory = pCategory; + category = pCategory; } private void checkCategory(final Object pProvider) { - if (!mCategory.isInstance(pProvider)) { - throw new IllegalArgumentException(pProvider + " not instance of category " + mCategory.getName()); + if (!category.isInstance(pProvider)) { + throw new IllegalArgumentException(pProvider + " not instance of category " + category.getName()); } } public boolean register(final T pProvider) { checkCategory(pProvider); - // NOTE: We only register the new instance, if we don't allready - // have an instance of pProvider's class. - if (!contatins(pProvider)) { - mProviders.put(pProvider.getClass(), pProvider); + // NOTE: We only register the new instance, if we don't already have an instance of pProvider's class. + if (!contains(pProvider)) { + providers.put(pProvider.getClass(), pProvider); processRegistration(pProvider); return true; } @@ -397,7 +397,7 @@ public class ServiceRegistry { void processRegistration(final T pProvider) { if (pProvider instanceof RegisterableService) { RegisterableService service = (RegisterableService) pProvider; - service.onRegistration(ServiceRegistry.this, mCategory); + service.onRegistration(ServiceRegistry.this, category); } } @@ -406,7 +406,7 @@ public class ServiceRegistry { // NOTE: We remove any provider of the same class, this may or may // not be the same instance as pProvider. - T oldProvider = mProviders.remove(pProvider.getClass()); + T oldProvider = providers.remove(pProvider.getClass()); if (oldProvider != null) { processDeregistration(oldProvider); @@ -419,12 +419,12 @@ public class ServiceRegistry { void processDeregistration(final T pOldProvider) { if (pOldProvider instanceof RegisterableService) { RegisterableService service = (RegisterableService) pOldProvider; - service.onDeregistration(ServiceRegistry.this, mCategory); + service.onDeregistration(ServiceRegistry.this, category); } } - public boolean contatins(final Object pProvider) { - return mProviders.containsKey(pProvider.getClass()); + public boolean contains(final Object pProvider) { + return providers.containsKey(pProvider != null ? pProvider.getClass() : null); } public Iterator providers() { @@ -432,9 +432,9 @@ public class ServiceRegistry { // using the deregister method will result in // ConcurrentModificationException in the iterator.. // We wrap the iterator to track deregistration right. - final Iterator iterator = mProviders.values().iterator(); + final Iterator iterator = providers.values().iterator(); return new Iterator() { - T mCurrent; + T current; public boolean hasNext() { return iterator.hasNext(); @@ -442,12 +442,12 @@ public class ServiceRegistry { } public T next() { - return (mCurrent = iterator.next()); + return (current = iterator.next()); } public void remove() { iterator.remove(); - processDeregistration(mCurrent); + processDeregistration(current); } }; } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java index f009905a..2b567ed6 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java @@ -2,10 +2,10 @@ package com.twelvemonkeys.lang; import junit.framework.TestCase; -import java.util.Map; -import java.util.HashMap; +import java.io.Serializable; import java.lang.reflect.InvocationTargetException; -import java.text.NumberFormat; +import java.util.HashMap; +import java.util.Map; /** * BeanUtilTestCase @@ -19,7 +19,7 @@ public class BeanUtilTestCase extends TestCase { public void testConfigureNoMehtod() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); map.put("noSuchMethod", "jaffa"); @@ -34,7 +34,7 @@ public class BeanUtilTestCase extends TestCase { public void testConfigureNoMethodArgs() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); map.put("doubleValue", new Object()); // Should not be able to convert this @@ -52,7 +52,7 @@ public class BeanUtilTestCase extends TestCase { public void testConfigureNullValue() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); map.put("stringValue", null); @@ -69,11 +69,11 @@ public class BeanUtilTestCase extends TestCase { public void testConfigureSimple() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); map.put("stringValue", "one"); - map.put("intValue", new Integer(2)); - map.put("doubleValue", new Double(.3)); + map.put("intValue", 2); + map.put("doubleValue", .3); try { BeanUtil.configure(bean, map); @@ -84,17 +84,17 @@ public class BeanUtilTestCase extends TestCase { assertEquals("one", bean.getStringValue()); assertEquals(2, bean.getIntValue()); - assertEquals(new Double(.3), bean.getDoubleValue()); + assertEquals(.3, bean.getDoubleValue()); } public void testConfigureConvert() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); - map.put("stringValue", new Integer(1)); + map.put("stringValue", 1); map.put("intValue", "2"); - map.put("doubleValue", NumberFormat.getNumberInstance().format(0.3)); // Note, format is locale specific... + map.put("doubleValue", ".3"); try { BeanUtil.configure(bean, map); @@ -105,13 +105,13 @@ public class BeanUtilTestCase extends TestCase { assertEquals("1", bean.getStringValue()); assertEquals(2, bean.getIntValue()); - assertEquals(new Double(.3), bean.getDoubleValue()); + assertEquals(0.3, bean.getDoubleValue()); } public void testConfigureAmbigious1() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); String value = "one"; map.put("ambigious", value); @@ -133,9 +133,9 @@ public class BeanUtilTestCase extends TestCase { public void testConfigureAmbigious2() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); - Integer value = new Integer(2); + Integer value = 2; map.put("ambigious", value); try { @@ -147,7 +147,7 @@ public class BeanUtilTestCase extends TestCase { assertNotNull(bean.getAmbigious()); assertEquals("Integer converted rather than invoking setAmbigiouos(Integer), ordering not predictable", - new Integer(2), bean.getAmbigious()); + 2, bean.getAmbigious()); assertSame("Integer converted rather than invoking setAmbigiouos(Integer), ordering not predictable", value, bean.getAmbigious()); } @@ -155,9 +155,9 @@ public class BeanUtilTestCase extends TestCase { public void testConfigureAmbigious3() { TestBean bean = new TestBean(); - Map map = new HashMap(); + Map map = new HashMap(); - Double value = new Double(.3); + Double value = .3; map.put("ambigious", value); try { @@ -175,54 +175,54 @@ public class BeanUtilTestCase extends TestCase { } static class TestBean { - private String mString; - private int mInt; - private Double mDouble; + private String stringVal; + private int intVal; + private Double doubleVal; - private Object mAmbigious; + private Object ambigious; public Double getDoubleValue() { - return mDouble; + return doubleVal; } public int getIntValue() { - return mInt; + return intVal; } public String getStringValue() { - return mString; + return stringVal; } public void setStringValue(String pString) { - mString = pString; + stringVal = pString; } public void setIntValue(int pInt) { - mInt = pInt; + intVal = pInt; } public void setDoubleValue(Double pDouble) { - mDouble = pDouble; + doubleVal = pDouble; } public void setAmbigious(String pString) { - mAmbigious = pString; + ambigious = pString; } public void setAmbigious(Object pObject) { - mAmbigious = pObject; + ambigious = pObject; } public void setAmbigious(Integer pInteger) { - mAmbigious = pInteger; + ambigious = pInteger; } public void setAmbigious(int pInt) { - mAmbigious = new Long(pInt); // Just to differentiate... + ambigious = (long) pInt; // Just to differentiate... } public Object getAmbigious() { - return mAmbigious; + return ambigious; } } } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java new file mode 100644 index 00000000..f6dce2ab --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/DateUtilTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +import org.junit.Test; + +import java.util.Calendar; +import java.util.TimeZone; + +import static org.junit.Assert.*; + +/** + * DateUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: DateUtilTest.java,v 1.0 11.04.12 16:21 haraldk Exp$ + */ +public class DateUtilTest { + private static Calendar getCalendar(long time) { + Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); + calendar.setTimeInMillis(time); + + return calendar; + } + + @Test + public void testRoundToSecond() { + Calendar calendar = getCalendar(DateUtil.roundToSecond(System.currentTimeMillis())); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + } + + @Test + public void testRoundToMinute() { + Calendar calendar = getCalendar(DateUtil.roundToMinute(System.currentTimeMillis())); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + } + + @Test + public void testRoundToHour() { + Calendar calendar = getCalendar(DateUtil.roundToHour(System.currentTimeMillis())); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + assertEquals(0, calendar.get(Calendar.MINUTE)); + } + + @Test + public void testRoundToDay() { + Calendar calendar = getCalendar(DateUtil.roundToDay(System.currentTimeMillis())); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + assertEquals(0, calendar.get(Calendar.MINUTE)); + assertEquals(0, calendar.get(Calendar.HOUR_OF_DAY)); + } + + @Test + public void testCurrentTimeSecond() { + Calendar calendar = getCalendar(DateUtil.currentTimeSecond()); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + } + + @Test + public void testCurrentTimeMinute() { + Calendar calendar = getCalendar(DateUtil.currentTimeMinute()); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + } + + @Test + public void testCurrentTimeHour() { + Calendar calendar = getCalendar(DateUtil.currentTimeHour()); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + assertEquals(0, calendar.get(Calendar.MINUTE)); + } + + @Test + public void testCurrentTimeDay() { + Calendar calendar = getCalendar(DateUtil.currentTimeDay()); + + assertEquals(0, calendar.get(Calendar.MILLISECOND)); + assertEquals(0, calendar.get(Calendar.SECOND)); + assertEquals(0, calendar.get(Calendar.MINUTE)); + assertEquals(0, calendar.get(Calendar.HOUR_OF_DAY)); + } +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java index 810f446e..6ed3f622 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java @@ -1,9 +1,11 @@ package com.twelvemonkeys.lang; -import junit.framework.TestCase; +import org.junit.Test; -import java.lang.reflect.Method; import java.io.*; +import java.lang.reflect.Method; + +import static org.junit.Assert.*; /** * AbstractObjectTestCase @@ -12,7 +14,7 @@ import java.io.*; * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java#1 $ */ -public abstract class ObjectAbstractTestCase extends TestCase { +public abstract class ObjectAbstractTestCase { // TODO: See com.tm.util.ObjectAbstractTestCase // TODO: The idea is that this should be some generic base-class that // implements the basic object tests @@ -20,19 +22,6 @@ public abstract class ObjectAbstractTestCase extends TestCase { // TODO: Create Serializable test similar way // TODO: Create Comparable test similar way - /** - * Creates a {@code TestCase}. - * - * @param testName the test class name - */ - protected ObjectAbstractTestCase(String testName) { - super(testName); - } - - protected ObjectAbstractTestCase() { - super(); - } - /** * Returns an instance of the class we are testing. * Implement this method to return the object to test. @@ -47,23 +36,23 @@ public abstract class ObjectAbstractTestCase extends TestCase { //protected abstract Object makeEqualObject(Object pObject); + @Test public void testToString() { assertNotNull(makeObject().toString()); // TODO: What more can we test? } // TODO: assert that either BOTH or NONE of equals/hashcode is overridden + @Test public void testEqualsHashCode(){ Object obj = makeObject(); Class cl = obj.getClass(); if (isEqualsOverriden(cl)) { - assertTrue("Class " + cl.getName() - + " implements equals but not hashCode", isHashCodeOverriden(cl)); + assertTrue("Class " + cl.getName() + " implements equals but not hashCode", isHashCodeOverriden(cl)); } else if (isHashCodeOverriden(cl)) { - assertTrue("Class " + cl.getName() - + " implements hashCode but not equals", isEqualsOverriden(cl)); + assertTrue("Class " + cl.getName() + " implements hashCode but not equals", isEqualsOverriden(cl)); } } @@ -85,11 +74,13 @@ public abstract class ObjectAbstractTestCase extends TestCase { } } + @Test public void testObjectEqualsSelf() { Object obj = makeObject(); assertEquals("An Object should equal itself", obj, obj); } + @Test public void testEqualsNull() { Object obj = makeObject(); // NOTE: Makes sure this doesn't throw NPE either @@ -97,11 +88,13 @@ public abstract class ObjectAbstractTestCase extends TestCase { assertFalse("An object should never equal null", obj.equals(null)); } + @Test public void testObjectHashCodeEqualsSelfHashCode() { Object obj = makeObject(); assertEquals("hashCode should be repeatable", obj.hashCode(), obj.hashCode()); } + @Test public void testObjectHashCodeEqualsContract() { Object obj1 = makeObject(); if (obj1.equals(obj1)) { @@ -129,6 +122,7 @@ public abstract class ObjectAbstractTestCase extends TestCase { //////////////////////////////////////////////////////////////////////////// // Cloneable interface + @Test public void testClone() throws Exception { Object obj = makeObject(); if (obj instanceof Cloneable) { @@ -143,7 +137,7 @@ public abstract class ObjectAbstractTestCase extends TestCase { clone.setAccessible(true); } - Object cloned = clone.invoke(obj, null); + Object cloned = clone.invoke(obj); assertNotNull("Cloned object should never be null", cloned); @@ -184,6 +178,7 @@ public abstract class ObjectAbstractTestCase extends TestCase { /////////////////////////////////////////////////////////////////////////// // Serializable interface + @Test public void testSerializeDeserializeThenCompare() throws Exception { Object obj = makeObject(); if (obj instanceof Serializable) { @@ -223,6 +218,7 @@ public abstract class ObjectAbstractTestCase extends TestCase { * @throws java.io.IOException * @throws ClassNotFoundException */ + @Test public void testSimpleSerialization() throws Exception { Object o = makeObject(); if (o instanceof Serializable) { @@ -305,14 +301,6 @@ public abstract class ObjectAbstractTestCase extends TestCase { } public static final class SanityTestTestCase extends ObjectAbstractTestCase { - /** - * Creates a {@code TestCase}. - * - */ - public SanityTestTestCase() { - super(SanityTestTestCase.class.getName()); - } - protected Object makeObject() { return new Cloneable() {}; } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/PlatformTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/PlatformTest.java new file mode 100644 index 00000000..ed1aa5e7 --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/PlatformTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * PlatformTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PlatformTest.java,v 1.0 11.04.12 16:21 haraldk Exp$ + */ +public class PlatformTest { + // TODO: Make a decision: 32/64 bit part of architecture, or separate property? + // TODO: Should all i386/386/x86/i686 be normalized to x86? + // TODO: Create a version class, to allow testing for equal or greater version requirements etc + // Break down version strings to tokens, compare numbers to numbers, strings to strings, etc. + // - 10.0 > 2.1 (numeric, numeric) + // - beta > alpha (alpha) + // - 1A > 1 + // - 10 > 1A (what about hex numbering, does anyone use that?) + // - 1.0B > 1.0A (numeric, numeric, alpha) + // - 10.0RC > 10.0a > 10.0 ?? (numeric, numeric, alpha) + // - Feisty Fawn > Dapper Drake + // - special recognition of 'alpha', 'beta', 'rc' etc, to represent negative values?? + // Have a look at the Maven version scheme/algorithm, and the JAR file spec (Package#isCompatibleWith(String)) + + @Test + public void testGet() { + assertNotNull(Platform.get()); + } + + @Test + public void testOS() { + assertNotNull(Platform.os()); + assertEquals(Platform.get().getOS(), Platform.os()); + } + + @Test + public void testVersion() { + assertNotNull(Platform.version()); + assertEquals(Platform.get().getVersion(), Platform.version()); + assertEquals(System.getProperty("os.version"), Platform.version()); + } + + @Test + public void testArch() { + assertNotNull(Platform.arch()); + assertEquals(Platform.get().getArchitecture(), Platform.arch()); + } + + private static Properties createProperties(final String osName, final String osVersion, final String osArch) { + Properties properties = new Properties(); + properties.put("os.name", osName); + properties.put("os.version", osVersion); + properties.put("os.arch", osArch); + + return properties; + } + + @Test + public void testCreateOSXx86_64() { + Platform platform = new Platform(createProperties("Mac OS X", "10.7.3", "x86_64")); + assertEquals(Platform.OperatingSystem.MacOS, platform.getOS()); + assertEquals(Platform.Architecture.X86, platform.getArchitecture()); + } + + @Test + public void testCreateOSXDarwinx86() { + Platform platform = new Platform(createProperties("Darwin", "0.0.0", "x86")); + assertEquals(Platform.OperatingSystem.MacOS, platform.getOS()); + assertEquals(Platform.Architecture.X86, platform.getArchitecture()); + } + + @Test + public void testCreateOSXPPC() { + Platform platform = new Platform(createProperties("Mac OS X", "10.5.4", "PPC")); + assertEquals(Platform.OperatingSystem.MacOS, platform.getOS()); + assertEquals(Platform.Architecture.PPC, platform.getArchitecture()); + } + + @Test + public void testCreateWindows386() { + Platform platform = new Platform(createProperties("Windows", "7.0.1.1", "i386")); + assertEquals(Platform.OperatingSystem.Windows, platform.getOS()); + assertEquals(Platform.Architecture.X86, platform.getArchitecture()); + } + + @Ignore("Known issue, needs resolve") + @Test + public void testCreateWindows686() { + Platform platform = new Platform(createProperties("Windows", "5.1", "686")); + assertEquals(Platform.OperatingSystem.Windows, platform.getOS()); + assertEquals(Platform.Architecture.X86, platform.getArchitecture()); + } + + @Ignore("Known issue, needs resolve") + @Test + public void testCreateLinuxX86() { + Platform platform = new Platform(createProperties("Linux", "3.0.18", "x86")); + assertEquals(Platform.OperatingSystem.Linux, platform.getOS()); + assertEquals(Platform.Architecture.X86, platform.getArchitecture()); + } + + @Test + public void testCreateLinuxPPC() { + Platform platform = new Platform(createProperties("Linux", "2.6.11", "PPC")); + assertEquals(Platform.OperatingSystem.Linux, platform.getOS()); + assertEquals(Platform.Architecture.PPC, platform.getArchitecture()); + } + + @Test + public void testCreateSolarisSparc() { + Platform platform = new Platform(createProperties("SunOS", "6.0", "Sparc")); + assertEquals(Platform.OperatingSystem.Solaris, platform.getOS()); + assertEquals(Platform.Architecture.SPARC, platform.getArchitecture()); + } + + @Test + public void testCreateSolarisX86() { + Platform platform = new Platform(createProperties("Solaris", "5.0", "x86")); + assertEquals(Platform.OperatingSystem.Solaris, platform.getOS()); + assertEquals(Platform.Architecture.X86, platform.getArchitecture()); + } + + @Test + public void testCreateUnknownUnknown() { + Platform platform = new Platform(createProperties("Amiga OS", "5.0", "68k")); + assertEquals(Platform.OperatingSystem.Unknown, platform.getOS()); + assertEquals(Platform.Architecture.Unknown, platform.getArchitecture()); + } +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/SystemUtilTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/SystemUtilTest.java new file mode 100644 index 00000000..4405fd9e --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/SystemUtilTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +import org.junit.Ignore; + +/** + * SystemUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: SystemUtilTest.java,v 1.0 11.04.12 16:21 haraldk Exp$ + */ +@Ignore +public class SystemUtilTest { +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/lang/ValidateTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/lang/ValidateTest.java new file mode 100644 index 00000000..760d747b --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/lang/ValidateTest.java @@ -0,0 +1,1013 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +import org.junit.Test; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.junit.Assert.*; + +/** + * ValidateTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ValidateTest.java,v 1.0 11.04.12 09:06 haraldk Exp$ + */ +public class ValidateTest { + // Not null + + @Test + public void testNotNull() { + assertEquals("foo", Validate.notNull("foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotNullNull() { + Validate.notNull(null); + } + + @Test + public void testNotNullWithParameter() { + assertEquals("foo", Validate.notNull("foo", "bar")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotNullWithParameterNull() { + try { + Validate.notNull(null, "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test + public void testNotNullWithNullParameter() { + Validate.notNull("foo", null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotNullWithNullParameterNull() { + Validate.notNull(null, null); + } + + // Not empty (CharSequence) + + @Test + public void testNotEmptyCharSequence() { + assertEquals("foo", Validate.notEmpty("foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceNull() { + Validate.notEmpty((CharSequence) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceEmpty() { + Validate.notEmpty(""); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceOnlyWS() { + Validate.notEmpty(" \t\r"); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceNullWithParameter() { + try { + Validate.notEmpty((CharSequence) null, "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceEmptyWithParameter() { + try { + Validate.notEmpty("", "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceOnlyWSWithParameter() { + try { + Validate.notEmpty(" \t", "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test + public void testNotEmptyCharSequenceWithParameter() { + assertEquals("foo", Validate.notEmpty("foo", "bar")); + } + + @Test + public void testNotEmptyCharSequenceWithParameterNull() { + assertEquals("foo", Validate.notEmpty("foo", null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceNullWithParameterNull() { + try { + Validate.notEmpty((CharSequence) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceEmptyWithParameterNull() { + try { + Validate.notEmpty("", null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCharSequenceOnlyWSWithParameterNull() { + try { + Validate.notEmpty(" \t\t \n", null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + // Not empty (array) + + @Test + public void testNotEmptyArray() { + Integer[] array = new Integer[2]; + assertSame(array, Validate.notEmpty(array)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyArrayNull() { + Validate.notEmpty((Object[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyArrayEmpty() { + Validate.notEmpty(new String[0]); + } + + @Test + public void testNotEmptyArrayParameter() { + Integer[] array = new Integer[2]; + assertSame(array, Validate.notEmpty(array, "bar")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyArrayNullParameter() { + try { + Validate.notEmpty((Object[]) null, "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyArrayEmptyParameter() { + try { + Validate.notEmpty(new Float[0], "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test + public void testNotEmptyArrayWithParameterNull() { + Byte[] array = new Byte[1]; + assertSame(array, Validate.notEmpty(array, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyArrayNullWithParameterNull() { + try { + Validate.notEmpty((Object[]) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyArrayEmptyWithParameterNull() { + try { + Validate.notEmpty(new Object[0], null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + // Not empty (Collection) + + @Test + public void testNotEmptyCollection() { + Collection collection = Arrays.asList(new Integer[2]); + assertSame(collection, Validate.notEmpty(collection)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCollectionNull() { + Validate.notEmpty((Collection) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCollectionEmpty() { + Validate.notEmpty(Collections.emptySet()); + } + + @Test + public void testNotEmptyCollectionParameter() { + List collection = Collections.singletonList(1); + assertSame(collection, Validate.notEmpty(collection, "bar")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCollectionNullParameter() { + try { + Validate.notEmpty((Collection) null, "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCollectionEmptyParameter() { + try { + Validate.notEmpty(new ArrayList(), "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test + public void testNotEmptyCollectionWithParameterNull() { + Set collection = Collections.singleton((byte) 1); + assertSame(collection, Validate.notEmpty(collection, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCollectionNullWithParameterNull() { + try { + Validate.notEmpty((Collection) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyCollectionEmptyWithParameterNull() { + try { + Validate.notEmpty((Collection) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + // Not empty (Map) + + @Test + public void testNotEmptyMap() { + Map map = new HashMap() {{ + put(1, null); + put(2, null); + }}; + assertSame(map, Validate.notEmpty(map)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyMapNull() { + Validate.notEmpty((Map) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyMapEmpty() { + Validate.notEmpty(Collections.emptyMap()); + } + + @Test + public void testNotEmptyMapParameter() { + Map map = Collections.singletonMap(1, null); + assertSame(map, Validate.notEmpty(map, "bar")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyMapNullParameter() { + try { + Validate.notEmpty((Map) null, "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyMapEmptyParameter() { + try { + Validate.notEmpty(new HashMap(), "xyzzy"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("xyzzy")); + throw e; + } + } + + @Test + public void testNotEmptyMapWithParameterNull() { + Map map = Collections.singletonMap((byte) 1, null); + assertSame(map, Validate.notEmpty(map, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyMapNullWithParameterNull() { + try { + Validate.notEmpty((Map) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNotEmptyMapEmptyWithParameterNull() { + try { + Validate.notEmpty((Map) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("parameter")); + throw e; + } + } + + // No null elements (array) + + @Test + public void testNoNullElementsArray() { + String[] array = new String[] {"foo", "bar", "baz"}; + assertSame(array, Validate.noNullElements(array)); + } + + @Test + public void testNoNullElementsArrayEmpty() { + Object[] array = new Object[0]; + assertSame(array, Validate.noNullElements(array)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayNull() { + Validate.noNullElements((Object[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayNullElements() { + Validate.noNullElements(new Object[3]); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayMixed() { + String[] array = new String[] {"foo", null, "bar"}; + Validate.noNullElements(array); + } + + @Test + public void testNoNullElementsArrayParameter() { + String[] array = new String[] {"foo", "bar", "baz"}; + assertSame(array, Validate.noNullElements(array, "foo")); + } + + @Test + public void testNoNullElementsArrayEmptyParameter() { + Object[] array = new Object[0]; + assertSame(array, Validate.noNullElements(array, "foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayNullParameter() { + try { + Validate.noNullElements((Object[]) null, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayNullElementsParameter() { + try { + Validate.noNullElements(new Object[3], "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayMixedParameter() { + try { + Validate.noNullElements(new String[] {"foo", null, "bar"}, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test + public void testNoNullElementsArrayParameterNull() { + String[] array = new String[] {"foo", "bar", "baz"}; + assertSame(array, Validate.noNullElements(array, null)); + } + + @Test + public void testNoNullElementsArrayEmptyParameterNull() { + Object[] array = new Object[0]; + assertSame(array, Validate.noNullElements(array, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayNullParameterNull() { + try { + Validate.noNullElements((Object[]) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayNullElementsParameterNull() { + try { + Validate.noNullElements(new Object[3], null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsArrayMixedParameterNull() { + try { + Validate.noNullElements(new String[] {"foo", null, "bar"}, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + // No null elements (Collection) + + @Test + public void testNoNullElementsCollection() { + List collection = Arrays.asList("foo", "bar", "baz"); + assertSame(collection, Validate.noNullElements(collection)); + } + + @Test + public void testNoNullElementsCollectionEmpty() { + Set collection = Collections.emptySet(); + assertSame(collection, Validate.noNullElements(collection)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionNull() { + Validate.noNullElements((Collection) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionNullElements() { + Validate.noNullElements(Arrays.asList(null, null, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionMixed() { + Validate.noNullElements(Arrays.asList("foo", null, "bar")); + } + + @Test + public void testNoNullElementsCollectionParameter() { + List collection = Arrays.asList("foo", "bar", "baz"); + assertSame(collection, Validate.noNullElements(collection, "foo")); + } + + @Test + public void testNoNullElementsCollectionEmptyParameter() { + List collection = new CopyOnWriteArrayList(); + assertSame(collection, Validate.noNullElements(collection, "foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionNullParameter() { + try { + Validate.noNullElements((Set) null, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionNullElementsParameter() { + try { + Validate.noNullElements(Collections.singletonList(null), "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionMixedParameter() { + try { + Validate.noNullElements(Arrays.asList("foo", null, "bar"), "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test + public void testNoNullElementsCollectionParameterNull() { + List collection = Arrays.asList("foo", "bar", "baz"); + assertSame(collection, Validate.noNullElements(collection, null)); + } + + @Test + public void testNoNullElementsCollectionEmptyParameterNull() { + Collection collection = Collections.emptySet(); + assertSame(collection, Validate.noNullElements(collection, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionNullParameterNull() { + try { + Validate.noNullElements((ArrayList) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionNullElementsParameterNull() { + try { + Validate.noNullElements(Collections.singleton(null), null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullElementsCollectionMixedParameterNull() { + Collection collection = Arrays.asList("foo", null, "bar"); + try { + Validate.noNullElements(collection, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + // No null values (Map) + + @Test + public void testNoNullValuesMap() { + Map map = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + assertSame(map, Validate.noNullValues(map)); + } + + @Test + public void testNoNullValuesEmpty() { + Map map = Collections.emptyMap(); + assertSame(map, Validate.noNullValues(map)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesNull() { + Validate.noNullValues((Map) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesNullElements() { + Validate.noNullValues(Collections.singletonMap("foo", null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesMixed() { + Validate.noNullValues(new HashMap() {{ + put("foo", 1); + put(null, null); + put("baz", null); + }}); + } + + @Test + public void testNoNullValuesParameter() { + Map map = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + assertSame(map, Validate.noNullValues(map, "foo")); + } + + @Test + public void testNoNullValuesEmptyParameter() { + Map map = new HashMap(); + assertSame(map, Validate.noNullValues(map, "foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesNullParameter() { + try { + Validate.noNullValues((Map) null, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesNullElementsParameter() { + try { + Validate.noNullValues(Collections.singletonMap("bar", null), "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesMixedParameter() { + try { + Validate.noNullValues(new HashMap() {{ + put("foo", 1); + put(null, null); + put("bar", null); + }}, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test + public void testNoNullValuesParameterNull() { + Map map = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + assertSame(map, Validate.noNullValues(map, null)); + } + + @Test + public void testNoNullValuesEmptyParameterNull() { + Map map = Collections.emptyMap(); + assertSame(map, Validate.noNullValues(map, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesNullParameterNull() { + try { + Validate.noNullValues((Map) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesNullElementsParameterNull() { + try { + Validate.noNullValues(Collections.singletonMap(null, null), null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullValuesMixedParameterNull() { + + try { + Validate.noNullValues(new HashMap() {{ + put("foo", 1); + put(null, null); + put("bar", null); + }}, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + // No null keys (Map) + + @Test + public void testNoNullKeysMap() { + Map map = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + assertSame(map, Validate.noNullKeys(map)); + } + + @Test + public void testNoNullKeysEmpty() { + Map map = Collections.emptyMap(); + assertSame(map, Validate.noNullKeys(map)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysNull() { + Validate.noNullKeys((Map) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysNullElements() { + Validate.noNullKeys(Collections.singletonMap(null, "foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysMixed() { + Validate.noNullKeys(new HashMap() {{ + put("foo", 1); + put(null, null); + put("baz", null); + }}); + } + + @Test + public void testNoNullKeysParameter() { + Map map = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + assertSame(map, Validate.noNullKeys(map, "foo")); + } + + @Test + public void testNoNullKeysEmptyParameter() { + Map map = new HashMap(); + assertSame(map, Validate.noNullKeys(map, "foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysNullParameter() { + try { + Validate.noNullKeys((Map) null, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysNullElementsParameter() { + try { + Validate.noNullKeys(Collections.singletonMap(null, "bar"), "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysMixedParameter() { + try { + Validate.noNullKeys(new HashMap() {{ + put("foo", 1); + put(null, null); + put("bar", null); + }}, "foo"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test + public void testNoNullKeysParameterNull() { + Map map = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + assertSame(map, Validate.noNullKeys(map, null)); + } + + @Test + public void testNoNullKeysEmptyParameterNull() { + Map map = Collections.emptyMap(); + assertSame(map, Validate.noNullKeys(map, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysNullParameterNull() { + try { + Validate.noNullKeys((Map) null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysNullElementsParameterNull() { + try { + Validate.noNullKeys(Collections.singletonMap(null, null), null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNullKeysMixedParameterNull() { + + try { + Validate.noNullKeys(new HashMap() {{ + put("foo", 1); + put(null, null); + put("bar", null); + }}, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("method parameter")); + throw e; + } + } + + // Is true + + @Test + public void testIsTrue() { + assertTrue(Validate.isTrue(true, "%s")); + } + + @Test(expected = IllegalArgumentException.class) + public void testIsTrueFalse() { + try { + Validate.isTrue(false, "is %s"); + } + catch (IllegalArgumentException e) { + assertEquals("is false", e.getMessage()); + throw e; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testIsTrueFalseNullParam() { + try { + Validate.isTrue(false, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("false")); + throw e; + } + } + + @Test + public void testIsTrueValue() { + Object object = new Object(); + assertSame(object, Validate.isTrue(true, object, "%s")); + } + + @Test(expected = IllegalArgumentException.class) + public void testIsTrueFalseValue() { + try { + Validate.isTrue(false, "baz", "foo is '%s'"); + } + catch (IllegalArgumentException e) { + assertEquals("foo is 'baz'", e.getMessage()); + throw e; + } + } + + @Test + public void testIsTrueValueParamNull() { + assertEquals("foo", Validate.isTrue(true, "foo", null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testIsTrueFalseValueParamNull() { + try { + Validate.isTrue(false, "foo", null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("foo")); + throw e; + } + } + + @Test + public void testIsTrueValueNullParamNull() { + assertNull(Validate.isTrue(true, null, null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testIsTrueFalseValueNullParamNull() { + try { + Validate.isTrue(false, null, null); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("null")); + throw e; + } + } + +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/CollectionUtilTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/CollectionUtilTest.java new file mode 100644 index 00000000..948cdbc2 --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/CollectionUtilTest.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util; + +import org.junit.Ignore; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +/** + * CollectionUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CollectionUtilTest.java,v 1.0 24.01.12 17:39 haraldk Exp$ + */ +public class CollectionUtilTest { + + private static final Object[] stringObjects = new Object[] {"foo", "bar", "baz"}; + private static final Object[] integerObjects = new Object[] {1, 2, 3}; + + @Test + public void testMergeArraysObject() { + Object[] merged = (Object[]) CollectionUtil.mergeArrays(stringObjects, integerObjects); + assertArrayEquals(new Object[] {"foo", "bar", "baz", 1, 2, 3}, merged); + } + + @Test + public void testMergeArraysObjectOffset() { + Object[] merged = (Object[]) CollectionUtil.mergeArrays(stringObjects, 1, 2, integerObjects, 2, 1); + assertArrayEquals(new Object[] {"bar", "baz", 3}, merged); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectBadOffset() { + CollectionUtil.mergeArrays(stringObjects, 4, 2, integerObjects, 2, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectBadSecondOffset() { + CollectionUtil.mergeArrays(stringObjects, 1, 2, integerObjects, 4, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectBadLength() { + CollectionUtil.mergeArrays(stringObjects, 1, 4, integerObjects, 2, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectBadSecondLength() { + CollectionUtil.mergeArrays(stringObjects, 1, 2, integerObjects, 2, 2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectNegativeOffset() { + CollectionUtil.mergeArrays(stringObjects, -1, 2, integerObjects, 2, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectNegativeSecondOffset() { + CollectionUtil.mergeArrays(stringObjects, 1, 2, integerObjects, -1, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectNegativeLength() { + CollectionUtil.mergeArrays(stringObjects, 1, -1, integerObjects, 2, 1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testMergeArraysObjectNegativeSecondLength() { + CollectionUtil.mergeArrays(stringObjects, 1, 2, integerObjects, 2, -1); + } + + @Test + public void testMergeArraysObjectAssignable() { + Integer[] integers = {1, 2, 3}; // Integer assignable to Object + + Object[] merged = (Object[]) CollectionUtil.mergeArrays(stringObjects, integers); + assertArrayEquals(new Object[] {"foo", "bar", "baz", 1, 2, 3}, merged); + } + + @Test(expected = ArrayStoreException.class) + public void testMergeArraysObjectIllegalType() { + String[] strings = {"foo", "bar", "baz"}; + Integer[] integers = {1, 2, 3}; // Integer not assignable to String + + CollectionUtil.mergeArrays(strings, integers); + } + + @Test(expected = ArrayStoreException.class) + public void testMergeArraysNativeIllegalType() { + char[] chars = {'a', 'b', 'c'}; + int[] integers = {1, 2, 3}; // Integer not assignable to String + + CollectionUtil.mergeArrays(chars, integers); + + } + + @Test + public void testMergeArraysNative() { + char[] chars = {'a', 'b', 'c'}; + char[] more = {'x', 'y', 'z'}; + + char[] merged = (char[]) CollectionUtil.mergeArrays(chars, more); + assertArrayEquals(new char[] {'a', 'b', 'c', 'x', 'y', 'z'}, merged); + } + + @Test + public void testSubArrayObject() { + String[] strings = CollectionUtil.subArray(new String[] {"foo", "bar", "baz", "xyzzy"}, 1, 2); + assertArrayEquals(new String[] {"bar", "baz"}, strings); + } + + @Test + public void testSubArrayNative() { + int[] numbers = (int[]) CollectionUtil.subArray(new int[] {1, 2, 3, 4, 5}, 1, 3); + assertArrayEquals(new int[] {2, 3, 4}, numbers); + } + + @Test(expected = IllegalArgumentException.class) + public void testEnumIteratorNull() { + CollectionUtil.iterator((Enumeration) null); + } + + @Test + public void testEnumIterator() { + @SuppressWarnings("unchecked") + Iterator iterator = CollectionUtil.iterator((Enumeration) new StringTokenizer("foo, bar, baz", ", ")); + + int count = 0; + for (Object stringObject : stringObjects) { + assertTrue(iterator.hasNext()); + assertEquals(stringObject, iterator.next()); + + try { + iterator.remove(); + fail("Enumeration has no remove method, iterator.remove() must throw exception"); + } + catch (UnsupportedOperationException expected) { + } + + count++; + } + + assertEquals(3, count); + assertFalse(iterator.hasNext()); + + try { + iterator.next(); + fail("Iterator has more elements than enumeration"); + } + catch (NoSuchElementException expected) { + } + } + + @Test(expected = IllegalArgumentException.class) + public void testArrayIteratorNull() { + CollectionUtil.iterator((Object[]) null); + } + + @Test + public void testArrayIterator() { + Iterator iterator = CollectionUtil.iterator(new String[] {"foo", "bar", "baz"}); + + int count = 0; + for (Object stringObject : stringObjects) { + assertTrue(iterator.hasNext()); + assertEquals(stringObject, iterator.next()); + + try { + iterator.remove(); + fail("Array have fixed length, iterator.remove() must throw exception"); + } + catch (UnsupportedOperationException expected) { + } + + count++; + } + + assertEquals(3, count); + assertFalse(iterator.hasNext()); + + try { + iterator.next(); + fail("Iterator has more elements than array"); + } + catch (NoSuchElementException expected) { + } + } + + @Test + public void testArrayListIterator() { + assertCorrectListIterator(CollectionUtil.iterator(new String[] {"foo", "bar", "baz"}), stringObjects); + } + + @Test + public void testArrayListIteratorRange() { + assertCorrectListIterator(CollectionUtil.iterator(new String[] {"foo", "bar", "baz", "boo"}, 1, 2), new String[] {"bar", "baz"}); + } + + @Test + public void testArrayListIteratorSanityCheckArraysAsList() { + assertCorrectListIterator(Arrays.asList(new String[] {"foo", "bar", "baz"}).listIterator(), stringObjects); + } + + @Test + public void testArrayListIteratorSanityCheckArraysAsListRange() { + // NOTE: sublist(fromInc, toExcl) vs iterator(start, length) + assertCorrectListIterator(Arrays.asList(new String[] {"foo", "bar", "baz", "boo"}).subList(1, 3).listIterator(0), new String[] {"bar", "baz"}, false, true); + } + + @Test + public void testArrayListIteratorSanityCheckArraysList() { + assertCorrectListIterator(new ArrayList(Arrays.asList(new String[] {"foo", "bar", "baz"})).listIterator(), stringObjects, true, true); + } + + @Test + public void testArrayListIteratorSanityCheckArraysListRange() { + // NOTE: sublist(fromInc, toExcl) vs iterator(start, length) + assertCorrectListIterator(new ArrayList(Arrays.asList(new String[] {"foo", "bar", "baz", "boo"})).subList(1, 3).listIterator(0), new String[] {"bar", "baz"}, true, true); + } + + private void assertCorrectListIterator(ListIterator iterator, final Object[] elements) { + assertCorrectListIterator(iterator, elements, false, false); + } + + // NOTE: The test is can only test list iterators with a starting index == 0 + private void assertCorrectListIterator(ListIterator iterator, final Object[] elements, boolean skipRemove, boolean skipAdd) { + // Index is now "before 0" + assertEquals(-1, iterator.previousIndex()); + assertEquals(0, iterator.nextIndex()); + + int count = 0; + for (Object element : elements) { + assertTrue("No next element for element '" + element + "' at index: " + count, iterator.hasNext()); + assertEquals(count > 0, iterator.hasPrevious()); + assertEquals(count, iterator.nextIndex()); + assertEquals(count - 1, iterator.previousIndex()); + assertEquals(element, iterator.next()); + + count++; + + if (!skipRemove) { + try { + iterator.remove(); + fail("Array has fixed length, iterator.remove() must throw exception"); + } + catch (UnsupportedOperationException expected) { + } + + // Verify cursor not moving + assertEquals(count, iterator.nextIndex()); + assertEquals(count - 1, iterator.previousIndex()); + } + + // NOTE: AbstractlList.ListItr.add() moves cursor forward, even if backing list's add() throws Exception + // (coll) AbstractList.ListItr.add might corrupt iterator state if enclosing add throws + // See: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6533203, fixed in Java 7 + if (!skipAdd && !("java.util.AbstractList$ListItr".equals(iterator.getClass().getName()) /* && isJava7OrLater() */)) { + try { + iterator.add("xyzzy"); + fail("Array has fixed length, iterator.add() must throw exception"); + } + catch (UnsupportedOperationException expected) { + } + + // Verify cursor not moving + assertEquals(count, iterator.nextIndex()); + assertEquals(count - 1, iterator.previousIndex()); + } + + // Set is supported + iterator.set(String.valueOf(count)); + } + + assertEquals(elements.length, count); + assertFalse(iterator.hasNext()); + + try { + iterator.next(); + fail("Iterator has more elements than array"); + } + catch (NoSuchElementException expected) { + } + + // Index should now be "before last" + assertEquals(elements.length - 1, iterator.previousIndex()); + assertEquals(elements.length, iterator.nextIndex()); + + for (int i = count; i > 0; i--) { + assertTrue("No previous element for element '" + elements[i - 1] + "' at index: " + (i - 1), iterator.hasPrevious()); + assertEquals(i < elements.length, iterator.hasNext()); + assertEquals(i - 1, iterator.previousIndex()); + assertEquals(i, iterator.nextIndex()); + + assertEquals(String.valueOf(i), iterator.previous()); + } + + // Index should now be back "before 0" + assertEquals(-1, iterator.previousIndex()); + assertEquals(0, iterator.nextIndex()); + assertFalse(iterator.hasPrevious()); + + try { + iterator.previous(); + fail("Iterator has more elements than array"); + } + catch (NoSuchElementException expected) { + } + } + + @Test(expected = IllegalArgumentException.class) + public void testArrayIteratorRangeNull() { + CollectionUtil.iterator(null, 0, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testArrayIteratorRangeBadStart() { + CollectionUtil.iterator(stringObjects, stringObjects.length + 1, 2); + } + @Test(expected = IllegalArgumentException.class) + public void testArrayIteratorRangeBadLength() { + CollectionUtil.iterator(stringObjects, 1, stringObjects.length); + } + + @Test + public void testArrayIteratorRange() { + Iterator iterator = CollectionUtil.iterator(new String[] {"foo", "bar", "baz", "xyzzy"}, 1, 2); + + for (int i = 1; i < 3; i++) { + assertTrue(iterator.hasNext()); + assertEquals(stringObjects[i], iterator.next()); + + try { + iterator.remove(); + fail("Array has no remove method, iterator.remove() must throw exception"); + } + catch (UnsupportedOperationException expected) { + } + } + + assertFalse(iterator.hasNext()); + + try { + iterator.next(); + fail("Iterator has more elements than array range"); + } + catch (NoSuchElementException expected) { + } + } + + @Test(expected = IllegalArgumentException.class) + public void testReverseOrderNull() { + CollectionUtil.reverseOrder(null); + } + + @Test + public void testReverseOrder() { + Comparator naturalOrder = new NaturalOrder(); + Comparator reverse = CollectionUtil.reverseOrder(naturalOrder); + + assertNotNull(reverse); + + assertEquals(0, naturalOrder.compare("foo", "foo")); + assertEquals(0, reverse.compare("foo", "foo")); + + assertTrue(naturalOrder.compare("bar", "baz") < 0); + assertTrue(reverse.compare("bar", "baz") > 0); + + assertTrue(naturalOrder.compare("baz", "bar") > 0); + assertTrue(reverse.compare("baz", "bar") < 0); + } + + @Test + public void testReverseOrderRandomIntegers() { + Comparator naturalOrder = new NaturalOrder(); + Comparator reverse = CollectionUtil.reverseOrder(naturalOrder); + + Random random = new Random(243249878l); // Stable "random" sequence + + for (int i = 0; i < 65536; i++) { + // Verified to be ~ 50/50 lt/gt + int integer = random.nextInt(); + int integerToo = random.nextInt(); + + assertEquals(0, reverse.compare(integer, integer)); + assertEquals(0, reverse.compare(integerToo, integerToo)); + + int natural = naturalOrder.compare(integer, integerToo); + + if (natural == 0) { + // Actually never hits, but eq case is tested above + assertEquals(0, reverse.compare(integer, integerToo)); + } + else if (natural < 0) { + assertTrue(reverse.compare(integer, integerToo) > 0); + } + else { + assertTrue(reverse.compare(integer, integerToo) < 0); + } + } + } + + @Ignore("For development only") + @Test + @SuppressWarnings({"UnusedDeclaration"}) + public void testGenerify() { + List list = Collections.singletonList("foo"); + @SuppressWarnings({"unchecked"}) + Set set = new HashSet(list); + + List strs0 = CollectionUtil.generify(list, String.class); + List objs0 = CollectionUtil.generify(list, String.class); +// List strs01 = CollectionUtil.generify(list, Object.class); // Not okay + try { + List strs1 = CollectionUtil.generify(set, String.class); // Not ok, runtime CCE unless set is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + + try { + ArrayList strs01 = CollectionUtil.generify(list, String.class); // Not ok, runtime CCE unless list is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + + Set setstr1 = CollectionUtil.generify(set, String.class); + Set setobj1 = CollectionUtil.generify(set, String.class); + try { + Set setobj44 = CollectionUtil.generify(list, String.class); // Not ok, runtime CCE unless list is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + + List strs2 = CollectionUtil., String>generify2(list); + List objs2 = CollectionUtil., String>generify2(list); +// List morestrs = CollectionUtil., String>generify2(list); // Not ok + try { + List strs3 = CollectionUtil., String>generify2(set); // Not ok, runtime CCE unless set is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + } + + private static class NaturalOrder> implements Comparator { + public int compare(T left, T right) { + return left.compareTo(right); + } + } +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java index 8f13b97b..91d0f564 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java @@ -34,17 +34,16 @@ public class LRUMapTestCase extends LinkedMapTestCase { //----------------------------------------------------------------------- public Map makeEmptyMap() { - LRUMap map = new LRUMap(); - return map; + return new LRUMap(); } //----------------------------------------------------------------------- public void testRemoveLRU() { - LRUMap map2 = new LRUMap(3); - map2.put(new Integer(1),"foo"); - map2.put(new Integer(2),"foo"); - map2.put(new Integer(3),"foo"); - map2.put(new Integer(4),"foo"); // removes 1 since max size exceeded + LRUMap map2 = new LRUMap(3); + map2.put(1,"foo"); + map2.put(2,"foo"); + map2.put(3,"foo"); + map2.put(4,"foo"); // removes 1 since max size exceeded map2.removeLRU(); // should be Integer(2) assertTrue("Second to last value should exist",map2.get(new Integer(3)).equals("foo")); @@ -52,11 +51,11 @@ public class LRUMapTestCase extends LinkedMapTestCase { } public void testMultiplePuts() { - LRUMap map2 = new LRUMap(2); - map2.put(new Integer(1),"foo"); - map2.put(new Integer(2),"bar"); - map2.put(new Integer(3),"foo"); - map2.put(new Integer(4),"bar"); + LRUMap map2 = new LRUMap(2); + map2.put(1,"foo"); + map2.put(2,"bar"); + map2.put(3,"foo"); + map2.put(4,"bar"); assertTrue("last value should exist",map2.get(new Integer(4)).equals("bar")); assertTrue("LRU should not exist", map2.get(new Integer(1)) == null); @@ -67,13 +66,13 @@ public class LRUMapTestCase extends LinkedMapTestCase { * to exceed its maxiumum size. */ public void testPutAll() { - LRUMap map2 = new LRUMap(3); - map2.put(new Integer(1),"foo"); - map2.put(new Integer(2),"foo"); - map2.put(new Integer(3),"foo"); + LRUMap map2 = new LRUMap(3); + map2.put(1,"foo"); + map2.put(2,"foo"); + map2.put(3,"foo"); - HashMap hashMap = new HashMap(); - hashMap.put(new Integer(4),"foo"); + HashMap hashMap = new HashMap(); + hashMap.put(4,"foo"); map2.putAll(hashMap); @@ -88,7 +87,7 @@ public class LRUMapTestCase extends LinkedMapTestCase { * when setMaximumSize(int) is called */ public void testSetMaximumSize() { - LRUMap map = new LRUMap(6); + LRUMap map = new LRUMap(6); map.put("1","1"); map.put("2","2"); map.put("3","3"); @@ -102,7 +101,7 @@ public class LRUMapTestCase extends LinkedMapTestCase { } public void testGetPromotion() { - LRUMap map = new LRUMap(3); + LRUMap map = new LRUMap(3); map.put("1","1"); map.put("2","2"); map.put("3","3"); @@ -116,7 +115,7 @@ public class LRUMapTestCase extends LinkedMapTestCase { // 2 should be evicted (then 3,1,4) map.put("4","4"); - Iterator keyIterator = map.keySet().iterator(); + Iterator keyIterator = map.keySet().iterator(); Object[] keys = new Object[3]; for (int i = 0; keyIterator.hasNext() ; ++i) { keys[i] = keyIterator.next(); @@ -134,7 +133,7 @@ public class LRUMapTestCase extends LinkedMapTestCase { * by the LRU algorithm (the removeLRU() method). */ public void testLRUSubclass() { - LRUCounter counter = new LRUCounter(3); + LRUCounter counter = new LRUCounter(3); // oldest <--> newest // 1 counter.put("1","foo"); @@ -165,9 +164,9 @@ public class LRUMapTestCase extends LinkedMapTestCase { //assertTrue("newest key is '2'",counter.get(1).equals("2")); } - private class LRUCounter extends LRUMap { + private class LRUCounter extends LRUMap { int removedCount = 0; - List list = new ArrayList(3); + List list = new ArrayList(3); LRUCounter(int i) { super(i); diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java index d0acb17b..3b54c3cb 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java @@ -20,20 +20,20 @@ import java.util.*; /** * Abstract test class for {@link java.util.Map} methods and contracts. - *

    + *

    * The forces at work here are similar to those in {@link CollectionAbstractTestCase}. * If your class implements the full Map interface, including optional * operations, simply extend this class, and implement the * {@link #makeEmptyMap()} method. - *

    + *

    * On the other hand, if your map implementation is weird, you may have to * override one or more of the other protected methods. They're described * below. - *

    + *

    * Entry Population Methods - *

    + *

    * Override these methods if your map requires special entries: - * + *

    *

      *
    • {@link #getSampleKeys()} *
    • {@link #getSampleValues()} @@ -41,11 +41,11 @@ import java.util.*; *
    • {@link #getOtherKeys()} *
    • {@link #getOtherValues()} *
    - * + *

    * Supported Operation Methods - *

    + *

    * Override these methods if your map doesn't support certain operations: - * + *

    *

      *
    • {@link #isPutAddSupported()} *
    • {@link #isPutChangeSupported()} @@ -56,9 +56,9 @@ import java.util.*; *
    • {@link #isAllowNullKey()} *
    • {@link #isAllowNullValue()} *
    - * + *

    * Fixture Methods - *

    + *

    * For tests on modification operations (puts and removes), fixtures are used * to verify that that operation results in correct state for the map and its * collection views. Basically, the modification is performed against your @@ -69,20 +69,20 @@ import java.util.*; * on both your map implementation and the confirmed map implementation, the * two maps are compared to see if their state is identical. The comparison * also compares the collection views to make sure they're still the same.

    - * + *

    * The upshot of all that is that any test that modifies the map in * any way will verify that all of the map's state is still * correct, including the state of its collection views. So for instance * if a key is removed by the map's key set's iterator, then the entry set * is checked to make sure the key/value pair no longer appears.

    - * + *

    * The {@link #map} field holds an instance of your collection implementation. * The {@link #entrySet}, {@link #keySet} and {@link #values} fields hold * that map's collection views. And the {@link #confirmed} field holds * an instance of the confirmed collection implementation. The * {@link #resetEmpty()} and {@link #resetFull()} methods set these fields to * empty or full maps, so that tests can proceed from a known state.

    - * + *

    * After a modification operation to both {@link #map} and {@link #confirmed}, * the {@link #verifyAll()} method is invoked to compare the results. The * {@link # verify0} method calls separate methods to verify the map and its three @@ -92,9 +92,9 @@ import java.util.*; * instance, TestDoubleOrderedMap would want override its * {@link #verifyValues()} method to verify that the values are unique and in * ascending order.

    - * + *

    * Other Notes - *

    + *

    * If your {@link Map} fails one of these tests by design, you may still use * this base set of cases. Simply override the test case (method) your map * fails and/or the methods that define the assumptions used by the test @@ -153,7 +153,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * {@link #makeEmptyMap()} and {@link #makeFullMap()} * support the {@code put} and {@code putAll} operations * adding new mappings. - *

    + *

    * Default implementation returns true. * Override if your collection class does not support put adding. */ @@ -166,7 +166,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * {@link #makeEmptyMap()} and {@link #makeFullMap()} * support the {@code put} and {@code putAll} operations * changing existing mappings. - *

    + *

    * Default implementation returns true. * Override if your collection class does not support put changing. */ @@ -178,7 +178,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * Returns true if the maps produced by * {@link #makeEmptyMap()} and {@link #makeFullMap()} * support the {@code setValue} operation on entrySet entries. - *

    + *

    * Default implementation returns isPutChangeSupported(). * Override if your collection class does not support setValue but does * support put changing. @@ -191,7 +191,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * Returns true if the maps produced by * {@link #makeEmptyMap()} and {@link #makeFullMap()} * support the {@code remove} and {@code clear} operations. - *

    + *

    * Default implementation returns true. * Override if your collection class does not support removal operations. */ @@ -203,7 +203,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * Returns true if the maps produced by * {@link #makeEmptyMap()} and {@link #makeFullMap()} * can cause structural modification on a get(). The example is LRUMap. - *

    + *

    * Default implementation returns false. * Override if your map class structurally modifies on get. */ @@ -226,7 +226,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * Returns true if the maps produced by * {@link #makeEmptyMap()} and {@link #makeFullMap()} * supports null keys. - *

    + *

    * Default implementation returns true. * Override if your collection class does not support null keys. */ @@ -238,7 +238,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * Returns true if the maps produced by * {@link #makeEmptyMap()} and {@link #makeFullMap()} * supports null values. - *

    + *

    * Default implementation returns true. * Override if your collection class does not support null values. */ @@ -250,7 +250,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * Returns true if the maps produced by * {@link #makeEmptyMap()} and {@link #makeFullMap()} * supports duplicate values. - *

    + *

    * Default implementation returns true. * Override if your collection class does not support duplicate values. */ @@ -259,24 +259,23 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } /** - * Returns the set of keys in the mappings used to test the map. This - * method must return an array with the same length as {@link - * #getSampleValues()} and all array elements must be different. The - * default implementation constructs a set of String keys, and includes a - * single null key if {@link #isAllowNullKey()} returns {@code true}. + * Returns the set of keys in the mappings used to test the map. This + * method must return an array with the same length as {@link + * #getSampleValues()} and all array elements must be different. The + * default implementation constructs a set of String keys, and includes a + * single null key if {@link #isAllowNullKey()} returns {@code true}. */ public Object[] getSampleKeys() { Object[] result = new Object[] { - "blah", "foo", "bar", "baz", "tmp", "gosh", "golly", "gee", - "hello", "goodbye", "we'll", "see", "you", "all", "again", - "key", - "key2", - (isAllowNullKey() && !JDK12) ? null : "nonnullkey" + "blah", "foo", "bar", "baz", "tmp", "gosh", "golly", "gee", + "hello", "goodbye", "we'll", "see", "you", "all", "again", + "key", + "key2", + (isAllowNullKey() && !JDK12) ? null : "nonnullkey" }; return result; } - public Object[] getOtherKeys() { return getOtherNonNullStringElements(); } @@ -288,15 +287,15 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { /** * Returns a list of string elements suitable for return by * {@link #getOtherKeys()} or {@link #getOtherValues}. - * + *

    *

    Override getOtherElements to returnthe results of this method if your * collection does not support heterogenous elements or the null element. *

    */ public Object[] getOtherNonNullStringElements() { return new Object[] { - "For","then","despite",/* of */"space","I","would","be","brought", - "From","limits","far","remote","where","thou","dost","stay" + "For", "then", "despite",/* of */"space", "I", "would", "be", "brought", + "From", "limits", "far", "remote", "where", "thou", "dost", "stay" }; } @@ -311,11 +310,11 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public Object[] getSampleValues() { Object[] result = new Object[] { - "blahv", "foov", "barv", "bazv", "tmpv", "goshv", "gollyv", "geev", - "hellov", "goodbyev", "we'llv", "seev", "youv", "allv", "againv", - (isAllowNullValue() && !JDK12) ? null : "nonnullvalue", - "value", - (isAllowDuplicateValues()) ? "value" : "value2", + "blahv", "foov", "barv", "bazv", "tmpv", "goshv", "gollyv", "geev", + "hellov", "goodbyev", "we'llv", "seev", "youv", "allv", "againv", + (isAllowNullValue() && !JDK12) ? null : "nonnullvalue", + "value", + (isAllowDuplicateValues()) ? "value" : "value2", }; return result; } @@ -333,18 +332,18 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public Object[] getNewSampleValues() { Object[] result = new Object[] { - (isAllowNullValue() && !JDK12 && isAllowDuplicateValues()) ? null : "newnonnullvalue", - "newvalue", - (isAllowDuplicateValues()) ? "newvalue" : "newvalue2", - "newblahv", "newfoov", "newbarv", "newbazv", "newtmpv", "newgoshv", - "newgollyv", "newgeev", "newhellov", "newgoodbyev", "newwe'llv", - "newseev", "newyouv", "newallv", "newagainv", + (isAllowNullValue() && !JDK12 && isAllowDuplicateValues()) ? null : "newnonnullvalue", + "newvalue", + (isAllowDuplicateValues()) ? "newvalue" : "newvalue2", + "newblahv", "newfoov", "newbarv", "newbazv", "newtmpv", "newgoshv", + "newgollyv", "newgeev", "newhellov", "newgoodbyev", "newwe'llv", + "newseev", "newyouv", "newallv", "newagainv", }; return result; } /** - * Helper method to add all the mappings described by + * Helper method to add all the mappings described by * {@link #getSampleKeys()} and {@link #getSampleValues()}. */ public void addSampleMappings(Map m) { @@ -352,30 +351,28 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Object[] keys = getSampleKeys(); Object[] values = getSampleValues(); - for(int i = 0; i < keys.length; i++) { + for (int i = 0; i < keys.length; i++) { try { m.put(keys[i], values[i]); - } catch (NullPointerException exception) { - assertTrue("NullPointerException only allowed to be thrown " + - "if either the key or value is null.", - keys[i] == null || values[i] == null); + } + catch (NullPointerException exception) { + assertTrue("NullPointerException only allowed to be thrown if either the key or value is null.", + keys[i] == null || values[i] == null); - assertTrue("NullPointerException on null key, but " + - "isAllowNullKey is not overridden to return false.", - keys[i] == null || !isAllowNullKey()); + assertTrue("NullPointerException on null key, but isAllowNullKey is not overridden to return false.", + keys[i] == null || !isAllowNullKey()); - assertTrue("NullPointerException on null value, but " + - "isAllowNullValue is not overridden to return false.", - values[i] == null || !isAllowNullValue()); + assertTrue("NullPointerException on null value, but isAllowNullValue is not overridden to return false.", + values[i] == null || !isAllowNullValue()); assertTrue("Unknown reason for NullPointer.", false); } } - assertEquals("size must reflect number of mappings added.", - keys.length, m.size()); + assertEquals("size must reflect number of mappings added.", keys.length, m.size()); } //----------------------------------------------------------------------- + /** * Return a new, empty {@link Map} to be used for testing. * @@ -431,7 +428,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { public String getCompatibilityVersion() { return super.getCompatibilityVersion(); } + //----------------------------------------------------------------------- + /** * Test to ensure the test setup is working properly. This method checks * to ensure that the getSampleKeys and getSampleValues methods are @@ -443,45 +442,32 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * isAllowDuplicateValues() returns true. */ public void testSampleMappings() { - Object[] keys = getSampleKeys(); - Object[] values = getSampleValues(); - Object[] newValues = getNewSampleValues(); + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + Object[] newValues = getNewSampleValues(); - assertTrue("failure in test: Must have keys returned from " + - "getSampleKeys.", keys != null); + assertTrue("failure in test: Must have keys returned from getSampleKeys.", keys != null); + assertTrue("failure in test: Must have values returned from getSampleValues.", values != null); - assertTrue("failure in test: Must have values returned from " + - "getSampleValues.", values != null); + // verify keys and values have equivalent lengths (in case getSampleX are + // overridden) + assertEquals("failure in test: not the same number of sample keys and values.", keys.length, values.length); + assertEquals("failure in test: not the same number of values and new values.", values.length, newValues.length); - // verify keys and values have equivalent lengths (in case getSampleX are - // overridden) - assertEquals("failure in test: not the same number of sample " + - "keys and values.", keys.length, values.length); + // verify there aren't duplicate keys, and check values + for (int i = 0; i < keys.length - 1; i++) { + for (int j = i + 1; j < keys.length; j++) { + assertTrue("failure in test: duplicate null keys.", (keys[i] != null || keys[j] != null)); + assertTrue("failure in test: duplicate non-null key.", + (keys[i] == null || keys[j] == null || (!keys[i].equals(keys[j]) && !keys[j].equals(keys[i])))); + } - - assertEquals("failure in test: not the same number of values and new values.", - values.length, newValues.length); - - // verify there aren't duplicate keys, and check values - for(int i = 0; i < keys.length - 1; i++) { - for(int j = i + 1; j < keys.length; j++) { - assertTrue("failure in test: duplicate null keys.", - (keys[i] != null || keys[j] != null)); - assertTrue("failure in test: duplicate non-null key.", - (keys[i] == null || keys[j] == null || - (!keys[i].equals(keys[j]) && - !keys[j].equals(keys[i])))); - } - assertTrue("failure in test: found null key, but isNullKeySupported " + - "is false.", keys[i] != null || isAllowNullKey()); - assertTrue("failure in test: found null value, but isNullValueSupported " + - "is false.", values[i] != null || isAllowNullValue()); - assertTrue("failure in test: found null new value, but isNullValueSupported " + - "is false.", newValues[i] != null || isAllowNullValue()); - assertTrue("failure in test: values should not be the same as new value", - values[i] != newValues[i] && - (values[i] == null || !values[i].equals(newValues[i]))); - } + assertTrue("failure in test: found null key, but isNullKeySupported is false.", keys[i] != null || isAllowNullKey()); + assertTrue("failure in test: found null value, but isNullValueSupported is false.", values[i] != null || isAllowNullValue()); + assertTrue("failure in test: found null new value, but isNullValueSupported is false.", newValues[i] != null || isAllowNullValue()); + assertTrue("failure in test: values should not be the same as new value", + values[i] != newValues[i] && (values[i] == null || !values[i].equals(newValues[i]))); + } } // tests begin here. Each test adds a little bit of tested functionality. @@ -495,26 +481,18 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public void testMakeMap() { Map em = makeEmptyMap(); - assertTrue("failure in test: makeEmptyMap must return a non-null map.", - em != null); + assertTrue("failure in test: makeEmptyMap must return a non-null map.", em != null); Map em2 = makeEmptyMap(); - assertTrue("failure in test: makeEmptyMap must return a non-null map.", - em != null); - - assertTrue("failure in test: makeEmptyMap must return a new map " + - "with each invocation.", em != em2); + assertTrue("failure in test: makeEmptyMap must return a non-null map.", em2 != null); + assertTrue("failure in test: makeEmptyMap must return a new map with each invocation.", em != em2); Map fm = makeFullMap(); - assertTrue("failure in test: makeFullMap must return a non-null map.", - fm != null); + assertTrue("failure in test: makeFullMap must return a non-null map.", fm != null); Map fm2 = makeFullMap(); - assertTrue("failure in test: makeFullMap must return a non-null map.", - fm != null); - - assertTrue("failure in test: makeFullMap must return a new map " + - "with each invocation.", fm != fm2); + assertTrue("failure in test: makeFullMap must return a non-null map.", fm2 != null); + assertTrue("failure in test: makeFullMap must return a new map with each invocation.", fm != fm2); } /** @@ -522,13 +500,11 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public void testMapIsEmpty() { resetEmpty(); - assertEquals("Map.isEmpty() should return true with an empty map", - true, map.isEmpty()); + assertEquals("Map.isEmpty() should return true with an empty map", true, map.isEmpty()); verifyAll(); resetFull(); - assertEquals("Map.isEmpty() should return false with a non-empty map", - false, map.isEmpty()); + assertEquals("Map.isEmpty() should return false with a non-empty map", false, map.isEmpty()); verifyAll(); } @@ -537,13 +513,11 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public void testMapSize() { resetEmpty(); - assertEquals("Map.size() should be 0 with an empty map", - 0, map.size()); + assertEquals("Map.size() should be 0 with an empty map", 0, map.size()); verifyAll(); resetFull(); - assertEquals("Map.size() should equal the number of entries " + - "in the map", getSampleKeys().length, map.size()); + assertEquals("Map.size() should equal the number of entries in the map", getSampleKeys().length, map.size()); verifyAll(); } @@ -561,7 +535,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { resetFull(); map.clear(); fail("Expected UnsupportedOperationException on clear"); - } catch (UnsupportedOperationException ex) {} + } + catch (UnsupportedOperationException ex) { + } return; } @@ -576,7 +552,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { verifyAll(); } - /** * Tests Map.containsKey(Object) by verifying it returns false for all * sample keys on a map created using an empty map and returns true for @@ -586,16 +561,14 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Object[] keys = getSampleKeys(); resetEmpty(); - for(int i = 0; i < keys.length; i++) { - assertTrue("Map must not contain key when map is empty", - !map.containsKey(keys[i])); + for (Object key : keys) { + assertTrue("Map must not contain key when map is empty", !map.containsKey(key)); } verifyAll(); resetFull(); - for(int i = 0; i < keys.length; i++) { - assertTrue("Map must contain key for a mapping in the map. " + - "Missing: " + keys[i], map.containsKey(keys[i])); + for (Object key : keys) { + assertTrue("Map must contain key for a mapping in the map. Missing: " + key, map.containsKey(key)); } verifyAll(); } @@ -609,21 +582,18 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Object[] values = getSampleValues(); resetEmpty(); - for(int i = 0; i < values.length; i++) { - assertTrue("Empty map must not contain value", - !map.containsValue(values[i])); + for (Object value : values) { + assertTrue("Empty map must not contain value", !map.containsValue(value)); } verifyAll(); resetFull(); - for(int i = 0; i < values.length; i++) { - assertTrue("Map must contain value for a mapping in the map.", - map.containsValue(values[i])); + for (Object value : values) { + assertTrue("Map must contain value for a mapping in the map.", map.containsValue(value)); } verifyAll(); } - /** * Tests Map.equals(Object) */ @@ -646,12 +616,10 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { resetFull(); assertTrue("equals(null) returned true.", !map.equals(null)); - assertTrue("equals(new Object()) returned true.", - !map.equals(new Object())); + assertTrue("equals(new Object()) returned true.", !map.equals(new Object())); verifyAll(); } - /** * Tests Map.get(Object) */ @@ -661,16 +629,15 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Object[] keys = getSampleKeys(); Object[] values = getSampleValues(); - for (int i = 0; i < keys.length; i++) { - assertTrue("Empty map.get() should return null.", - map.get(keys[i]) == null); + for (Object key : keys) { + assertTrue("Empty map.get() should return null.", map.get(key) == null); } + verifyAll(); resetFull(); for (int i = 0; i < keys.length; i++) { - assertEquals("Full map.get() should return value from mapping.", - values[i], map.get(keys[i])); + assertEquals("Full map.get() should return value from mapping.", values[i], map.get(keys[i])); } } @@ -679,12 +646,10 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public void testMapHashCode() { resetEmpty(); - assertTrue("Empty maps have different hashCodes.", - map.hashCode() == confirmed.hashCode()); + assertTrue("Empty maps have different hashCodes.", map.hashCode() == confirmed.hashCode()); resetFull(); - assertTrue("Equal maps have different hashCodes.", - map.hashCode() == confirmed.hashCode()); + assertTrue("Equal maps have different hashCodes.", map.hashCode() == confirmed.hashCode()); } /** @@ -698,17 +663,14 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { */ public void testMapToString() { resetEmpty(); - assertTrue("Empty map toString() should not return null", - map.toString() != null); + assertTrue("Empty map toString() should not return null", map.toString() != null); verifyAll(); resetFull(); - assertTrue("Empty map toString() should not return null", - map.toString() != null); + assertTrue("Empty map toString() should not return null", map.toString() != null); verifyAll(); } - /** * Compare the current serialized form of the Map * against the canonical version in CVS. @@ -737,13 +699,13 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * against the canonical version in CVS. */ //public void testFullMapCompatibility() throws Exception { - /** - * Create canonical objects with this code - Map map = makeFullMap(); - if (!(map instanceof Serializable)) return; + /** + * Create canonical objects with this code + Map map = makeFullMap(); + if (!(map instanceof Serializable)) return; - writeExternalFormToDisk((Serializable) map, getCanonicalFullCollectionName(map)); - */ + writeExternalFormToDisk((Serializable) map, getCanonicalFullCollectionName(map)); + */ /* // test to make sure the canonical form has been preserved Map map = makeFullMap(); @@ -769,9 +731,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { verifyAll(); assertTrue("First map.put should return null", o == null); assertTrue("Map should contain key after put", - map.containsKey(keys[i])); + map.containsKey(keys[i])); assertTrue("Map should contain value after put", - map.containsValue(values[i])); + map.containsValue(values[i])); } if (isPutChangeSupported()) { for (int i = 0; i < keys.length; i++) { @@ -779,35 +741,41 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { confirmed.put(keys[i], newValues[i]); verifyAll(); assertEquals("Map.put should return previous value when changed", - values[i], o); + values[i], o); assertTrue("Map should still contain key after put when changed", - map.containsKey(keys[i])); + map.containsKey(keys[i])); assertTrue("Map should contain new value after put when changed", - map.containsValue(newValues[i])); + map.containsValue(newValues[i])); // if duplicates are allowed, we're not guaranteed that the value // no longer exists, so don't try checking that. if (!isAllowDuplicateValues()) { assertTrue("Map should not contain old value after put when changed", - !map.containsValue(values[i])); + !map.containsValue(values[i])); } } - } else { + } + else { try { // two possible exception here, either valid map.put(keys[0], newValues[0]); fail("Expected IllegalArgumentException or UnsupportedOperationException on put (change)"); - } catch (IllegalArgumentException ex) { - } catch (UnsupportedOperationException ex) {} + } + catch (IllegalArgumentException ex) { + } + catch (UnsupportedOperationException ex) { + } } - - } else if (isPutChangeSupported()) { + } + else if (isPutChangeSupported()) { resetEmpty(); try { map.put(keys[0], values[0]); fail("Expected UnsupportedOperationException or IllegalArgumentException on put (add) when fixed size"); - } catch (IllegalArgumentException ex) { - } catch (UnsupportedOperationException ex) { + } + catch (IllegalArgumentException ex) { + } + catch (UnsupportedOperationException ex) { } resetFull(); @@ -818,24 +786,27 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Object value = confirmed.put(key, newValues[i]); verifyAll(); assertEquals("Map.put should return previous value when changed", - value, o); + value, o); assertTrue("Map should still contain key after put when changed", - map.containsKey(key)); + map.containsKey(key)); assertTrue("Map should contain new value after put when changed", - map.containsValue(newValues[i])); + map.containsValue(newValues[i])); // if duplicates are allowed, we're not guaranteed that the value // no longer exists, so don't try checking that. if (!isAllowDuplicateValues()) { assertTrue("Map should not contain old value after put when changed", - !map.containsValue(values[i])); + !map.containsValue(values[i])); } } - } else { + } + else { try { map.put(keys[0], values[0]); fail("Expected UnsupportedOperationException on put (add)"); - } catch (UnsupportedOperationException ex) {} + } + catch (UnsupportedOperationException ex) { + } } } @@ -849,12 +820,16 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { if (isPutAddSupported()) { if (isAllowNullKey()) { map.put(null, values[0]); - } else { + } + else { try { map.put(null, values[0]); fail("put(null, value) should throw NPE/IAE"); - } catch (NullPointerException ex) { - } catch (IllegalArgumentException ex) {} + } + catch (NullPointerException ex) { + } + catch (IllegalArgumentException ex) { + } } } } @@ -869,12 +844,16 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { if (isPutAddSupported()) { if (isAllowNullValue()) { map.put(keys[0], null); - } else { + } + else { try { map.put(keys[0], null); fail("put(key, null) should throw NPE/IAE"); - } catch (NullPointerException ex) { - } catch (IllegalArgumentException ex) {} + } + catch (NullPointerException ex) { + } + catch (IllegalArgumentException ex) { + } } } } @@ -890,7 +869,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { try { map.putAll(temp); fail("Expected UnsupportedOperationException on putAll"); - } catch (UnsupportedOperationException ex) {} + } + catch (UnsupportedOperationException ex) { + } } return; } @@ -908,7 +889,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { m2 = makeConfirmedMap(); Object[] keys = getSampleKeys(); Object[] values = getSampleValues(); - for(int i = 0; i < keys.length; i++) { + for (int i = 0; i < keys.length; i++) { m2.put(keys[i], values[i]); } @@ -926,7 +907,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { resetFull(); map.remove(map.keySet().iterator().next()); fail("Expected UnsupportedOperationException on remove"); - } catch (UnsupportedOperationException ex) {} + } + catch (UnsupportedOperationException ex) { + } return; } @@ -934,7 +917,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Object[] keys = getSampleKeys(); Object[] values = getSampleValues(); - for(int i = 0; i < keys.length; i++) { + for (int i = 0; i < keys.length; i++) { Object o = map.remove(keys[i]); assertTrue("First map.remove should return null", o == null); } @@ -942,13 +925,13 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { resetFull(); - for(int i = 0; i < keys.length; i++) { + for (int i = 0; i < keys.length; i++) { Object o = map.remove(keys[i]); confirmed.remove(keys[i]); verifyAll(); assertEquals("map.remove with valid key should return value", - values[i], o); + values[i], o); } Object[] other = getOtherKeys(); @@ -958,20 +941,23 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { for (int i = 0; i < other.length; i++) { Object o = map.remove(other[i]); assertEquals("map.remove for nonexistent key should return null", - o, null); + o, null); assertEquals("map.remove for nonexistent key should not " + - "shrink map", size, map.size()); + "shrink map", size, map.size()); } verifyAll(); } //----------------------------------------------------------------------- + /** * Tests that the {@link Map#values} collection is backed by * the underlying map for clear(). */ public void testValuesClearChangesMap() { - if (!isRemoveSupported()) return; + if (!isRemoveSupported()) { + return; + } // clear values, reflected in map resetFull(); @@ -997,7 +983,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * the underlying map for clear(). */ public void testKeySetClearChangesMap() { - if (!isRemoveSupported()) return; + if (!isRemoveSupported()) { + return; + } // clear values, reflected in map resetFull(); @@ -1023,7 +1011,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * the underlying map for clear(). */ public void testEntrySetClearChangesMap() { - if (!isRemoveSupported()) return; + if (!isRemoveSupported()) { + return; + } // clear values, reflected in map resetFull(); @@ -1051,6 +1041,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Map.Entry entry = (Map.Entry) entrySet.iterator().next(); assertEquals(true, entrySet.contains(entry)); } + public void testEntrySetContains2() { resetFull(); Set entrySet = map.entrySet(); @@ -1058,6 +1049,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { Map.Entry test = cloneMapEntry(entry); assertEquals(true, entrySet.contains(test)); } + public void testEntrySetContains3() { resetFull(); Set entrySet = map.entrySet(); @@ -1069,7 +1061,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } public void testEntrySetRemove1() { - if (!isRemoveSupported()) return; + if (!isRemoveSupported()) { + return; + } resetFull(); int size = map.size(); Set entrySet = map.entrySet(); @@ -1080,8 +1074,11 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { assertEquals(false, map.containsKey(key)); assertEquals(size - 1, map.size()); } + public void testEntrySetRemove2() { - if (!isRemoveSupported()) return; + if (!isRemoveSupported()) { + return; + } resetFull(); int size = map.size(); Set entrySet = map.entrySet(); @@ -1093,8 +1090,11 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { assertEquals(false, map.containsKey(key)); assertEquals(size - 1, map.size()); } + public void testEntrySetRemove3() { - if (!isRemoveSupported()) return; + if (!isRemoveSupported()) { + return; + } resetFull(); int size = map.size(); Set entrySet = map.entrySet(); @@ -1110,6 +1110,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } //----------------------------------------------------------------------- + /** * Tests that the {@link Map#values} collection is backed by * the underlying map by removing from the values collection @@ -1135,7 +1136,8 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { while (values.contains(sampleValues[i]) && j < 10000) { try { values.remove(sampleValues[i]); - } catch (UnsupportedOperationException e) { + } + catch (UnsupportedOperationException e) { // if values.remove is unsupported, just skip this test return; } @@ -1143,8 +1145,8 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } assertTrue("values().remove(obj) is broken", j < 10000); assertTrue( - "Value should have been removed from the underlying map.", - !map.containsValue(sampleValues[i])); + "Value should have been removed from the underlying map.", + !map.containsValue(sampleValues[i])); } } } @@ -1161,13 +1163,14 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { for (int i = 0; i < sampleKeys.length; i++) { try { keys.remove(sampleKeys[i]); - } catch (UnsupportedOperationException e) { + } + catch (UnsupportedOperationException e) { // if key.remove is unsupported, just skip this test return; } assertTrue( - "Key should have been removed from the underlying map.", - !map.containsKey(sampleKeys[i])); + "Key should have been removed from the underlying map.", + !map.containsKey(sampleKeys[i])); } } @@ -1176,13 +1179,12 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { // same for EntrySet/KeySet/values's // Iterator.remove, removeAll, retainAll - /** * Utility methods to create an array of Map.Entry objects * out of the given key and value arrays.

    * - * @param keys the array of keys - * @param values the array of values + * @param keys the array of keys + * @param values the array of values * @return an array of Map.Entry of those keys to those values */ private Map.Entry[] makeEntryArray(Object[] keys, Object[] values) { @@ -1195,7 +1197,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { return result; } - /** * Bulk test {@link Map#entrySet()}. This method runs through all of * the tests in {@link SetAbstractTestCase}. @@ -1238,13 +1239,16 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { // Collection views don't support add operations. return false; } + public boolean isRemoveSupported() { // Entry set should only support remove if map does return MapAbstractTestCase.this.isRemoveSupported(); } + public boolean isGetStructuralModify() { return MapAbstractTestCase.this.isGetStructuralModify(); } + public boolean isTestSerialization() { return false; } @@ -1279,9 +1283,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { public void testMapEntrySetIteratorEntrySetValue() { Object key1 = getSampleKeys()[0]; - Object key2 = (getSampleKeys().length ==1 ? getSampleKeys()[0] : getSampleKeys()[1]); + Object key2 = (getSampleKeys().length == 1 ? getSampleKeys()[0] : getSampleKeys()[1]); Object newValue1 = getNewSampleValues()[0]; - Object newValue2 = (getNewSampleValues().length ==1 ? getNewSampleValues()[0] : getNewSampleValues()[1]); + Object newValue2 = (getNewSampleValues().length == 1 ? getNewSampleValues()[0] : getNewSampleValues()[1]); resetFull(); // explicitly get entries as sample values/keys are connected for some maps @@ -1299,7 +1303,8 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { if (isSetValueSupported() == false) { try { entry1.setValue(newValue1); - } catch (UnsupportedOperationException ex) { + } + catch (UnsupportedOperationException ex) { } return; } @@ -1338,7 +1343,8 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { entry = temp; break; } - } else if (temp.getKey().equals(key)) { + } + else if (temp.getKey().equals(key)) { entry = temp; break; } @@ -1348,7 +1354,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } public void testMapEntrySetRemoveNonMapEntry() { - if (isRemoveSupported() == false) return; + if (isRemoveSupported() == false) { + return; + } resetFull(); assertEquals(false, getSet().remove(null)); assertEquals(false, getSet().remove(new Object())); @@ -1360,7 +1368,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } } - /** * Bulk test {@link Map#keySet()}. This method runs through all of * the tests in {@link SetAbstractTestCase}. @@ -1395,12 +1402,15 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { public boolean isNullSupported() { return MapAbstractTestCase.this.isAllowNullKey(); } + public boolean isAddSupported() { return false; } + public boolean isRemoveSupported() { return MapAbstractTestCase.this.isRemoveSupported(); } + public boolean isTestSerialization() { return false; } @@ -1423,7 +1433,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } } - /** * Bulk test {@link Map#values()}. This method runs through all of * the tests in {@link CollectionAbstractTestCase}. @@ -1431,7 +1440,7 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { * that the map and the other collection views are still valid. * * @return a {@link CollectionAbstractTestCase} instance for testing the map's - * values collection + * values collection */ /* public BulkTest bulkTestMapValues() { @@ -1459,12 +1468,15 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { public boolean isNullSupported() { return MapAbstractTestCase.this.isAllowNullKey(); } + public boolean isAddSupported() { return false; } + public boolean isRemoveSupported() { return MapAbstractTestCase.this.isRemoveSupported(); } + public boolean isTestSerialization() { return false; } @@ -1507,7 +1519,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { // the value equal to the value returned from the values iterator. } - /** * Resets the {@link #map}, {@link #entrySet}, {@link #keySet}, * {@link #values} and {@link #confirmed} fields to empty. @@ -1533,7 +1544,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { } } - /** * Resets the collection view fields. */ @@ -1543,7 +1553,6 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { this.entrySet = map.entrySet(); } - /** * Verifies that {@link #map} is still equal to {@link #confirmed}. * This method checks that the map is equal to the HashMap, @@ -1564,12 +1573,9 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { public void verifyMap() { int size = confirmed.size(); boolean empty = confirmed.isEmpty(); - assertEquals("Map should be same size as HashMap", - size, map.size()); - assertEquals("Map should be empty if HashMap is", - empty, map.isEmpty()); - assertEquals("hashCodes should be the same", - confirmed.hashCode(), map.hashCode()); + assertEquals("Map should be same size as HashMap", size, map.size()); + assertEquals("Map should be empty if HashMap is", empty, map.isEmpty()); + assertEquals("hashCodes should be the same", confirmed.hashCode(), map.hashCode()); // this fails for LRUMap because confirmed.equals() somehow modifies // map, causing concurrent modification exceptions. //assertEquals("Map should still equal HashMap", confirmed, map); @@ -1584,39 +1590,29 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { public void verifyEntrySet() { int size = confirmed.size(); boolean empty = confirmed.isEmpty(); - assertEquals("entrySet should be same size as HashMap's" + - "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), - size, entrySet.size()); - assertEquals("entrySet should be empty if HashMap is" + - "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), - empty, entrySet.isEmpty()); - assertTrue("entrySet should contain all HashMap's elements" + - "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), - entrySet.containsAll(confirmed.entrySet())); - assertEquals("entrySet hashCodes should be the same" + - "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), - confirmed.entrySet().hashCode(), entrySet.hashCode()); - assertEquals("Map's entry set should still equal HashMap's", - confirmed.entrySet(), entrySet); + assertEquals("entrySet should be same size as HashMap's\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + size, entrySet.size()); + assertEquals("entrySet should be empty if HashMap is\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + empty, entrySet.isEmpty()); + assertTrue("entrySet should contain all HashMap's elements\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + entrySet.containsAll(confirmed.entrySet())); + assertEquals("entrySet hashCodes should be the same\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + confirmed.entrySet().hashCode(), entrySet.hashCode()); + assertEquals("Map's entry set should still equal HashMap's", confirmed.entrySet(), entrySet); } public void verifyKeySet() { int size = confirmed.size(); boolean empty = confirmed.isEmpty(); - assertEquals("keySet should be same size as HashMap's" + - "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), - size, keySet.size()); - assertEquals("keySet should be empty if HashMap is" + - "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), - empty, keySet.isEmpty()); - assertTrue("keySet should contain all HashMap's elements" + - "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), - keySet.containsAll(confirmed.keySet())); - assertEquals("keySet hashCodes should be the same" + - "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), - confirmed.keySet().hashCode(), keySet.hashCode()); - assertEquals("Map's key set should still equal HashMap's", - confirmed.keySet(), keySet); + assertEquals("keySet should be same size as HashMap's\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + size, keySet.size()); + assertEquals("keySet should be empty if HashMap is\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + empty, keySet.isEmpty()); + assertTrue("keySet should contain all HashMap's elements\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + keySet.containsAll(confirmed.keySet())); + assertEquals("keySet hashCodes should be the same\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + confirmed.keySet().hashCode(), keySet.hashCode()); + assertEquals("Map's key set should still equal HashMap's", confirmed.keySet(), keySet); } public void verifyValues() { @@ -1625,27 +1621,20 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { int size = confirmed.size(); boolean empty = confirmed.isEmpty(); - assertEquals("values should be same size as HashMap's" + - "\nTest: " + test + "\nReal: " + known, - size, values.size()); - assertEquals("values should be empty if HashMap is" + - "\nTest: " + test + "\nReal: " + known, - empty, values.isEmpty()); - assertTrue("values should contain all HashMap's elements" + - "\nTest: " + test + "\nReal: " + known, - test.containsAll(known)); - assertTrue("values should contain all HashMap's elements" + - "\nTest: " + test + "\nReal: " + known, - known.containsAll(test)); - // originally coded to use a HashBag, but now separate jar so... - for (Iterator it = known.iterator(); it.hasNext();) { - boolean removed = test.remove(it.next()); + + assertEquals("values should be same size as HashMap's\nTest: " + test + "\nReal: " + known, size, values.size()); + assertEquals("values should be empty if HashMap is\nTest: " + test + "\nReal: " + known, empty, values.isEmpty()); + assertTrue("values should contain all HashMap's elements\nTest: " + test + "\nReal: " + known, test.containsAll(known)); + assertTrue("values should contain all HashMap's elements\nTest: " + test + "\nReal: " + known, known.containsAll(test)); + + for (Object aKnown : known) { + boolean removed = test.remove(aKnown); assertTrue("Map's values should still equal HashMap's", removed); } + assertTrue("Map's values should still equal HashMap's", test.isEmpty()); } - /** * Erases any leftover instance variables by setting them to null. */ @@ -1656,5 +1645,4 @@ public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { values = null; confirmed = null; } - } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java index 7efbc438..c781d776 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java @@ -15,7 +15,7 @@ */ package com.twelvemonkeys.util; -import org.jmock.cglib.MockObjectTestCase; +import junit.framework.TestCase; import java.io.*; @@ -35,9 +35,9 @@ import java.io.*; * @author Stephen Colebourne * @author Anonymous */ -public abstract class ObjectAbstractTestCase extends MockObjectTestCase { - +public abstract class ObjectAbstractTestCase extends TestCase { //----------------------------------------------------------------------- + /** * Implement this method to return the object to test. * diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java index 006acde8..ec6aeecc 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java @@ -1,11 +1,10 @@ package com.twelvemonkeys.util.convert; import com.twelvemonkeys.lang.DateUtil; +import org.junit.Test; import java.text.DateFormat; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; +import java.util.*; /** * DateConverterTestCase @@ -44,15 +43,19 @@ public class DateConverterTestCase extends PropertyConverterAbstractTestCase { }; } + @Test @Override public void testConvert() { - TimeZone old = TimeZone.getDefault(); + // Custom setup, to make test cases stable: Always use GMT + TimeZone oldTZ = TimeZone.getDefault(); + try { TimeZone.setDefault(TimeZone.getTimeZone("GMT")); super.testConvert(); } finally { - TimeZone.setDefault(old); + // Restore + TimeZone.setDefault(oldTZ); } } } \ No newline at end of file diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java index e5ff4226..65c879bd 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java @@ -1,5 +1,8 @@ package com.twelvemonkeys.util.convert; +import java.io.File; +import java.net.URI; + /** * DefaultConverterTestCase *

    @@ -20,7 +23,9 @@ public class DefaultConverterTestCase extends PropertyConverterAbstractTestCase new Conversion("true", Boolean.TRUE), new Conversion("TRUE", Boolean.TRUE, null, "true"), new Conversion("false", Boolean.FALSE), - new Conversion("FALSE", new Boolean(false), null, "false"), + new Conversion("FALSE", false, null, "false"), + + new Conversion("2", 2), // Stupid but valid new Conversion("fooBar", "fooBar"), @@ -29,6 +34,22 @@ public class DefaultConverterTestCase extends PropertyConverterAbstractTestCase // Stupid test class that reveres chars new Conversion("fooBar", new FooBar("fooBar")), + // String array tests + new Conversion("foo, bar, baz", new String[] {"foo", "bar", "baz"}), + new Conversion("foo", new String[] {"foo"}), + new Conversion("foo;bar; baz", new String[] {"foo", "bar", "baz"}, "; ", "foo; bar; baz"), + + // Native array tests + new Conversion("1, 2, 3", new int[] {1, 2, 3}), + new Conversion("-1, 42, 0", new long[] {-1, 42, 0}), + new Conversion("true, true, false", new boolean[] {true, true, false}), + new Conversion(".3, 4E7, .97", new float[] {.3f, 4e7f, .97f}, ", ", "0.3, 4.0E7, 0.97"), + + // Object array test + new Conversion("foo, bar", new FooBar[] {new FooBar("foo"), new FooBar("bar")}), + new Conversion("/temp, /usr/local/bin", new File[] {new File("/temp"), new File("/usr/local/bin")}), + new Conversion("file:/temp, http://java.net/", new URI[] {URI.create("file:/temp"), URI.create("http://java.net/")}), + // TODO: More tests }; } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java index 2f557027..a848ad75 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java @@ -1,6 +1,12 @@ package com.twelvemonkeys.util.convert; import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import com.twelvemonkeys.lang.Validate; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.*; /** * PropertyConverterAbstractTestCase @@ -11,7 +17,6 @@ import com.twelvemonkeys.lang.ObjectAbstractTestCase; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java#2 $ */ public abstract class PropertyConverterAbstractTestCase extends ObjectAbstractTestCase { - protected Object makeObject() { return makePropertyConverter(); } @@ -20,8 +25,8 @@ public abstract class PropertyConverterAbstractTestCase extends ObjectAbstractTe protected abstract Conversion[] getTestConversions(); + @Test public void testConvert() { - PropertyConverter converter = makePropertyConverter(); Conversion[] tests = getTestConversions(); @@ -30,70 +35,120 @@ public abstract class PropertyConverterAbstractTestCase extends ObjectAbstractTe Object obj; try { obj = converter.toObject(test.original(), test.type(), test.format()); - assertEquals("'" + test.original() + "' convtered to incorrect type", test.type(), obj.getClass()); - assertEquals("'" + test.original() + "' not converted", test.value(), obj); + + assertEquals(String.format("'%s' converted to incorrect type", test.original()), test.type(), obj.getClass()); + if (test.type().isArray()) { + assertArrayEquals0(String.format("'%s' not converted", test.original()), test.value(), obj); + } + else { + assertEquals(String.format("'%s' not converted", test.original()), test.value(), obj); + } String result = converter.toString(test.value(), test.format()); - assertEquals("'" + test.converted() + "' does not macth", test.converted(), result); + assertEquals(String.format("'%s' does not match", test.converted()), test.converted(), result); obj = converter.toObject(result, test.type(), test.format()); - assertEquals("'" + test.original() + "' convtered to incorrect type", test.type(), obj.getClass()); - assertEquals("'" + test.original() + "' did not survive roundrip conversion", test.value(), obj); + assertEquals(String.format("'%s' converted to incorrect type", test.original()), test.type(), obj.getClass()); + + if (test.type().isArray()) { + assertArrayEquals0(String.format("'%s' did not survive round trip conversion", test.original()), test.value(), obj); + } + else { + assertEquals(String.format("'%s' did not survive round trip conversion", test.original()), test.value(), obj); + } } catch (ConversionException e) { - e.printStackTrace(); - fail("Converting '" + test.original() + "' to " + test.type() + " failed: " + e.getMessage()); + failBecause(String.format("Converting '%s' to %s failed", test.original(), test.type()), e); } } } + private static void assertArrayEquals0(final String message, final Object left, final Object right) { + Class componentType = left.getClass().getComponentType(); + if (componentType.isPrimitive()) { + if (int.class == componentType) { + assertArrayEquals(message, (int[]) left, (int[]) right); + } + else if (short.class == componentType) { + assertArrayEquals(message, (short[]) left, (short[]) right); + } + else if (long.class == componentType) { + assertArrayEquals(message, (long[]) left, (long[]) right); + } + else if (float.class == componentType) { + assertArrayEquals(message, (float[]) left, (float[]) right, 0f); + } + else if (double.class == componentType) { + assertArrayEquals(message, (double[]) left, (double[]) right, 0d); + } + else if (boolean.class == componentType) { + assertTrue(message, Arrays.equals((boolean[]) left, (boolean[]) right)); + } + else if (byte.class == componentType) { + assertArrayEquals(message, (byte[]) left, (byte[]) right); + } + else if (char.class == componentType) { + assertArrayEquals(message, (char[]) left, (char[]) right); + } + else { + fail(String.format("Unknown primitive type: %s", componentType)); + } + } + else { + assertArrayEquals(message, (Object[]) left, (Object[]) right); + } + } + + private static void failBecause(String message, Throwable exception) { + AssertionError error = new AssertionError(message); + error.initCause(exception); + throw error; + } + public static final class Conversion { - private final String mStrVal; - private final Object mObjVal; - private final String mFormat; - private final String mConvertedStrVal; + private final String strVal; + private final Object objVal; + private final String format; + private final String convertedStrVal; public Conversion(String pStrVal, Object pObjVal) { - this(pStrVal, pObjVal, null, null); + this(pStrVal, pObjVal, null); } public Conversion(String pStrVal, Object pObjVal, String pFormat) { - this(pStrVal, pObjVal, pFormat, null); + this(pStrVal, pObjVal, pFormat, pStrVal); } public Conversion(String pStrVal, Object pObjVal, String pFormat, String pConvertedStrVal) { - if (pStrVal == null) { - throw new IllegalArgumentException("pStrVal == null"); - } - if (pObjVal == null) { - throw new IllegalArgumentException("pObjVal == null"); - } + Validate.notNull(pStrVal, "strVal"); + Validate.notNull(pObjVal, "objVal"); + Validate.notNull(pConvertedStrVal, "convertedStrVal"); - mStrVal = pStrVal; - mObjVal = pObjVal; - mFormat = pFormat; - mConvertedStrVal = pConvertedStrVal != null ? pConvertedStrVal : pStrVal; + strVal = pStrVal; + objVal = pObjVal; + format = pFormat; + convertedStrVal = pConvertedStrVal; } public String original() { - return mStrVal; + return strVal; } public Object value() { - return mObjVal; + return objVal; } public Class type() { - return mObjVal.getClass(); + return objVal.getClass(); } public String format() { - return mFormat; + return format; } public String converted() { - return mConvertedStrVal; + return convertedStrVal; } } } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPI.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPI.java new file mode 100644 index 00000000..aa078a3e --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPI.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util.service; + +/** +* ExampleSPI +* +* @author Harald Kuhr +* @author last modified by $Author: haraldk$ +* @version $Id: ExampleSPI.java,v 1.0 25.01.12 16:24 haraldk Exp$ +*/ +abstract class DummySPI { +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPIImpl.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPIImpl.java new file mode 100644 index 00000000..8ae8b908 --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPIImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util.service; + +/** + * DummySPIImpl + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: DummySPIImpl.java,v 1.0 25.01.12 16:25 haraldk Exp$ + */ +class DummySPIImpl extends DummySPI { +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPIToo.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPIToo.java new file mode 100644 index 00000000..4d2f6c73 --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/DummySPIToo.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util.service; + +/** + * DummySPIToo + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: DummySPIToo.java,v 1.0 25.01.12 16:27 haraldk Exp$ + */ +class DummySPIToo extends DummySPI { +} diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/service/ServiceRegistryTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/ServiceRegistryTest.java new file mode 100644 index 00000000..fe7b8f32 --- /dev/null +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/service/ServiceRegistryTest.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util.service; + +import com.twelvemonkeys.util.CollectionUtil; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + +/** + * ServiceRegistryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ServiceRegistryTest.java,v 1.0 25.01.12 16:16 haraldk Exp$ + */ +public class ServiceRegistryTest { + + private final TestRegistry registry = new TestRegistry(); + + @Test(expected = IllegalArgumentException.class) + public void testCreateNull() { + new ServiceRegistry(null); + } + + @Test + public void testCreateEmptyIterator() { + // A completely useless registry... + ServiceRegistry registry = new ServiceRegistry(Collections.>emptyList().iterator()); + registry.registerApplicationClasspathSPIs(); + + while (registry.categories().hasNext()) { + fail("No categories"); + } + } + + @Test(expected = ServiceConfigurationError.class) + public void testCreateBadConfig() { + @SuppressWarnings("unchecked") + ServiceRegistry registry = new ServiceRegistry(Arrays.asList(BadSPI.class).iterator()); + registry.registerApplicationClasspathSPIs(); + + // DONE: Test non-class + + // TODO: Test class not implementing SPI category + // TODO: Test class that throws exception in constructor + // TODO: Test class that has no public no-args constructor + // TODO: Test IOException + // Some of these can be tested using stubs, via the package protected registerSPIs method + } + + @Test + public void testCategories() { + // Categories + Iterator> categories = registry.categories(); + assertTrue(categories.hasNext()); + Class category = categories.next(); + assertEquals(DummySPI.class, category); + assertFalse(categories.hasNext()); + } + + @Test + public void testProviders() { + // Providers + Iterator providers = registry.providers(DummySPI.class); + List providerList = new ArrayList(); + CollectionUtil.addAll(providerList, providers); + + assertEquals(2, providerList.size()); + + // Order should be as in configuration file + assertNotNull(providerList.get(0)); + assertEquals(DummySPIImpl.class, providerList.get(0).getClass()); + assertNotNull(providerList.get(1)); + assertEquals(DummySPIToo.class, providerList.get(1).getClass()); + } + + @Test + public void testCompatibleCategoriesNull() { + // Compatible categories + Iterator> categories = registry.compatibleCategories(null); + assertFalse(categories.hasNext()); + } + + @Test + public void testCompatibleCategoriesImpl() { + Iterator> categories = registry.compatibleCategories(new DummySPIImpl()); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testCompatibleCategoriesToo() { + Iterator> categories = registry.compatibleCategories(new DummySPIToo()); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testCompatibleCategoriesNonRegistered() { + Iterator> categories = registry.compatibleCategories(new DummySPI() {}); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testCompatibleCategoriesUnknownType() { + Iterator> categories = registry.compatibleCategories(new Object()); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesNull() { + // Containing categories + Iterator> categories = registry.containingCategories(null); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesKnownInstanceImpl() { + Iterator providers = registry.providers(DummySPI.class); + assertTrue(providers.hasNext()); // Sanity check + + Iterator> categories = registry.containingCategories(providers.next()); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesKnownInstanceToo() { + Iterator providers = registry.providers(DummySPI.class); + providers.next(); + assertTrue(providers.hasNext()); // Sanity check + + Iterator> categories = registry.containingCategories(providers.next()); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesNewInstanceRegisteredImpl() { + // NOTE: Currently we match based on type, rather than instance, but it does make sense... + Iterator> categories = registry.containingCategories(new DummySPIImpl()); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesNewInstanceRegisteredToo() { + // NOTE: Currently we match based on type, rather than instance, but it does make sense... + Iterator> categories = registry.containingCategories(new DummySPIToo()); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesCompatibleNonRegisteredType() { + Iterator> categories = registry.containingCategories(new DummySPI() {}); + assertFalse(categories.hasNext()); + } + + @Test + public void testContainingCategoriesUnknownType() { + Iterator> categories = registry.containingCategories(new Object()); + assertFalse(categories.hasNext()); + } + + @Test + public void testRegister() { + // Register + DummySPI dummy = new DummySPI() {}; + assertTrue(registry.register(dummy)); + + // Should now have category + Iterator> categories = registry.containingCategories(dummy); + assertTrue(categories.hasNext()); + assertEquals(DummySPI.class, categories.next()); + assertFalse(categories.hasNext()); + + // Should now be in providers + Iterator providers = registry.providers(DummySPI.class); + List providerList = new ArrayList(); + CollectionUtil.addAll(providerList, providers); + + assertEquals(3, providerList.size()); + + assertNotNull(providerList.get(1)); + assertSame(dummy, providerList.get(2)); + } + + @Test + public void testRegisterAlreadyRegistered() { + Iterator providers = registry.providers(DummySPI.class); + assertTrue(providers.hasNext()); // Sanity check + + assertFalse(registry.register(providers.next())); + } + + @Test + public void testRegisterNull() { + assertFalse(registry.register(null)); + } + + @Test + public void testRegisterIncompatible() { + assertFalse(registry.register(new Object())); + } + + @Test + public void testDeregisterNull() { + assertFalse(registry.deregister(null)); + } + + @Test + public void testDeregisterIncompatible() { + assertFalse(registry.deregister(new Object())); + } + + @Test + public void testDeregisterCompatibleNonRegistered() { + DummySPI dummy = new DummySPI() {}; + assertFalse(registry.deregister(dummy)); + } + + @Test + public void testDeregister() { + Iterator providers = registry.providers(DummySPI.class); + assertTrue(providers.hasNext()); // Sanity check + DummySPI instance = providers.next(); + assertTrue(registry.deregister(instance)); + + // Test no longer in registry + providers = registry.providers(DummySPI.class); + int count = 0; + while (providers.hasNext()) { + DummySPI next = providers.next(); + assertNotSame(instance, next); + count++; + } + + assertEquals(1, count); + } + + // TODO: Test register with category + // TODO: Test register with unknown category + // TODO: Test register with null category + + // TODO: Test de-register with category + // TODO: Test de-register with unknown category + // TODO: Test de-register with null category + + + private static class TestRegistry extends ServiceRegistry { + @SuppressWarnings("unchecked") + public TestRegistry() { + super(Arrays.asList(DummySPI.class).iterator()); + registerApplicationClasspathSPIs(); + } + } + + public static class BadSPI {} +} diff --git a/common/common-lang/src/test/resources/META-INF/services/com.twelvemonkeys.util.service.DummySPI b/common/common-lang/src/test/resources/META-INF/services/com.twelvemonkeys.util.service.DummySPI new file mode 100644 index 00000000..fc5fac87 --- /dev/null +++ b/common/common-lang/src/test/resources/META-INF/services/com.twelvemonkeys.util.service.DummySPI @@ -0,0 +1,2 @@ +com.twelvemonkeys.util.service.DummySPIImpl +com.twelvemonkeys.util.service.DummySPIToo diff --git a/common/common-lang/src/test/resources/META-INF/services/com.twelvemonkeys.util.service.ServiceRegistryTest$BadSPI b/common/common-lang/src/test/resources/META-INF/services/com.twelvemonkeys.util.service.ServiceRegistryTest$BadSPI new file mode 100644 index 00000000..39afdee7 --- /dev/null +++ b/common/common-lang/src/test/resources/META-INF/services/com.twelvemonkeys.util.service.ServiceRegistryTest$BadSPI @@ -0,0 +1,3 @@ +# Any line that does not resolve to a class name, will make registerApplicationClasspathSPIs throw error +Bad, bad spi +So bad. So bad. diff --git a/common/pom.xml b/common/pom.xml index 319c92a7..f537ad61 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -17,33 +17,34 @@ - common-io - common-lang - common-image + common-lang + common-io + common-image + + + + ${project.groupId} + common-lang + ${project.version} + - - - - ${project.groupId} - common-lang - ${project.version} - - - ${project.groupId} - common-lang - ${project.version} - tests - test - - - ${project.groupId} - common-io - ${project.version} - - - + + ${project.groupId} + common-lang + ${project.version} + tests + test + + + + ${project.groupId} + common-io + ${project.version} + + + junit @@ -51,13 +52,5 @@ 4.7 test - - - jmock - jmock-cglib - 1.0.1 - test - - diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java index ec0bd874..43d57219 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java @@ -74,7 +74,7 @@ import java.util.Map; * @see batik-dev */ public class SVGImageReader extends ImageReaderBase { - private Rasterizer mRasterizer = new Rasterizer(); + private Rasterizer rasterizer = new Rasterizer(); /** * Creates an {@code SVGImageReader}. @@ -91,16 +91,16 @@ public class SVGImageReader extends ImageReaderBase { @Override public void dispose() { super.dispose(); - mRasterizer = null; + rasterizer = null; } @Override - public void setInput(Object pInput, boolean pSeekForwardOnly, boolean pIgnoreMetadata) { - super.setInput(pInput, pSeekForwardOnly, pIgnoreMetadata); + public void setInput(Object pInput, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(pInput, seekForwardOnly, ignoreMetadata); - if (mImageInput != null) { - TranscoderInput input = new TranscoderInput(IIOUtil.createStreamAdapter(mImageInput)); - mRasterizer.setInput(input); + if (imageInput != null) { + TranscoderInput input = new TranscoderInput(IIOUtil.createStreamAdapter(imageInput)); + rasterizer.setInput(input); } } @@ -114,7 +114,7 @@ public class SVGImageReader extends ImageReaderBase { // Set IIOParams as hints // Note: The cast to Map invokes a different method that preserves // unset defaults, DO NOT REMOVE! - mRasterizer.setTranscodingHints((Map) paramsToHints(svgParam)); + rasterizer.setTranscodingHints((Map) paramsToHints(svgParam)); // Get the base URI (not a hint) baseURI = svgParam.getBaseURI(); @@ -134,8 +134,8 @@ public class SVGImageReader extends ImageReaderBase { try { processImageStarted(pIndex); - mRasterizer.mTranscoderInput.setURI(baseURI); - BufferedImage image = mRasterizer.getImage(); + rasterizer.mTranscoderInput.setURI(baseURI); + BufferedImage image = rasterizer.getImage(); Graphics2D g = destination.createGraphics(); try { @@ -223,7 +223,7 @@ public class SVGImageReader extends ImageReaderBase { public int getWidth(int pIndex) throws IOException { checkBounds(pIndex); try { - return mRasterizer.getDefaultWidth(); + return rasterizer.getDefaultWidth(); } catch (TranscoderException e) { throw new IIOException(e.getMessage(), e); @@ -233,7 +233,7 @@ public class SVGImageReader extends ImageReaderBase { public int getHeight(int pIndex) throws IOException { checkBounds(pIndex); try { - return mRasterizer.getDefaultHeight(); + return rasterizer.getDefaultHeight(); } catch (TranscoderException e) { throw new IIOException(e.getMessage(), e); @@ -241,7 +241,7 @@ public class SVGImageReader extends ImageReaderBase { } public Iterator getImageTypes(int imageIndex) throws IOException { - return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(mRasterizer.createImage(1, 1))).iterator(); + return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator(); } /** diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGReadParam.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGReadParam.java index a4ec26f4..9d384807 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGReadParam.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGReadParam.java @@ -37,23 +37,23 @@ import java.awt.*; * */ public class SVGReadParam extends ImageReadParam { - private Paint mBackground; - private String mBaseURI; + private Paint background; + private String baseURI; public Paint getBackgroundColor() { - return mBackground; + return background; } public void setBackgroundColor(Paint pColor) { - mBackground = pColor; + background = pColor; } public String getBaseURI() { - return mBaseURI; + return baseURI; } public void setBaseURI(String pBaseURI) { - mBaseURI = pBaseURI; + baseURI = pBaseURI; } @Override diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java deleted file mode 100755 index caab3b4c..00000000 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.imageio.plugins.tiff; - -import com.twelvemonkeys.image.ImageUtil; -import com.twelvemonkeys.imageio.ImageReaderBase; -import org.apache.batik.ext.awt.image.codec.SeekableStream; -import org.apache.batik.ext.awt.image.codec.tiff.TIFFDecodeParam; -import org.apache.batik.ext.awt.image.codec.tiff.TIFFImageDecoder; - -import javax.imageio.ImageReadParam; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.spi.ImageReaderSpi; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -/** - * TIFFImageReader class description. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: TIFFImageReader.java,v 1.0 29.jul.2004 12:52:33 haku Exp $ - */ -// TODO: Massive clean-up -// TODO: Support raster decoding... -public class TIFFImageReader extends ImageReaderBase { - - private TIFFImageDecoder mDecoder = null; - private List mImages = new ArrayList(); - - protected TIFFImageReader(final ImageReaderSpi pOriginatingProvider) { - super(pOriginatingProvider); - } - - protected void resetMembers() { - mDecoder = null; - } - - public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException { - // Decode image, convert and return as BufferedImage - RenderedImage image = readAsRenderedImage(pIndex, pParam); - return ImageUtil.toBuffered(image); - } - - public RenderedImage readAsRenderedImage(int pIndex, ImageReadParam pParam) throws IOException { - init(pIndex); - - processImageStarted(pIndex); - - if (pParam == null) { - // Cache image for use by getWidth and getHeight methods - RenderedImage image; - if (mImages.size() > pIndex && mImages.get(pIndex) != null) { - image = mImages.get(pIndex); - } - else { - // Decode - image = mDecoder.decodeAsRenderedImage(pIndex); - - // Make room - for (int i = mImages.size(); i < pIndex; i++) { - mImages.add(pIndex, null); - } - mImages.add(pIndex, image); - } - - if (abortRequested()) { - processReadAborted(); - return image; - } - - processImageComplete(); - return image; - } - else { - // TODO: Parameter conversion - mDecoder.setParam(new TIFFDecodeParam()); - - RenderedImage image = mDecoder.decodeAsRenderedImage(pIndex); - - // Subsample and apply AOI - if (pParam.getSourceRegion() != null) { - image = fakeAOI(ImageUtil.toBuffered(image), pParam); - } - if (pParam.getSourceXSubsampling() > 1 || pParam.getSourceYSubsampling() > 1) { - image = ImageUtil.toBuffered(fakeSubsampling(ImageUtil.toBuffered(image), pParam)); - } - - processImageComplete(); - return image; - } - } - - private void init(int pIndex) throws IOException { - init(); - checkBounds(pIndex); - } - - protected void checkBounds(int pIndex) throws IOException { - if (pIndex < getMinIndex()){ - throw new IndexOutOfBoundsException("index < minIndex"); - } - else if (pIndex >= getNumImages(true)) { - throw new IndexOutOfBoundsException("index > numImages"); - } - } - - private synchronized void init() { - if (mDecoder == null) { - if (mImageInput == null) { - throw new IllegalStateException("input == null"); - } - - mDecoder = new TIFFImageDecoder(new SeekableStream() { - public int read() throws IOException { - return mImageInput.read(); - } - - public int read(final byte[] pBytes, final int pStart, final int pLength) throws IOException { - return mImageInput.read(pBytes, pStart, pLength); - } - - public long getFilePointer() throws IOException { - return mImageInput.getStreamPosition(); - } - - public void seek(final long pPos) throws IOException { - mImageInput.seek(pPos); - } - }, null); - } - } - - public int getWidth(int pIndex) throws IOException { - init(pIndex); - - // TODO: Use cache... - return mDecoder.decodeAsRenderedImage(pIndex).getWidth(); - } - - public int getHeight(int pIndex) throws IOException { - init(pIndex); - - // TODO: Use cache... - return mDecoder.decodeAsRenderedImage(pIndex).getHeight(); - } - - public Iterator getImageTypes(final int imageIndex) throws IOException { - throw new UnsupportedOperationException("Method getImageTypes not implemented");// TODO: Implement - } - - public int getNumImages(boolean pAllowSearch) throws IOException { - init(); - if (pAllowSearch) { - return mDecoder.getNumPages(); - } - return -1; - } -} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java deleted file mode 100755 index f8ae25a1..00000000 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.imageio.plugins.tiff; - -import com.twelvemonkeys.imageio.spi.ProviderInfo; -import com.twelvemonkeys.lang.SystemUtil; -import com.twelvemonkeys.imageio.util.IIOUtil; - -import javax.imageio.ImageReader; -import javax.imageio.spi.ImageReaderSpi; -import javax.imageio.spi.ServiceRegistry; -import javax.imageio.stream.ImageInputStream; -import java.io.IOException; -import java.util.Locale; - -/** - * TIFFImageReaderSpi - *

    - * - * @author Harald Kuhr - * @version $Id: TIFFImageReaderSpi.java,v 1.1 2003/12/02 16:45:00 wmhakur Exp $ - */ -public class TIFFImageReaderSpi extends ImageReaderSpi { - - final static boolean TIFF_CLASSES_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader"); - - /** - * Creates a {@code TIFFImageReaderSpi}. - */ - public TIFFImageReaderSpi() { - this(IIOUtil.getProviderInfo(TIFFImageReaderSpi.class)); - } - - private TIFFImageReaderSpi(final ProviderInfo pProviderInfo) { - super( - pProviderInfo.getVendorName(), // Vendor name - pProviderInfo.getVersion(), // Version - TIFF_CLASSES_AVAILABLE ? new String[]{"tiff", "TIFF"} : new String[] {""}, // Names - TIFF_CLASSES_AVAILABLE ? new String[]{"tiff", "tif"} : null, // Suffixes - TIFF_CLASSES_AVAILABLE ? new String[]{"image/tiff", "image/x-tiff"} : null, // Mime-types - "com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader", // Writer class name..? - ImageReaderSpi.STANDARD_INPUT_TYPE, // Output types - new String[]{"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriterSpi"}, // Writer SPI names - true, // Supports standard stream metadata format - null, // Native stream metadata format name - null, // Native stream metadata format class name - null, // Extra stream metadata format names - null, // Extra stream metadata format class names - true, // Supports standard image metadata format - null, // Native image metadata format name - null, // Native image metadata format class name - null, // Extra image metadata format names - null // Extra image metadata format class names - ); - } - - public boolean canDecodeInput(Object source) throws IOException { - return source instanceof ImageInputStream && TIFF_CLASSES_AVAILABLE && canDecode((ImageInputStream) source); - } - - - static boolean canDecode(ImageInputStream pInput) throws IOException { - try { - pInput.mark(); - int byte0 = pInput.read(); // Byte order 1 (M or I) - int byte1 = pInput.read(); // Byte order 2 (always same as 1) - int byte2 = pInput.read(); // Version number 1 (M: 0, I: 42) - int byte3 = pInput.read(); // Version number 2 (M: 42, I: 0) - - // Test for Motorola or Intel byte order, and version number == 42 - if ((byte0 == 'M' && byte1 == 'M' && byte2 == 0 && byte3 == 42) - || (byte0 == 'I' && byte1 == 'I' && byte2 == 42 && byte3 == 0)) { - return true; - } - - } - finally { - pInput.reset(); - } - - return false; - } - - public ImageReader createReaderInstance(Object extension) throws IOException { - return new TIFFImageReader(this); - } - - public String getDescription(Locale locale) { - return "Tagged Image File Format (TIFF) image reader"; - } - - @SuppressWarnings({"deprecation"}) - @Override - public void onRegistration(ServiceRegistry registry, Class category) { - if (!TIFF_CLASSES_AVAILABLE) { - IIOUtil.deregisterProvider(registry, this, category); - } - } -} \ No newline at end of file diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java deleted file mode 100755 index 212dc2f4..00000000 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.imageio.plugins.tiff; - -import com.twelvemonkeys.image.ImageUtil; -import com.twelvemonkeys.imageio.ImageWriterBase; -import com.twelvemonkeys.imageio.util.IIOUtil; -import org.apache.batik.ext.awt.image.codec.ImageEncodeParam; -import org.apache.batik.ext.awt.image.codec.tiff.TIFFEncodeParam; -import org.apache.batik.ext.awt.image.codec.tiff.TIFFImageEncoder; - -import javax.imageio.IIOImage; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.ImageWriteParam; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.spi.ImageWriterSpi; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; -import java.io.IOException; - -/** - * TIFFImageWriter class description. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: TIFFImageWriter.java,v 1.0 29.jul.2004 12:52:54 haku Exp $ - */ -public class TIFFImageWriter extends ImageWriterBase { - - private TIFFImageEncoder mEncoder = null; - - protected TIFFImageWriter(final ImageWriterSpi pProvider) { - super(pProvider); - } - - @Override - public void setOutput(final Object pOutput) { - mEncoder = null; - super.setOutput(pOutput); - } - - public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) { - throw new UnsupportedOperationException("Method getDefaultImageMetadata not implemented");// TODO: Implement - } - - public IIOMetadata convertImageMetadata(final IIOMetadata inData, final ImageTypeSpecifier imageType, final ImageWriteParam param) { - throw new UnsupportedOperationException("Method convertImageMetadata not implemented");// TODO: Implement - } - - public void write(final IIOMetadata pStreamMetadata, final IIOImage pImage, final ImageWriteParam pParam) throws IOException { - RenderedImage renderedImage = pImage.getRenderedImage(); - init(); - - ImageEncodeParam param; - if (pParam != null) { - param = new TIFFEncodeParam(); - // TODO: Convert params - - mEncoder.setParam(param); - } - - BufferedImage image; - - // FIX: TIFFEnocder chokes on a any of the TYPE_INT_* types... - // (The TIFFEncoder expects int types to have 1 sample of size 32 - // while there actually is 4 samples of size 8, according to the - // SampleModel...) - if (renderedImage instanceof BufferedImage && ( - ((BufferedImage) renderedImage).getType() == BufferedImage.TYPE_INT_ARGB - || ((BufferedImage) renderedImage).getType() == BufferedImage.TYPE_INT_ARGB_PRE)) { - image = ImageUtil.toBuffered(renderedImage, BufferedImage.TYPE_4BYTE_ABGR); - } - else if (renderedImage instanceof BufferedImage && ( - ((BufferedImage) renderedImage).getType() == BufferedImage.TYPE_INT_BGR - || ((BufferedImage) renderedImage).getType() == BufferedImage.TYPE_INT_RGB)) { - image = ImageUtil.toBuffered(renderedImage, BufferedImage.TYPE_3BYTE_BGR); - } - else { - image = ImageUtil.toBuffered(renderedImage); - } - - image = fakeAOI(image, pParam); - image = ImageUtil.toBuffered(fakeSubsampling(image, pParam)); - - /* - System.out.println("Image: " + pImage); - SampleModel sampleModel = pImage.getSampleModel(); - System.out.println("SampleModel: " + sampleModel); - int sampleSize[] = sampleModel.getSampleSize(); - System.out.println("Samples: " + sampleSize.length); - for (int i = 0; i < sampleSize.length; i++) { - System.out.println("SampleSize[" + i + "]: " + sampleSize[i]); - } - int dataType = sampleModel.getDataType(); - System.out.println("DataType: " + dataType); - */ - - processImageStarted(0); - - mEncoder.encode(image); - mImageOutput.flush(); - - processImageComplete(); - } - - public void dispose() { - super.dispose(); - mEncoder = null; - } - - private synchronized void init() { - if (mEncoder == null) { - if (mImageOutput == null) { - throw new IllegalStateException("output == null"); - } - mEncoder = new TIFFImageEncoder(IIOUtil.createStreamAdapter(mImageOutput), null); - } - } -} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java deleted file mode 100755 index 48d69594..00000000 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.imageio.plugins.tiff; - -import com.twelvemonkeys.imageio.spi.ProviderInfo; -import com.twelvemonkeys.imageio.util.IIOUtil; - -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.ImageWriter; -import javax.imageio.spi.ImageWriterSpi; -import javax.imageio.spi.ServiceRegistry; -import java.io.IOException; -import java.util.Locale; - -/** - * TIFFmageWriterSpi - * - * @author Harald Kuhr - * @version $Id: TIFFImageWriterSpi.java,v 1.2 2004/01/14 15:21:44 wmhakur Exp $ - */ -public class TIFFImageWriterSpi extends ImageWriterSpi { - - /** - * Creates a {@code TIFFImageWriterSpi}. - */ - public TIFFImageWriterSpi() { - this(IIOUtil.getProviderInfo(TIFFImageWriterSpi.class)); - } - - private TIFFImageWriterSpi(final ProviderInfo pProviderInfo) { - super( - pProviderInfo.getVendorName(), // Vendor name - pProviderInfo.getVersion(), // Version - new String[]{"tiff", "TIFF"}, // Names - new String[]{"tif", "tiff"}, // Suffixes - new String[]{"image/tiff", "image/x-tiff"}, // Mime-types - "com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter", // Writer class name..? - STANDARD_OUTPUT_TYPE, // Output types - new String[]{"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi"}, // Reader SPI names - true, // Supports standard stream metadata format - null, // Native stream metadata format name - null, // Native stream metadata format class name - null, // Extra stream metadata format names - null, // Extra stream metadata format class names - true, // Supports standard image metadata format - null, // Native image metadata format name - null, // Native image metadata format class name - null, // Extra image metadata format names - null // Extra image metadata format class names - ); - } - - public boolean canEncodeImage(ImageTypeSpecifier type) { - return true; - } - - public ImageWriter createWriterInstance(Object extension) throws IOException { - try { - return new TIFFImageWriter(this); - } - catch (Throwable t) { - // Wrap in IOException if the writer can't be instantiated. - // This makes the IIORegistry deregister this service provider - IOException exception = new IOException(t.getMessage()); - exception.initCause(t); - throw exception; - } - } - - public String getDescription(Locale locale) { - return "Tagged Image File Format (TIFF) image writer"; - } - - @SuppressWarnings({"deprecation"}) - @Override - public void onRegistration(ServiceRegistry registry, Class category) { - if (!TIFFImageReaderSpi.TIFF_CLASSES_AVAILABLE) { - IIOUtil.deregisterProvider(registry, this, category); - } - } -} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReader.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReader.java index 9f78ed5c..5391a2a1 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReader.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReader.java @@ -57,17 +57,18 @@ import java.util.Iterator; // TODO: Consider using temp file instead of in-memory stream public class WMFImageReader extends ImageReaderBase { - private SVGImageReader mReader = null; + private SVGImageReader reader = null; public WMFImageReader(final ImageReaderSpi pProvider) { super(pProvider); } protected void resetMembers() { - if (mReader != null) { - mReader.dispose(); + if (reader != null) { + reader.dispose(); } - mReader = null; + + reader = null; } public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException { @@ -75,11 +76,12 @@ public class WMFImageReader extends ImageReaderBase { processImageStarted(pIndex); - BufferedImage image = mReader.read(pIndex, pParam); + BufferedImage image = reader.read(pIndex, pParam); if (abortRequested()) { processReadAborted(); return image; } + processImageProgress(100f); processImageComplete(); @@ -88,17 +90,17 @@ public class WMFImageReader extends ImageReaderBase { private synchronized void init() throws IOException { // Need the extra test, to avoid throwing an IOException from the Transcoder - if (mImageInput == null) { + if (imageInput == null) { throw new IllegalStateException("input == null"); } - if (mReader == null) { + if (reader == null) { WMFTranscoder transcoder = new WMFTranscoder(); ByteArrayOutputStream output = new ByteArrayOutputStream(); Writer writer = new OutputStreamWriter(output, "UTF8"); try { - TranscoderInput in = new TranscoderInput(IIOUtil.createStreamAdapter(mImageInput)); + TranscoderInput in = new TranscoderInput(IIOUtil.createStreamAdapter(imageInput)); TranscoderOutput out = new TranscoderOutput(writer); // TODO: Transcodinghints? @@ -109,8 +111,8 @@ public class WMFImageReader extends ImageReaderBase { throw new IIOException(e.getMessage(), e); } - mReader = new SVGImageReader(getOriginatingProvider()); - mReader.setInput(ImageIO.createImageInputStream(new ByteArrayInputStream(output.toByteArray()))); + reader = new SVGImageReader(getOriginatingProvider()); + reader.setInput(ImageIO.createImageInputStream(new ByteArrayInputStream(output.toByteArray()))); } } @@ -121,17 +123,17 @@ public class WMFImageReader extends ImageReaderBase { public int getWidth(int pIndex) throws IOException { init(); - return mReader.getWidth(pIndex); + return reader.getWidth(pIndex); } public int getHeight(int pIndex) throws IOException { init(); - return mReader.getHeight(pIndex); + return reader.getHeight(pIndex); } public Iterator getImageTypes(final int pImageIndex) throws IOException { init(); - return mReader.getImageTypes(pImageIndex); + return reader.getImageTypes(pImageIndex); } } diff --git a/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi index 42f4f345..6ce07b66 100755 --- a/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi +++ b/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -1,3 +1,2 @@ com.twelvemonkeys.imageio.plugins.svg.SVGImageReaderSpi com.twelvemonkeys.imageio.plugins.wmf.WMFImageReaderSpi -#com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi diff --git a/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi deleted file mode 100755 index 54dbaa61..00000000 --- a/imageio/imageio-batik/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi +++ /dev/null @@ -1 +0,0 @@ -#com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriterSpi \ No newline at end of file diff --git a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java index 6821bc33..a3772238 100755 --- a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java +++ b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTestCase.java @@ -43,7 +43,7 @@ import java.util.List; * @version $Id: SVGImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ public class SVGImageReaderTestCase extends ImageReaderAbstractTestCase { - private SVGImageReaderSpi mSVGImageReaderSpi = new SVGImageReaderSpi(); + private SVGImageReaderSpi provider = new SVGImageReaderSpi(); protected List getTestData() { return Arrays.asList( @@ -52,7 +52,7 @@ public class SVGImageReaderTestCase extends ImageReaderAbstractTestCase { - private WMFImageReaderSpi mSVGImageReaderSpi = new WMFImageReaderSpi(); + private WMFImageReaderSpi provider = new WMFImageReaderSpi(); protected List getTestData() { return Arrays.asList( @@ -53,7 +53,7 @@ public class WMFImageReaderTestCase extends ImageReaderAbstractTestCase= getNumImages(false)) { - throw new IndexOutOfBoundsException("index >= numImages (" + pIndex + " >= " + getNumImages(false) + ")"); + else if (getNumImages(false) != -1 && index >= getNumImages(false)) { + throw new IndexOutOfBoundsException("index >= numImages (" + index + " >= " + getNumImages(false) + ")"); } } @@ -192,50 +196,56 @@ public abstract class ImageReaderBase extends ImageReader { } /** - * Returns the {@code BufferedImage} to which decoded pixel - * data should be written. + * Returns the {@code BufferedImage} to which decoded pixel data should be written. *

    * As {@link javax.imageio.ImageReader#getDestination} but tests if the explicit destination - * image (if set) is valid according to the {@code ImageTypeSpecifier}s given in {@code pTypes} + * image (if set) is valid according to the {@code ImageTypeSpecifier}s given in {@code types}. * - * - * @param pParam an {@code ImageReadParam} to be used to get + * @param param an {@code ImageReadParam} to be used to get * the destination image or image type, or {@code null}. - * @param pTypes an {@code Iterator} of + * @param types an {@code Iterator} of * {@code ImageTypeSpecifier}s indicating the legal image * types, with the default first. - * @param pWidth the true width of the image or tile begin decoded. - * @param pHeight the true width of the image or tile being decoded. + * @param width the true width of the image or tile begin decoded. + * @param height the true width of the image or tile being decoded. * * @return the {@code BufferedImage} to which decoded pixel * data should be written. * - * @exception IIOException if the {@code ImageTypeSpecifier} or {@code BufferedImage} - * specified by {@code pParam} does not match any of the legal - * ones from {@code pTypes}. - * @throws IllegalArgumentException if {@code pTypes} + * @exception javax.imageio.IIOException if the {@code ImageTypeSpecifier} or {@code BufferedImage} + * specified by {@code param} does not match any of the legal + * ones from {@code types}. + * @throws IllegalArgumentException if {@code types} * is {@code null} or empty, or if an object not of type * {@code ImageTypeSpecifier} is retrieved from it. * Or, if the resulting image would have a width or height less than 1, - * or if the product of {@code pWidth} and {@code pHeight} is greater than + * or if the product of {@code width} and {@code height} of the resulting image is greater than * {@code Integer.MAX_VALUE}. */ - public static BufferedImage getDestination(final ImageReadParam pParam, final Iterator pTypes, - final int pWidth, final int pHeight) throws IIOException { - BufferedImage image = ImageReader.getDestination(pParam, pTypes, pWidth, pHeight); + public static BufferedImage getDestination(final ImageReadParam param, final Iterator types, + final int width, final int height) throws IIOException { + // Adapted from http://java.net/jira/secure/attachment/29712/TIFFImageReader.java.patch, + // to allow reading parts/tiles of huge images. + + if (types == null || !types.hasNext()) { + throw new IllegalArgumentException("imageTypes null or empty!"); + } + + ImageTypeSpecifier imageType = null; + + // If param is non-null, use it + if (param != null) { + // Try to get the explicit destinaton image + BufferedImage dest = param.getDestination(); - if (pParam != null) { - BufferedImage dest = pParam.getDestination(); if (dest != null) { boolean found = false; - // NOTE: This is bad, as it relies on implementation details of super method... - // We know that the iterator has not been touched if explicit destination.. - while (pTypes.hasNext()) { - ImageTypeSpecifier specifier = pTypes.next(); - int imageType = specifier.getBufferedImageType(); + while (types.hasNext()) { + ImageTypeSpecifier specifier = types.next(); + int bufferedImageType = specifier.getBufferedImageType(); - if (imageType != 0 && imageType == dest.getType()) { + if (bufferedImageType != 0 && bufferedImageType == dest.getType()) { // Known types equal, perfect match found = true; break; @@ -254,12 +264,50 @@ public abstract class ImageReaderBase extends ImageReader { } if (!found) { - throw new IIOException(String.format("Illegal explicit destination image %s", dest)); + throw new IIOException(String.format("Destination image from ImageReadParam does not match legal imageTypes from reader: %s", dest)); } + + return dest; + } + + // No image, get the image type + imageType = param.getDestinationType(); + } + + // No info from param, use fallback image type + if (imageType == null) { + imageType = types.next(); + } + else { + boolean foundIt = false; + + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + + if (type.equals(imageType)) { + foundIt = true; + break; + } + } + + if (!foundIt) { + throw new IIOException(String.format("Destination type from ImageReadParam does not match legal imageTypes from reader: %s", imageType)); } } - return image; + Rectangle srcRegion = new Rectangle(0, 0, 0, 0); + Rectangle destRegion = new Rectangle(0, 0, 0, 0); + computeRegions(param, width, height, null, srcRegion, destRegion); + + int destWidth = destRegion.x + destRegion.width; + int destHeight = destRegion.y + destRegion.height; + + if ((long) destWidth * destHeight > Integer.MAX_VALUE) { + throw new IllegalArgumentException(String.format("destination width * height > Integer.MAX_VALUE: %d", (long) destWidth * destHeight)); + } + + // Create a new image based on the type specifier + return imageType.createBufferedImage(destWidth, destHeight); } /** @@ -304,10 +352,26 @@ public abstract class ImageReaderBase extends ImageReader { return IIOUtil.fakeSubsampling(pImage, pParam); } + /** + * Tests if param has explicit destination. + * + * @param pParam the image read parameter, or {@code null} + * @return true if {@code pParam} is non-{@code null} and either its {@code getDestination}, + * {@code getDestinationType} returns a non-{@code null} value, + * or {@code getDestinationOffset} returns a {@link Point} that is not the upper left corner {@code (0, 0)}. + */ + protected static boolean hasExplicitDestination(final ImageReadParam pParam) { + return pParam != null && + ( + pParam.getDestination() != null || pParam.getDestinationType() != null || + !ORIGIN.equals(pParam.getDestinationOffset()) + ); + } + public static void main(String[] pArgs) throws IOException { BufferedImage image = ImageIO.read(new File(pArgs[0])); if (image == null) { - System.err.println("Supported formats: " + Arrays.toString(ImageIO.getReaderFormatNames())); + System.err.println("Supported formats: " + Arrays.toString(IIOUtil.getNormalizedReaderFormatNames())); System.exit(1); } showIt(image, pArgs[0]); @@ -318,10 +382,21 @@ public abstract class ImageReaderBase extends ImageReader { SwingUtilities.invokeAndWait(new Runnable() { public void run() { JFrame frame = new JFrame(pTitle); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + frame.getRootPane().getActionMap().put("window-close", new AbstractAction() { + public void actionPerformed(ActionEvent e) { + Window window = SwingUtilities.getWindowAncestor((Component) e.getSource()); + window.setVisible(false); + window.dispose(); + } + }); + frame.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "window-close"); + frame.addWindowListener(new ExitIfNoWindowPresentHandler()); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.setLocationByPlatform(true); JPanel pane = new JPanel(new BorderLayout()); - JScrollPane scroll = new JScrollPane(new ImageLabel(pImage)); + JScrollPane scroll = new JScrollPane(pImage != null ? new ImageLabel(pImage) : new JLabel("(no image data)", JLabel.CENTER)); scroll.setBorder(null); pane.add(scroll); frame.setContentPane(pane); @@ -334,27 +409,31 @@ public abstract class ImageReaderBase extends ImageReader { Thread.currentThread().interrupt(); } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw new RuntimeException(e); } } private static class ImageLabel extends JLabel { - Paint mBackground; + Paint backgroundPaint; - final Paint mCheckeredBG; - final Color mDefaultBG; + final Paint checkeredBG; + final Color defaultBG; public ImageLabel(final BufferedImage pImage) { super(new BufferedImageIcon(pImage)); setOpaque(false); setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); - mCheckeredBG = createTexture(); + checkeredBG = createTexture(); // For indexed color, default to the color of the transparent pixel, if any - mDefaultBG = getDefaultBackground(pImage); + defaultBG = getDefaultBackground(pImage); - mBackground = mDefaultBG != null ? mDefaultBG : mCheckeredBG; + backgroundPaint = defaultBG != null ? defaultBG : checkeredBG; JPopupMenu popup = createBackgroundPopup(); @@ -373,7 +452,7 @@ public abstract class ImageReaderBase extends ImageReader { JPopupMenu popup = new JPopupMenu(); ButtonGroup group = new ButtonGroup(); - addCheckBoxItem(new ChangeBackgroundAction("Checkered", mCheckeredBG), popup, group); + addCheckBoxItem(new ChangeBackgroundAction("Checkered", checkeredBG), popup, group); popup.addSeparator(); addCheckBoxItem(new ChangeBackgroundAction("White", Color.WHITE), popup, group); addCheckBoxItem(new ChangeBackgroundAction("Light", Color.LIGHT_GRAY), popup, group); @@ -381,7 +460,7 @@ public abstract class ImageReaderBase extends ImageReader { addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), popup, group); addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), popup, group); popup.addSeparator(); - addCheckBoxItem(new ChooseBackgroundAction("Choose...", mDefaultBG != null ? mDefaultBG : Color.BLUE), popup, group); + addCheckBoxItem(new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : Color.BLUE), popup, group); return popup; } @@ -425,21 +504,21 @@ public abstract class ImageReaderBase extends ImageReader { @Override protected void paintComponent(Graphics g) { Graphics2D gr = (Graphics2D) g; - gr.setPaint(mBackground); + gr.setPaint(backgroundPaint); gr.fillRect(0, 0, getWidth(), getHeight()); super.paintComponent(g); } private class ChangeBackgroundAction extends AbstractAction { - protected Paint mPaint; + protected Paint paint; public ChangeBackgroundAction(final String pName, final Paint pPaint) { super(pName); - mPaint = pPaint; + paint = pPaint; } public void actionPerformed(ActionEvent e) { - mBackground = mPaint; + backgroundPaint = paint; repaint(); } } @@ -450,7 +529,7 @@ public abstract class ImageReaderBase extends ImageReader { putValue(Action.SMALL_ICON, new Icon() { public void paintIcon(Component c, Graphics pGraphics, int x, int y) { Graphics g = pGraphics.create(); - g.setColor((Color) mPaint); + g.setColor((Color) paint); g.fillRect(x, y, 16, 16); g.dispose(); } @@ -467,12 +546,23 @@ public abstract class ImageReaderBase extends ImageReader { @Override public void actionPerformed(ActionEvent e) { - Color selected = JColorChooser.showDialog(ImageLabel.this, "Choose background", (Color) mPaint); + Color selected = JColorChooser.showDialog(ImageLabel.this, "Choose background", (Color) paint); if (selected != null) { - mPaint = selected; + paint = selected; super.actionPerformed(e); } } } } + + private static class ExitIfNoWindowPresentHandler extends WindowAdapter { + @Override + public void windowClosed(final WindowEvent e) { + Window[] windows = Window.getWindows(); + + if (windows == null || windows.length == 0) { + System.exit(0); + } + } + } } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageWriterBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageWriterBase.java index 8c60c2d2..f6239068 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageWriterBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageWriterBase.java @@ -52,7 +52,7 @@ public abstract class ImageWriterBase extends ImageWriter { * For convenience. Only set if the output is an {@code ImageInputStream}. * @see #setOutput(Object) */ - protected ImageOutputStream mImageOutput; + protected ImageOutputStream imageOutput; /** * Constructs an {@code ImageWriter} and sets its @@ -65,10 +65,10 @@ public abstract class ImageWriterBase extends ImageWriter { * the extension object is unsuitable, an * {@code IllegalArgumentException} should be thrown. * - * @param pProvider the {@code ImageWriterSpi} that is constructing this object, or {@code null}. + * @param provider the {@code ImageWriterSpi} that is constructing this object, or {@code null}. */ - protected ImageWriterBase(final ImageWriterSpi pProvider) { - super(pProvider); + protected ImageWriterBase(final ImageWriterSpi provider) { + super(provider); } public String getFormatName() throws IOException { @@ -76,11 +76,15 @@ public abstract class ImageWriterBase extends ImageWriter { } @Override - public void setOutput(final Object pOutput) { - super.setOutput(pOutput); + public void setOutput(final Object output) { + resetMembers(); + super.setOutput(output); - if (pOutput instanceof ImageOutputStream) { - mImageOutput = (ImageOutputStream) pOutput; + if (output instanceof ImageOutputStream) { + imageOutput = (ImageOutputStream) output; + } + else { + imageOutput = null; } } @@ -95,24 +99,33 @@ public abstract class ImageWriterBase extends ImageWriter { } } + @Override + public void reset() { + super.reset(); + resetMembers(); + } + + protected void resetMembers() { + } + /** * Returns {@code null} * - * @param pParam ignored. + * @param param ignored. * @return {@code null}. */ - public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam pParam) { + public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam param) { return null; } /** * Returns {@code null} * - * @param pInData ignored. - * @param pParam ignored. + * @param inData ignored. + * @param param ignored. * @return {@code null}. */ - public IIOMetadata convertStreamMetadata(final IIOMetadata pInData, final ImageWriteParam pParam) { + public IIOMetadata convertStreamMetadata(final IIOMetadata inData, final ImageWriteParam param) { return null; } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/CMYKColorSpace.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CMYKColorSpace.java similarity index 61% rename from imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/CMYKColorSpace.java rename to imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CMYKColorSpace.java index 54715827..2223ec77 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/CMYKColorSpace.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CMYKColorSpace.java @@ -1,17 +1,17 @@ /* - * Copyright (c) 2008, Harald Kuhr + * Copyright (c) 2011, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT @@ -26,25 +26,24 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.plugins.psd; +package com.twelvemonkeys.imageio.color; import java.awt.color.ColorSpace; /** - * CMYKColorSpace + * A fallback CMYK ColorSpace, in case none can be read from disk. * * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: CMYKColorSpace.java,v 1.0 Apr 30, 2008 1:38:13 PM haraldk Exp$ */ -// TODO: Move to com.twelvemonkeys.image? -// TODO: Read a ICC CMYK profile from classpath resource (from ECI)? ISO coated? final class CMYKColorSpace extends ColorSpace { static final ColorSpace INSTANCE = new CMYKColorSpace(); + final ColorSpace sRGB = getInstance(CS_sRGB); - CMYKColorSpace() { + private CMYKColorSpace() { super(ColorSpace.TYPE_CMYK, 4); } @@ -58,6 +57,7 @@ final class CMYKColorSpace extends ColorSpace { (1 - colorvalue[1]) * (1 - colorvalue[3]), (1 - colorvalue[2]) * (1 - colorvalue[3]) }; + // TODO: Convert via CIEXYZ space using sRGB space, as suggested in docs // return sRGB.fromCIEXYZ(toCIEXYZ(colorvalue)); } @@ -73,33 +73,35 @@ final class CMYKColorSpace extends ColorSpace { // Convert to CMYK values return new float[] {(c - k), (m - k), (y - k), k}; -/* -http://www.velocityreviews.com/forums/t127265-rgb-to-cmyk.html -(Step 0: Normalize R,G, and B values to fit into range [0.0 ... 1.0], or -adapt the following matrix.) + /* + http://www.velocityreviews.com/forums/t127265-rgb-to-cmyk.html -Step 1: RGB to CMY + (Step 0: Normalize R,G, and B values to fit into range [0.0 ... 1.0], or + adapt the following matrix.) -| C | | 1 | | R | -| M | = | 1 | - | G | -| Y | | 1 | | B | + Step 1: RGB to CMY -Step 2: CMY to CMYK + | C | | 1 | | R | + | M | = | 1 | - | G | + | Y | | 1 | | B | -| C' | | C | | min(C,M,Y) | -| M' | | M | | min(C,M,Y) | -| Y' | = | Y | - | min(C,M,Y) | -| K' | | min(C,M,Y) | | 0 | + Step 2: CMY to CMYK -Easier to calculate if K' is calculated first, because K' = min(C,M,Y): + | C' | | C | | min(C,M,Y) | + | M' | | M | | min(C,M,Y) | + | Y' | = | Y | - | min(C,M,Y) | + | K' | | min(C,M,Y) | | 0 | -| C' | | C | | K' | -| M' | | M | | K' | -| Y' | = | Y | - | K' | -| K' | | K'| | 0 | - */ -// return fromCIEXYZ(sRGB.toCIEXYZ(rgbvalue)); + Easier to calculate if K' is calculated first, because K' = min(C,M,Y): + + | C' | | C | | K' | + | M' | | M | | K' | + | Y' | = | Y | - | K' | + | K' | | K'| | 0 | + */ + + // return fromCIEXYZ(sRGB.toCIEXYZ(rgbvalue)); } public float[] toCIEXYZ(float[] colorvalue) { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java new file mode 100644 index 00000000..13fabe81 --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.color; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.Platform; +import com.twelvemonkeys.lang.SystemUtil; +import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.util.LRUHashMap; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; + +/** + * A helper class for working with ICC color profiles and color spaces. + *

    + * Standard ICC color profiles are read from system-specific locations + * for known operating systems. + *

    + * Color profiles may be configured by placing a property-file + * {@code com/twelvemonkeys/imageio/color/icc_profiles.properties} + * on the classpath, specifying the full path to the profile. + * ICC color profiles are probably already present on your system, or + * can be downloaded from + * ICC, + * Adobe or other places. + *

    + * Example property file: + *

    + * # icc_profiles.properties
    + * ADOBE_RGB_1998=/path/to/Adobe RGB 1998.icc
    + * GENERIC_CMYK=/path/to/Generic CMYK.icc
    + * 
    + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ColorSpaces.java,v 1.0 24.01.11 17.51 haraldk Exp$ + */ +public final class ColorSpaces { + + private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.color.debug")); + + // JDK 7 seems to handle non-perceptual rendering intents gracefully, so we don't need to fiddle with the profiles + private final static boolean JDK_HANDLES_RENDERING_INTENTS = SystemUtil.isClassAvailable("java.lang.invoke.CallSite"); + + // NOTE: java.awt.color.ColorSpace.CS_* uses 1000-1004, we'll use 5000+ to not interfere with future additions + + /** The Adobe RGB 1998 (or compatible) color space. Either read from disk or built-in. */ + public static final int CS_ADOBE_RGB_1998 = 5000; + + /** A best-effort "generic" CMYK color space. Either read from disk or built-in. */ + public static final int CS_GENERIC_CMYK = 5001; + + // Weak references to hold the color spaces while cached + private static WeakReference adobeRGB1998 = new WeakReference(null); + private static WeakReference genericCMYK = new WeakReference(null); + + // Cache for the latest used color spaces + private static final Map cache = new LRUHashMap(10); + + private ColorSpaces() {} + + /** + * Creates an ICC color space from the given ICC color profile. + *

    + * For standard Java color spaces, the built-in instance is returned. + * Otherwise, color spaces are looked up from cache and created on demand. + * + * @param profile the ICC color profile. May not be {@code null}. + * @return an ICC color space + * @throws IllegalArgumentException if {@code profile} is {@code null} + */ + public static ICC_ColorSpace createColorSpace(final ICC_Profile profile) { + Validate.notNull(profile, "profile"); + + byte[] profileHeader = profile.getData(ICC_Profile.icSigHead); + + ICC_ColorSpace cs = getInternalCS(profile.getColorSpaceType(), profileHeader); + if (cs != null) { + return cs; + } + + // Special case for color profiles with rendering intent != 0, see isOffendingColorProfile method + // NOTE: Rendering intent is really a 4 byte value, but legal values are 0-3 (ICC1v42_2006_05_1.pdf, 7.2.15, p. 19) + if (profileHeader[ICC_Profile.icHdrRenderingIntent] != 0) { + profileHeader[ICC_Profile.icHdrRenderingIntent] = 0; + + // Test again if this is an internal CS + cs = getInternalCS(profile.getColorSpaceType(), profileHeader); + if (cs != null) { + return cs; + } + + // NOTE: The intent change breaks JDK7: Seems to be a bug in ICC_Profile.getData/setData, + // as calling it with unchanged header data, still breaks when creating new ICC_ColorSpace... + // However, we simply skip that, as JDK7 handles the rendering intents already. + if (!JDK_HANDLES_RENDERING_INTENTS) { + // Fix profile before lookup/create + profile.setData(ICC_Profile.icSigHead, profileHeader); + } + } + + return getCachedOrCreateCS(profile, profileHeader); + } + + private static ICC_ColorSpace getInternalCS(final int profileCSType, final byte[] profileHeader) { + if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, sRGB.header)) { + return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_sRGB); + } + else if (profileCSType == ColorSpace.TYPE_GRAY && Arrays.equals(profileHeader, GRAY.header)) { + return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_GRAY); + } + else if (profileCSType == ColorSpace.TYPE_3CLR && Arrays.equals(profileHeader, PYCC.header)) { + return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_PYCC); + } + else if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, LINEAR_RGB.header)) { + return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB); + } + else if (profileCSType == ColorSpace.TYPE_XYZ && Arrays.equals(profileHeader, CIEXYZ.header)) { + return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_CIEXYZ); + } + + return null; + } + + private static ICC_ColorSpace getCachedOrCreateCS(final ICC_Profile profile, final byte[] profileHeader) { + Key key = new Key(profileHeader); + + synchronized (cache) { + ICC_ColorSpace cs = cache.get(key); + + if (cs == null) { + cs = new ICC_ColorSpace(profile); + cache.put(key, cs); + } + + return cs; + } + } + + /** + * Tests whether an ICC color profile is known to cause problems for {@link java.awt.image.ColorConvertOp}. + *

    + * + * Note that this method only tests if a color conversion using this profile is known to fail. + * There's no guarantee that the color conversion will succeed even if this method returns {@code false}. + * + * + * @param profile the ICC color profile. May not be {@code null}. + * @return {@code true} if known to be offending, {@code false} otherwise + * @throws IllegalArgumentException if {@code profile} is {@code null} + */ + public static boolean isOffendingColorProfile(final ICC_Profile profile) { + Validate.notNull(profile, "profile"); + + // NOTE: + // Several embedded ICC color profiles are non-compliant with Java pre JDK7 and throws CMMException + // The problem with these embedded ICC profiles seems to be the rendering intent + // being 1 (01000000) - "Media Relative Colormetric" in the offending profiles, + // and 0 (00000000) - "Perceptual" in the good profiles + // (that is 1 single bit of difference right there.. ;-) + + // This is particularly annoying, as the byte copying isn't really necessary, + // except the getRenderingIntent method is package protected in java.awt.color + byte[] data = profile.getData(ICC_Profile.icSigHead); + + return data[ICC_Profile.icHdrRenderingIntent] != 0; + } + + /** + * Returns the color space specified by the given color space constant. + *

    + * For standard Java color spaces, the built-in instance is returned. + * Otherwise, color spaces are looked up from cache and created on demand. + * + * @param colorSpace the color space constant. + * @return the {@link ColorSpace} specified by the color space constant. + * @throws IllegalArgumentException if {@code colorSpace} is not one of the defined color spaces ({@code CS_*}). + * @see ColorSpace + * @see ColorSpaces#CS_ADOBE_RGB_1998 + * @see ColorSpaces#CS_GENERIC_CMYK + */ + public static ColorSpace getColorSpace(int colorSpace) { + ICC_Profile profile; + + switch (colorSpace) { + case CS_ADOBE_RGB_1998: + synchronized (ColorSpaces.class) { + profile = adobeRGB1998.get(); + + if (profile == null) { + // Try to get system default or user-defined profile + profile = readProfileFromPath(Profiles.getPath("ADOBE_RGB_1998")); + + if (profile == null) { + // Fall back to the bundled ClayRGB1998 public domain Adobe RGB 1998 compatible profile, + // identical for all practical purposes + profile = readProfileFromClasspathResource("/profiles/ClayRGB1998.icc"); + + if (profile == null) { + // Should never happen given we now bundle fallback profile... + throw new IllegalStateException("Could not read AdobeRGB1998 profile"); + } + } + + adobeRGB1998 = new WeakReference(profile); + } + } + + return createColorSpace(profile); + + case CS_GENERIC_CMYK: + synchronized (ColorSpaces.class) { + profile = genericCMYK.get(); + + if (profile == null) { + // Try to get system default or user-defined profile + profile = readProfileFromPath(Profiles.getPath("GENERIC_CMYK")); + + if (profile == null) { + if (DEBUG) { + System.out.println("Using fallback profile"); + } + + // Fall back to generic CMYK ColorSpace, which is *insanely slow* using ColorConvertOp... :-P + return CMYKColorSpace.getInstance(); + } + + genericCMYK = new WeakReference(profile); + } + } + + return createColorSpace(profile); + + default: + // Default cases for convenience + return ColorSpace.getInstance(colorSpace); + } + } + + private static ICC_Profile readProfileFromClasspathResource(final String profilePath) { + InputStream stream = ColorSpaces.class.getResourceAsStream(profilePath); + + if (stream != null) { + if (DEBUG) { + System.out.println("Loading profile from classpath resource: " + profilePath); + } + + try { + + return ICC_Profile.getInstance(stream); + } + catch (IOException ignore) { + if (DEBUG) { + ignore.printStackTrace(); + } + } + finally { + FileUtil.close(stream); + } + } + + return null; + } + + private static ICC_Profile readProfileFromPath(final String profilePath) { + if (profilePath != null) { + if (DEBUG) { + System.out.println("Loading profile from: " + profilePath); + } + + try { + return ICC_Profile.getInstance(profilePath); + } + catch (IOException ignore) { + if (DEBUG) { + ignore.printStackTrace(); + } + } + } + + return null; + } + + private static final class Key { + private final byte[] data; + + public Key(byte[] data) { + this.data = data; + } + + @Override + public boolean equals(Object other) { + return other instanceof Key && Arrays.equals(data, ((Key) other).data); + } + + @Override + public int hashCode() { + return Arrays.hashCode(data); + } + } + + // Cache header profile data to avoid excessive array creation/copying in static inner class for on-demand lazy init + private static class sRGB { + private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(ICC_Profile.icSigHead); + } + private static class CIEXYZ { + private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ).getData(ICC_Profile.icSigHead); + } + private static class PYCC { + private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_PYCC).getData(ICC_Profile.icSigHead); + } + private static class GRAY { + private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_GRAY).getData(ICC_Profile.icSigHead); + } + private static class LINEAR_RGB { + private static final byte[] header = ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB).getData(ICC_Profile.icSigHead); + } + + private static class Profiles { + private static final Properties PROFILES = loadProfiles(Platform.os()); + + private static Properties loadProfiles(final Platform.OperatingSystem os) { + Properties systemDefaults; + try { + systemDefaults = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles_" + os.id()); + } + catch (IOException ignore) { + ignore.printStackTrace(); + systemDefaults = null; + } + + // Create map with defaults and add user overrides if any + Properties profiles = new Properties(systemDefaults); + + try { + Properties userOverrides = SystemUtil.loadProperties(ColorSpaces.class, "com/twelvemonkeys/imageio/color/icc_profiles"); + profiles.putAll(userOverrides); + } + catch (IOException ignore) { + } + + if (DEBUG) { + System.out.println("User ICC profiles: " + profiles); + System.out.println("System ICC profiles : " + systemDefaults); + } + + return profiles; + } + + public static String getPath(final String profileName) { + return PROFILES.getProperty(profileName); + } + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java index f9deb312..f78bbbe5 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/spi/ProviderInfo.java @@ -16,9 +16,9 @@ public class ProviderInfo { // TODO: Consider reading the META-INF/MANIFEST.MF from the class path using java.util.jar.Manifest. // Use the manifest that is located in the same class path folder as the package. - private final String mTitle; - private final String mVendorName; - private final String mVersion; + private final String title; + private final String vendorName; + private final String version; /** * Creates a provider information instance based on the given package. @@ -32,13 +32,13 @@ public class ProviderInfo { Validate.notNull(pPackage, "package"); String title = pPackage.getImplementationTitle(); - mTitle = title != null ? title : pPackage.getName(); + this.title = title != null ? title : pPackage.getName(); String vendor = pPackage.getImplementationVendor(); - mVendorName = vendor != null ? vendor : fakeVendor(pPackage); + vendorName = vendor != null ? vendor : fakeVendor(pPackage); String version = pPackage.getImplementationVersion(); - mVersion = version != null ? version : fakeVersion(pPackage); + this.version = version != null ? version : fakeVersion(pPackage); } private static String fakeVendor(final Package pPackage) { @@ -60,7 +60,7 @@ public class ProviderInfo { * @return the implementation title */ final String getImplementationTitle() { - return mTitle; + return title; } /** @@ -72,7 +72,7 @@ public class ProviderInfo { * @return the vendor name. */ public final String getVendorName() { - return mVendorName; + return vendorName; } /** @@ -83,11 +83,11 @@ public class ProviderInfo { * @return the vendor name. */ public final String getVersion() { - return mVersion; + return version; } @Override public String toString() { - return mTitle + ", " + mVersion + " by " + mVendorName; + return title + ", " + version + " by " + vendorName; } } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java index 9c75baa2..221a22ba 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java @@ -23,12 +23,12 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme static final int DEFAULT_BUFFER_SIZE = 8192; - private ImageInputStream mStream; + private ImageInputStream stream; - private byte[] mBuffer; - private long mBufferStart = 0; - private int mBufferPos = 0; - private int mBufferLength = 0; + private byte[] buffer; + private long bufferStart = 0; + private int bufferPos = 0; + private int bufferLength = 0; public BufferedImageInputStream(final ImageInputStream pStream) throws IOException { this(pStream, DEFAULT_BUFFER_SIZE); @@ -37,19 +37,19 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme private BufferedImageInputStream(final ImageInputStream pStream, final int pBufferSize) throws IOException { Validate.notNull(pStream, "stream"); - mStream = pStream; + stream = pStream; streamPos = pStream.getStreamPosition(); - mBuffer = new byte[pBufferSize]; + buffer = new byte[pBufferSize]; } private void fillBuffer() throws IOException { - mBufferStart = streamPos; - mBufferLength = mStream.read(mBuffer, 0, mBuffer.length); - mBufferPos = 0; + bufferStart = streamPos; + bufferLength = stream.read(buffer, 0, buffer.length); + bufferPos = 0; } private boolean isBufferValid() throws IOException { - return mBufferPos < mBufferLength && mBufferStart == mStream.getStreamPosition() - mBufferLength; + return bufferPos < bufferLength && bufferStart == stream.getStreamPosition() - bufferLength; } @Override @@ -58,14 +58,14 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme fillBuffer(); } - if (mBufferLength <= 0) { + if (bufferLength <= 0) { return -1; } bitOffset = 0; streamPos++; - return mBuffer[mBufferPos++] & 0xff; + return buffer[bufferPos++] & 0xff; } @Override @@ -74,7 +74,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme if (!isBufferValid()) { // Bypass cache if cache is empty for reads longer than buffer - if (pLength >= mBuffer.length) { + if (pLength >= buffer.length) { return readDirect(pBuffer, pOffset, pLength); } else { @@ -87,30 +87,30 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme private int readDirect(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { // TODO: Figure out why reading more than the buffer length causes alignment issues... - int read = mStream.read(pBuffer, pOffset, Math.min(mBuffer.length, pLength)); + int read = stream.read(pBuffer, pOffset, Math.min(buffer.length, pLength)); if (read > 0) { streamPos += read; } - mBufferStart = mStream.getStreamPosition(); - mBufferLength = 0; + bufferStart = stream.getStreamPosition(); + bufferLength = 0; return read; } private int readBuffered(final byte[] pBuffer, final int pOffset, final int pLength) { - if (mBufferLength <= 0) { + if (bufferLength <= 0) { return -1; } // Read as much as possible from buffer - int length = Math.min(mBufferLength - mBufferPos, pLength); + int length = Math.min(bufferLength - bufferPos, pLength); if (length > 0) { - System.arraycopy(mBuffer, mBufferPos, pBuffer, pOffset, length); - mBufferPos += length; + System.arraycopy(buffer, bufferPos, pBuffer, pOffset, length); + bufferPos += length; } streamPos += length; @@ -121,42 +121,42 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme @Override public void seek(long pPosition) throws IOException { // TODO: Could probably be optimized to not invalidate buffer if new position is within current buffer - mStream.seek(pPosition); - mBufferLength = 0; // Will invalidate buffer - streamPos = mStream.getStreamPosition(); + stream.seek(pPosition); + bufferLength = 0; // Will invalidate buffer + streamPos = stream.getStreamPosition(); } @Override public void flushBefore(long pos) throws IOException { - mStream.flushBefore(pos); + stream.flushBefore(pos); } @Override public long getFlushedPosition() { - return mStream.getFlushedPosition(); + return stream.getFlushedPosition(); } @Override public boolean isCached() { - return mStream.isCached(); + return stream.isCached(); } @Override public boolean isCachedMemory() { - return mStream.isCachedMemory(); + return stream.isCachedMemory(); } @Override public boolean isCachedFile() { - return mStream.isCachedFile(); + return stream.isCachedFile(); } @Override public void close() throws IOException { - if (mStream != null) { - //mStream.close(); - mStream = null; - mBuffer = null; + if (stream != null) { + //stream.close(); + stream = null; + buffer = null; } super.close(); } @@ -170,7 +170,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme public long length() { // WTF?! This method is allowed to throw IOException in the interface... try { - return mStream.length(); + return stream.length(); } catch (IOException ignore) { } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java index 1c2119df..18ae9cef 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStream.java @@ -13,35 +13,50 @@ import java.io.IOException; * @version $Id: ByteArrayImageInputStream.java,v 1.0 May 15, 2008 2:12:12 PM haraldk Exp$ */ public final class ByteArrayImageInputStream extends ImageInputStreamImpl { - private final byte[] mData; + private final byte[] data; + private final int dataOffset; + private final int dataLength; public ByteArrayImageInputStream(final byte[] pData) { + this(pData, 0, pData != null ? pData.length : -1); + } + + public ByteArrayImageInputStream(final byte[] pData, int offset, int length) { Validate.notNull(pData, "data"); - mData = pData; + Validate.isTrue(offset >= 0 && offset <= pData.length, offset, "offset out of range: %d"); + Validate.isTrue(length >= 0 && length <= pData.length - offset, length, "length out of range: %d"); + + data = pData; + dataOffset = offset; + dataLength = length; } public int read() throws IOException { - if (streamPos >= mData.length) { + if (streamPos >= dataLength) { return -1; } + bitOffset = 0; - return mData[((int) streamPos++)] & 0xff; + + return data[((int) streamPos++) + dataOffset] & 0xff; } public int read(byte[] pBuffer, int pOffset, int pLength) throws IOException { - if (streamPos >= mData.length) { + if (streamPos >= dataLength) { return -1; } - int length = (int) Math.min(mData.length - streamPos, pLength); + + int length = (int) Math.min(this.dataLength - streamPos, pLength); bitOffset = 0; - System.arraycopy(mData, (int) streamPos, pBuffer, pOffset, length); + System.arraycopy(data, (int) streamPos + dataOffset, pBuffer, pOffset, length); streamPos += length; + return length; } @Override public long length() { - return mData.length; + return dataLength; } @Override diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageInputStream.java index 2c4ce03c..46600aa3 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/SubImageInputStream.java @@ -16,9 +16,9 @@ import java.io.IOException; public final class SubImageInputStream extends ImageInputStreamImpl { // NOTE: This class is based on com.sun.imageio.plugins.common.SubImageInputStream, but fixes some of its bugs. - private final ImageInputStream mStream; - private final long mStartPos; - private final long mLength; + private final ImageInputStream stream; + private final long startPos; + private final long length; /** * Creates a {@link ImageInputStream}, reading up to a maximum number of bytes from the underlying stream. @@ -32,21 +32,19 @@ public final class SubImageInputStream extends ImageInputStreamImpl { */ public SubImageInputStream(final ImageInputStream pStream, final long pLength) throws IOException { Validate.notNull(pStream, "stream"); - if (pLength < 0) { - throw new IllegalArgumentException("length < 0"); - } + Validate.isTrue(pLength >= 0, pLength, "length < 0: %d"); - mStream = pStream; - mStartPos = pStream.getStreamPosition(); - mLength = pLength; + stream = pStream; + startPos = pStream.getStreamPosition(); + length = pLength; } public int read() throws IOException { - if (streamPos >= mLength) { // Local EOF + if (streamPos >= length) { // Local EOF return -1; } else { - int read = mStream.read(); + int read = stream.read(); if (read >= 0) { streamPos++; @@ -57,13 +55,13 @@ public final class SubImageInputStream extends ImageInputStreamImpl { } public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (streamPos >= mLength) { // Local EOF + if (streamPos >= length) { // Local EOF return -1; } // Safe cast, as pLength can never cause int overflow - int length = (int) Math.min(pLength, mLength - streamPos); - int count = mStream.read(pBytes, pOffset, length); + int length = (int) Math.min(pLength, this.length - streamPos); + int count = stream.read(pBytes, pOffset, length); if (count >= 0) { streamPos += count; @@ -75,8 +73,8 @@ public final class SubImageInputStream extends ImageInputStreamImpl { @Override public long length() { try { - long length = mStream.length(); - return length < 0 ? -1 : Math.min(length - mStartPos, mLength); + long length = stream.length(); + return length < 0 ? -1 : Math.min(length - startPos, this.length); } catch (IOException ignore) { } @@ -90,7 +88,7 @@ public final class SubImageInputStream extends ImageInputStreamImpl { throw new IndexOutOfBoundsException("pos < flushedPosition"); } - mStream.seek(mStartPos + pPosition); + stream.seek(startPos + pPosition); streamPos = pPosition; } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapter.java index 1941efff..9e5b1d52 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapter.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapter.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.imageio.util; +import com.twelvemonkeys.lang.Validate; + import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.io.InputStream; @@ -43,10 +45,10 @@ import java.io.InputStream; * @version $Id: IIOInputStreamAdapter.java,v 1.0 Sep 26, 2007 11:35:59 AM haraldk Exp$ */ class IIOInputStreamAdapter extends InputStream { - private ImageInputStream mInput; - private final boolean mHasLength; - private long mLeft; - private long mMarkPosition; + private ImageInputStream input; + private final boolean hasLength; + private long left; + private long markPosition; // TODO: Enforce stream boundaries! // TODO: Stream start position.... @@ -75,16 +77,12 @@ class IIOInputStreamAdapter extends InputStream { } private IIOInputStreamAdapter(ImageInputStream pInput, long pLength, boolean pHasLength) { - if (pInput == null) { - throw new IllegalArgumentException("stream == null"); - } - if (pHasLength && pLength < 0) { - throw new IllegalArgumentException("length < 0"); - } + Validate.notNull(pInput, "stream"); + Validate.isTrue(!pHasLength || pLength >= 0, pLength, "length < 0: %f"); - mInput = pInput; - mHasLength = pHasLength; - mLeft = pLength; + input = pInput; + left = pLength; + hasLength = pHasLength; } @@ -93,18 +91,19 @@ class IIOInputStreamAdapter extends InputStream { * This implementation does not close the underlying stream. */ public void close() throws IOException { - if (mHasLength) { - mInput.seek(mInput.getStreamPosition() + mLeft); + if (hasLength) { + input.seek(input.getStreamPosition() + left); } - mLeft = 0; - mInput = null; + left = 0; + input = null; } public int available() throws IOException { - if (mHasLength) { - return mLeft > 0 ? (int) Math.min(Integer.MAX_VALUE, mLeft) : 0; + if (hasLength) { + return left > 0 ? (int) Math.min(Integer.MAX_VALUE, left) : 0; } + return 0; // We don't really know, so we say 0 to be safe. } @@ -115,7 +114,7 @@ class IIOInputStreamAdapter extends InputStream { public void mark(int pReadLimit) { try { - mMarkPosition = mInput.getStreamPosition(); + markPosition = input.getStreamPosition(); } catch (IOException e) { // Let's hope this never happens, because it's not possible to reset then... @@ -124,17 +123,17 @@ class IIOInputStreamAdapter extends InputStream { } public void reset() throws IOException { - long diff = mInput.getStreamPosition() - mMarkPosition; - mInput.seek(mMarkPosition); - mLeft += diff; + long diff = input.getStreamPosition() - markPosition; + input.seek(markPosition); + left += diff; } public int read() throws IOException { - if (mHasLength && mLeft-- <= 0) { - mLeft = 0; + if (hasLength && left-- <= 0) { + left = 0; return -1; } - return mInput.read(); + return input.read(); } public final int read(byte[] pBytes) throws IOException { @@ -142,13 +141,13 @@ class IIOInputStreamAdapter extends InputStream { } public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (mHasLength && mLeft <= 0) { + if (hasLength && left <= 0) { return -1; } - int read = mInput.read(pBytes, pOffset, (int) findMaxLen(pLength)); - if (mHasLength) { - mLeft = read < 0 ? 0 : mLeft - read; + int read = input.read(pBytes, pOffset, (int) findMaxLen(pLength)); + if (hasLength) { + left = read < 0 ? 0 : left - read; } return read; } @@ -161,8 +160,8 @@ class IIOInputStreamAdapter extends InputStream { * @return the maximum number of bytes to read */ private long findMaxLen(long pLength) { - if (mHasLength && mLeft < pLength) { - return Math.max(mLeft, 0); + if (hasLength && left < pLength) { + return Math.max(left, 0); } else { return Math.max(pLength, 0); @@ -170,8 +169,8 @@ class IIOInputStreamAdapter extends InputStream { } public long skip(long pLength) throws IOException { - long skipped = mInput.skipBytes(findMaxLen(pLength)); // Skips 0 or more, never -1 - mLeft -= skipped; + long skipped = input.skipBytes(findMaxLen(pLength)); // Skips 0 or more, never -1 + left -= skipped; return skipped; } } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapter.java index 9927b983..c83fd3d4 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapter.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapter.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.imageio.util; +import com.twelvemonkeys.lang.Validate; + import javax.imageio.stream.ImageOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -40,34 +42,47 @@ import java.io.OutputStream; * @version $Id: IIOOutputStreamAdapter.java,v 1.0 Sep 26, 2007 11:50:38 AM haraldk Exp$ */ class IIOOutputStreamAdapter extends OutputStream { - private ImageOutputStream mOutput; + private ImageOutputStream output; public IIOOutputStreamAdapter(final ImageOutputStream pOutput) { - mOutput = pOutput; + Validate.notNull(pOutput, "stream == null"); + + output = pOutput; } @Override public void write(final byte[] pBytes) throws IOException { - mOutput.write(pBytes); + assertOpen(); + output.write(pBytes); } @Override public void write(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - mOutput.write(pBytes, pOffset, pLength); + assertOpen(); + output.write(pBytes, pOffset, pLength); } @Override public void write(final int pByte) throws IOException { - mOutput.write(pByte); + assertOpen(); + output.write(pByte); } @Override public void flush() throws IOException { - mOutput.flush(); + // NOTE: The contract of OutputStream.flush is very different from ImageOutputStream.flush. We can't delegate. + // TODO: Fulfill the contract of OutputStream.flush? This seems to be good enough for now. + assertOpen(); + } + + private void assertOpen() throws IOException { + if (output == null) { + throw new IOException("stream already closed"); + } } @Override public void close() throws IOException { - mOutput = null; + output = null; } } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java index 67c3ee1b..e17376dc 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java @@ -4,6 +4,7 @@ import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.spi.ProviderInfo; import javax.imageio.IIOParam; +import javax.imageio.ImageIO; import javax.imageio.spi.IIOServiceProvider; import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; @@ -14,6 +15,8 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.util.SortedSet; +import java.util.TreeSet; /** * IIOUtil @@ -54,6 +57,9 @@ public final class IIOUtil { /** * Creates an {@code OutputStream} adapter that writes to an underlying {@code ImageOutputStream}. + *

    + * Note: The adapter is buffered, and MUST be properly flushed/closed after use, + * otherwise data may be lost. * * @param pStream the stream to write to. * @return an {@code OutputSteam} writing to {@code pStream}. @@ -146,4 +152,35 @@ public final class IIOUtil { pRegistry.deregisterServiceProvider(pCategory.cast(pProvider), pCategory); } + /** + * Returns a sorted array of format names, that can be read by ImageIO. + * The names are all upper-case, and contains no duplicates. + * + * @return a normalized array of {@code String}s. + * @see javax.imageio.ImageIO#getReaderFormatNames() + */ + public static String[] getNormalizedReaderFormatNames() { + return normalizeNames(ImageIO.getReaderFormatNames()); + } + + /** + * Returns a sorted array of format names, that can be written by ImageIO. + * The names are all upper-case, and contains no duplicates. + * + * @return a normalized array of {@code String}s. + * @see javax.imageio.ImageIO#getWriterFormatNames() + */ + public static String[] getNormalizedWriterFormatNames() { + return normalizeNames(ImageIO.getWriterFormatNames()); + } + + private static String[] normalizeNames(final String[] names) { + SortedSet normalizedNames = new TreeSet(); + + for (String name : names) { + normalizedNames.add(name.toUpperCase()); + } + + return normalizedNames.toArray(new String[normalizedNames.size()]); + } } \ No newline at end of file diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IndexedImageTypeSpecifier.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IndexedImageTypeSpecifier.java index 42cf52d0..a9ba4b6a 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IndexedImageTypeSpecifier.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IndexedImageTypeSpecifier.java @@ -37,5 +37,4 @@ public class IndexedImageTypeSpecifier extends ImageTypeSpecifier { throw new IllegalArgumentException("Array size > Integer.MAX_VALUE!"); } } - } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ProgressListenerBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ProgressListenerBase.java index 4a4ba7ff..816dc142 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ProgressListenerBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ProgressListenerBase.java @@ -44,51 +44,51 @@ public abstract class ProgressListenerBase implements IIOReadProgressListener, I protected ProgressListenerBase() { } - public void imageComplete(ImageReader pSource) { + public void imageComplete(ImageReader source) { } - public void imageProgress(ImageReader pSource, float pPercentageDone) { + public void imageProgress(ImageReader source, float percentageDone) { } - public void imageStarted(ImageReader pSource, int pImageIndex) { + public void imageStarted(ImageReader source, int imageIndex) { } - public void readAborted(ImageReader pSource) { + public void readAborted(ImageReader source) { } - public void sequenceComplete(ImageReader pSource) { + public void sequenceComplete(ImageReader source) { } - public void sequenceStarted(ImageReader pSource, int pMinIndex) { + public void sequenceStarted(ImageReader source, int minIndex) { } - public void thumbnailComplete(ImageReader pSource) { + public void thumbnailComplete(ImageReader source) { } - public void thumbnailProgress(ImageReader pSource, float pPercentageDone) { + public void thumbnailProgress(ImageReader source, float percentageDone) { } - public void thumbnailStarted(ImageReader pSource, int pImageIndex, int pThumbnailIndex) { + public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { } - public void imageComplete(ImageWriter pSource) { + public void imageComplete(ImageWriter source) { } - public void imageProgress(ImageWriter pSource, float pPercentageDone) { + public void imageProgress(ImageWriter source, float percentageDone) { } - public void imageStarted(ImageWriter pSource, int pImageIndex) { + public void imageStarted(ImageWriter source, int imageIndex) { } - public void thumbnailComplete(ImageWriter pSource) { + public void thumbnailComplete(ImageWriter source) { } - public void thumbnailProgress(ImageWriter pSource, float pPercentageDone) { + public void thumbnailProgress(ImageWriter source, float percentageDone) { } - public void thumbnailStarted(ImageWriter pSource, int pImageIndex, int pThumbnailIndex) { + public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) { } - public void writeAborted(ImageWriter pSource) { + public void writeAborted(ImageWriter source) { } } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ReaderFileSuffixFilter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ReaderFileSuffixFilter.java index bdad8e28..e94b61b0 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ReaderFileSuffixFilter.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/ReaderFileSuffixFilter.java @@ -47,15 +47,15 @@ import java.util.Map; * @version $Id: ReaderFileSuffixFilter.java,v 1.0 11.okt.2006 20:05:36 haku Exp$ */ public final class ReaderFileSuffixFilter extends FileFilter implements java.io.FileFilter { - private final String mDescription; - private final Map mKnownSuffixes = new HashMap(32); + private final String description; + private final Map knownSuffixes = new HashMap(32); public ReaderFileSuffixFilter() { this("Images (all supported input formats)"); } public ReaderFileSuffixFilter(String pDescription) { - mDescription = pDescription; + description = pDescription; } public boolean accept(File pFile) { @@ -71,19 +71,20 @@ public final class ReaderFileSuffixFilter extends FileFilter implements java.io. } private boolean hasReaderForSuffix(String pSuffix) { - if (mKnownSuffixes.get(pSuffix) == Boolean.TRUE) { + if (knownSuffixes.get(pSuffix) == Boolean.TRUE) { return true; } try { // Cahce lookup Iterator iterator = ImageIO.getImageReadersBySuffix(pSuffix); + if (iterator.hasNext()) { - mKnownSuffixes.put(pSuffix, Boolean.TRUE); + knownSuffixes.put(pSuffix, Boolean.TRUE); return true; } else { - mKnownSuffixes.put(pSuffix, Boolean.FALSE); + knownSuffixes.put(pSuffix, Boolean.FALSE); return false; } } @@ -93,6 +94,6 @@ public final class ReaderFileSuffixFilter extends FileFilter implements java.io. } public String getDescription() { - return mDescription; + return description; } } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/WriterFileSuffixFilter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/WriterFileSuffixFilter.java index 935faebd..20a43ae4 100755 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/WriterFileSuffixFilter.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/WriterFileSuffixFilter.java @@ -47,15 +47,15 @@ import java.util.Map; * @version $Id: WriterFileSuffixFilter.java,v 1.0 11.okt.2006 20:05:36 haku Exp$ */ public final class WriterFileSuffixFilter extends FileFilter implements java.io.FileFilter { - private final String mDescription; - private MapmKnownSuffixes = new HashMap(32); + private final String description; + private Map knownSuffixes = new HashMap(32); public WriterFileSuffixFilter() { this("Images (all supported output formats)"); } public WriterFileSuffixFilter(String pDescription) { - mDescription = pDescription; + description = pDescription; } public boolean accept(File pFile) { @@ -66,24 +66,25 @@ public final class WriterFileSuffixFilter extends FileFilter implements java.io. // Test if we have an ImageWriter for this suffix String suffix = FileUtil.getExtension(pFile); - return !StringUtil.isEmpty(suffix) && hasWriterForSuffix(suffix); + return !StringUtil.isEmpty(suffix) && hasWriterForSuffix(suffix); } private boolean hasWriterForSuffix(String pSuffix) { - if (mKnownSuffixes.get(pSuffix) == Boolean.TRUE) { + if (knownSuffixes.get(pSuffix) == Boolean.TRUE) { return true; } try { // Cahce lookup Iterator iterator = ImageIO.getImageWritersBySuffix(pSuffix); + if (iterator.hasNext()) { - mKnownSuffixes.put(pSuffix, Boolean.TRUE); + knownSuffixes.put(pSuffix, Boolean.TRUE); return true; } else { - mKnownSuffixes.put(pSuffix, Boolean.FALSE); + knownSuffixes.put(pSuffix, Boolean.FALSE); return false; } } @@ -93,6 +94,6 @@ public final class WriterFileSuffixFilter extends FileFilter implements java.io. } public String getDescription() { - return mDescription; + return description; } } diff --git a/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_osx.properties b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_osx.properties new file mode 100644 index 00000000..a516ec1c --- /dev/null +++ b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_osx.properties @@ -0,0 +1,29 @@ +# +# Copyright (c) 2011, Harald Kuhr +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name "TwelveMonkeys" nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. +# +GENERIC_CMYK=/System/Library/ColorSync/Profiles/Generic CMYK Profile.icc +ADOBE_RGB_1998=/System/Library/ColorSync/Profiles/AdobeRGB1998.icc diff --git a/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties new file mode 100644 index 00000000..bab2bf93 --- /dev/null +++ b/imageio/imageio-core/src/main/resources/com/twelvemonkeys/imageio/color/icc_profiles_win.properties @@ -0,0 +1,29 @@ +# +# Copyright (c) 2011, Harald Kuhr +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name "TwelveMonkeys" nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. +# +GENERIC_CMYK=/C:/Windows/System32/spool/drivers/color/RSWOP.icm +#ADOBE_RGB_1998=use built in for now \ No newline at end of file diff --git a/imageio/imageio-core/src/main/resources/profiles/ClayRGB1998.icc b/imageio/imageio-core/src/main/resources/profiles/ClayRGB1998.icc new file mode 100644 index 00000000..1eedf093 Binary files /dev/null and b/imageio/imageio-core/src/main/resources/profiles/ClayRGB1998.icc differ diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/ImageReaderBaseTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/ImageReaderBaseTest.java new file mode 100644 index 00000000..d0fb66df --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/ImageReaderBaseTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio; + +import org.junit.Test; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * ImageReaderBaseTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ImageReaderBaseTest.java,v 1.0 23.05.12 09:50 haraldk Exp$ + */ +public class ImageReaderBaseTest { + + private static final List TYPES = Arrays.asList( + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB) + ); + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationZeroWidth() throws IIOException { + ImageReaderBase.getDestination(null, TYPES.iterator(), 0, 42); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationNegativeWidth() throws IIOException { + ImageReaderBase.getDestination(null, TYPES.iterator(), -1, 42); + + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationZeroHeight() throws IIOException { + ImageReaderBase.getDestination(null, TYPES.iterator(), 42, 0); + + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationNegativeHeight() throws IIOException { + ImageReaderBase.getDestination(null, TYPES.iterator(), 42, -1); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationNullTypes() throws IIOException { + ImageReaderBase.getDestination(null, null, 42, 42); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationNoTypes() throws IIOException { + ImageReaderBase.getDestination(null, Collections.emptyList().iterator(), 42, 42); + } + + @Test + public void testGetDestinationParamSourceRegionWider() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setSourceRegion(new Rectangle(42, 1)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 3, 3); + assertEquals(3, destination.getWidth()); + assertEquals(1, destination.getHeight()); + assertEquals(TYPES.get(0).getBufferedImageType(), destination.getType()); + } + + @Test + public void testGetDestinationParamSourceRegionTaller() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setSourceRegion(new Rectangle(1, 42)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 3, 3); + assertEquals(1, destination.getWidth()); + assertEquals(3, destination.getHeight()); + assertEquals(TYPES.get(0).getBufferedImageType(), destination.getType()); + } + + @Test + public void testGetDestinationParamDestinationWider() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestination(new BufferedImage(42, 1, BufferedImage.TYPE_INT_RGB)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 3, 3); + assertEquals(42, destination.getWidth()); + assertEquals(1, destination.getHeight()); + assertEquals(BufferedImage.TYPE_INT_RGB, destination.getType()); + } + + @Test + public void testGetDestinationParamDestinationTaller() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestination(new BufferedImage(1, 42, BufferedImage.TYPE_INT_ARGB)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 3, 3); + assertEquals(1, destination.getWidth()); + assertEquals(42, destination.getHeight()); + assertEquals(BufferedImage.TYPE_INT_ARGB, destination.getType()); + } + + @Test + public void testGetDestinationNoParam() throws IIOException { + BufferedImage destination = ImageReaderBase.getDestination(null, TYPES.iterator(), 42, 1); + assertEquals(BufferedImage.TYPE_INT_RGB, destination.getType()); + assertEquals(42, destination.getWidth()); + assertEquals(1, destination.getHeight()); + } + + @Test + public void testGetDestinationParamNoDestination() throws IIOException { + BufferedImage destination = ImageReaderBase.getDestination(new ImageReadParam(), TYPES.iterator(), 42, 1); + assertEquals(BufferedImage.TYPE_INT_RGB, destination.getType()); + assertEquals(42, destination.getWidth()); + assertEquals(1, destination.getHeight()); + } + + @Test + public void testGetDestinationParamGoodDestination() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestination(new BufferedImage(21, 1, BufferedImage.TYPE_INT_ARGB)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 42, 1); + assertEquals(BufferedImage.TYPE_INT_ARGB, destination.getType()); + assertEquals(21, destination.getWidth()); + assertEquals(1, destination.getHeight()); + } + + @Test(expected = IIOException.class) + public void testGetDestinationParamIllegalDestination() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestination(new BufferedImage(21, 1, BufferedImage.TYPE_USHORT_565_RGB)); + ImageReaderBase.getDestination(param, TYPES.iterator(), 42, 1); + } + + @Test + public void testGetDestinationParamGoodDestinationType() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestinationType(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 6, 7); + assertEquals(BufferedImage.TYPE_INT_ARGB, destination.getType()); + assertEquals(6, destination.getWidth()); + assertEquals(7, destination.getHeight()); + } + + @Test + public void testGetDestinationParamGoodDestinationTypeAlt() throws IIOException { + ImageReadParam param = new ImageReadParam(); + // In essence, this is the same as TYPE_INT_ARGB + ImageTypeSpecifier type = ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB), 0xff0000, 0xff00, 0xff, 0xff000000, DataBuffer.TYPE_INT, false); + param.setDestinationType(type); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), 6, 7); + assertEquals(BufferedImage.TYPE_INT_ARGB, destination.getType()); + assertEquals(6, destination.getWidth()); + assertEquals(7, destination.getHeight()); + } + + @Test(expected = IIOException.class) + public void testGetDestinationParamIllegalDestinationType() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestinationType(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY)); + ImageReaderBase.getDestination(param, TYPES.iterator(), 6, 7); + } + + @Test(expected = IIOException.class) + public void testGetDestinationParamIllegalDestinationTypeAlt() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setDestinationType(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)); + ImageReaderBase.getDestination(param, TYPES.iterator(), 6, 7); + } + + @Test + public void testGetDestinationSourceExceedsIntegerMax() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setSourceRegion(new Rectangle(42, 7)); + BufferedImage destination = ImageReaderBase.getDestination(param, TYPES.iterator(), Integer.MAX_VALUE, 42);// 90 194 313 174 pixels + assertEquals(42, destination.getWidth()); + assertEquals(7, destination.getHeight()); + assertEquals(TYPES.get(0).getBufferedImageType(), destination.getType()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationParamDestinationExceedsIntegerMax() throws IIOException { + ImageReadParam param = new ImageReadParam(); + param.setSourceRegion(new Rectangle(3 * Short.MAX_VALUE, 2 * Short.MAX_VALUE)); // 6 442 057 734 pixels + ImageReaderBase.getDestination(param, TYPES.iterator(), 6 * Short.MAX_VALUE, 4 * Short.MAX_VALUE); // 25 768 230 936 pixels + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDestinationExceedsIntegerMax() throws IIOException { + ImageReaderBase.getDestination(null, TYPES.iterator(), 3 * Short.MAX_VALUE, 2 * Short.MAX_VALUE); // 6 442 057 734 pixels + } + + @Test + public void testHasExplicitDestinationNull() { + assertFalse(ImageReaderBase.hasExplicitDestination(null)); + + } + + @Test + public void testHasExplicitDestinationDefaultParam() { + assertFalse(ImageReaderBase.hasExplicitDestination(new ImageReadParam())); + } + + @Test + public void testHasExplicitDestinationParamWithDestination() { + ImageReadParam param = new ImageReadParam(); + param.setDestination(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY)); + assertTrue(ImageReaderBase.hasExplicitDestination(param)); + } + + @Test + public void testHasExplicitDestinationParamWithDestinationType() { + ImageReadParam param = new ImageReadParam(); + param.setDestinationType(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB)); + assertTrue(ImageReaderBase.hasExplicitDestination(param)); + } + + @Test + public void testHasExplicitDestinationParamWithDestinationOffset() { + ImageReadParam param = new ImageReadParam(); + param.setDestinationOffset(new Point(42, 42)); + assertTrue(ImageReaderBase.hasExplicitDestination(param)); + } + + @Test + public void testHasExplicitDestinationParamWithDestinationOffsetUnspecified() { + ImageReadParam param = new ImageReadParam(); + // getDestinationOffset should now return new Point(0, 0) + assertFalse(ImageReaderBase.hasExplicitDestination(param)); + } + + @Test + public void testHasExplicitDestinationParamWithDestinationOffsetOrigin() { + ImageReadParam param = new ImageReadParam(); + param.setDestinationOffset(new Point(0, 0)); + assertFalse(ImageReaderBase.hasExplicitDestination(param)); + } +} diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java new file mode 100644 index 00000000..9dddc7aa --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/ColorSpacesTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.color; + +import org.junit.Test; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; + +import static org.junit.Assert.*; + +/** + * ColorSpacesTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ColorSpacesTest.java,v 1.0 07.02.11 14.32 haraldk Exp$ + */ +public class ColorSpacesTest { + @Test + public void testCreateColorSpaceFromKnownProfileReturnsInternalCS_sRGB() { + ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_sRGB); + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(created, ColorSpace.getInstance(ColorSpace.CS_sRGB)); + assertTrue(created.isCS_sRGB()); + } + + @Test + public void testCreateColorSpaceFromKnownProfileDataReturnsInternalCS_sRGB() { + ICC_Profile internal = ICC_Profile.getInstance(ColorSpace.CS_sRGB); + byte[] data = internal.getData(); + assertNotSame(internal.getData(), data); // Sanity check + + ICC_Profile profile = ICC_Profile.getInstance(data); + assertNotSame(internal, profile); // Sanity check + + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(ColorSpace.getInstance(ColorSpace.CS_sRGB), created); + assertTrue(created.isCS_sRGB()); + } + + @Test + public void testCreateColorSpaceFromBrokenProfileIsFixedCS_sRGB() { + ICC_Profile internal = ICC_Profile.getInstance(ColorSpace.CS_sRGB); + ICC_Profile profile = createBrokenProfile(internal); + assertNotSame(internal, profile); // Sanity check + + assertTrue(ColorSpaces.isOffendingColorProfile(profile)); + + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(ColorSpace.getInstance(ColorSpace.CS_sRGB), created); + assertTrue(created.isCS_sRGB()); + } + + private ICC_Profile createBrokenProfile(ICC_Profile internal) { + byte[] data = internal.getData(); + data[ICC_Profile.icHdrRenderingIntent] = 1; // Intent: 1 == Relative Colormetric + return ICC_Profile.getInstance(data); + } + + @Test + public void testIsOffendingColorProfile() { + ICC_Profile broken = createBrokenProfile(ICC_Profile.getInstance(ColorSpace.CS_GRAY)); + assertTrue(ColorSpaces.isOffendingColorProfile(broken)); + } + + @Test + public void testCreateColorSpaceFromKnownProfileReturnsInternalCS_GRAY() { + ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_GRAY); + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(ColorSpace.getInstance(ColorSpace.CS_GRAY), created); + } + + @Test + public void testCreateColorSpaceFromKnownProfileReturnsInternalCS_PYCC() { + ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_PYCC); + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(ColorSpace.getInstance(ColorSpace.CS_PYCC), created); + } + + @Test + public void testCreateColorSpaceFromKnownProfileReturnsInternalCS_CIEXYZ() { + ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ); + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(ColorSpace.getInstance(ColorSpace.CS_CIEXYZ), created); + } + + @Test + public void testCreateColorSpaceFromKnownProfileReturnsInternalCS_LINEAR_RGB() { + ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB); + ICC_ColorSpace created = ColorSpaces.createColorSpace(profile); + assertSame(ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB), created); + } + + @Test + public void testAdobeRGB98NotNull() { + assertNotNull(ColorSpaces.getColorSpace(ColorSpaces.CS_ADOBE_RGB_1998)); + } + + @Test + public void testAdobeRGB98IsTypeRGB() { + assertEquals(ColorSpace.TYPE_RGB, ColorSpaces.getColorSpace(ColorSpaces.CS_ADOBE_RGB_1998).getType()); + } + + @Test + public void testAdobeRGB98AlwaysSame() { + ColorSpace cs = ColorSpaces.getColorSpace(ColorSpaces.CS_ADOBE_RGB_1998); + assertSame(cs, ColorSpaces.getColorSpace(ColorSpaces.CS_ADOBE_RGB_1998)); + + if (cs instanceof ICC_ColorSpace) { + ICC_ColorSpace iccCs = (ICC_ColorSpace) cs; + assertSame(cs, ColorSpaces.createColorSpace(iccCs.getProfile())); + } + else { + System.err.println("Not an ICC_ColorSpace: " + cs); + } + } + + @Test + public void testCMYKNotNull() { + assertNotNull(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK)); + } + + @Test + public void testCMYKIsTypeCMYK() { + assertEquals(ColorSpace.TYPE_CMYK, ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK).getType()); + } + + @Test + public void testCMYKAlwaysSame() { + ColorSpace cs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); + assertSame(cs, ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK)); + + if (cs instanceof ICC_ColorSpace) { + ICC_ColorSpace iccCs = (ICC_ColorSpace) cs; + assertSame(cs, ColorSpaces.createColorSpace(iccCs.getProfile())); + } + else { + System.err.println("Not an ICC_ColorSpace: " + cs); + } + } +} diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTestCase.java index 8c04d347..90e4b12b 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTestCase.java @@ -18,7 +18,7 @@ import java.util.Random; * @version $Id: BufferedImageInputStreamTestCase.java,v 1.0 Jun 30, 2008 3:07:42 PM haraldk Exp$ */ public class BufferedImageInputStreamTestCase extends TestCase { - protected final Random mRandom = new Random(); + protected final Random random = new Random(); public void testCreate() throws IOException { new BufferedImageInputStream(new ByteArrayImageInputStream(new byte[0])); @@ -58,7 +58,7 @@ public class BufferedImageInputStreamTestCase extends TestCase { // Fill bytes byte[] bytes = new byte[size * 2]; - mRandom.nextBytes(bytes); + random.nextBytes(bytes); // Create wrapper stream BufferedImageInputStream stream = new BufferedImageInputStream(new ByteArrayImageInputStream(bytes)); @@ -79,7 +79,7 @@ public class BufferedImageInputStreamTestCase extends TestCase { public void testBufferPositionCorrect() throws IOException { // Fill bytes byte[] bytes = new byte[1024]; - mRandom.nextBytes(bytes); + random.nextBytes(bytes); ByteArrayImageInputStream input = new ByteArrayImageInputStream(bytes); diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTestCase.java index 372fe6ed..868b082c 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTestCase.java @@ -1,11 +1,12 @@ package com.twelvemonkeys.imageio.stream; -import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTestCase.rangeEquals; import junit.framework.TestCase; import java.io.IOException; import java.util.Random; +import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTestCase.rangeEquals; + /** * ByteArrayImageInputStreamTestCase * @@ -14,7 +15,7 @@ import java.util.Random; * @version $Id: ByteArrayImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$ */ public class ByteArrayImageInputStreamTestCase extends TestCase { - protected final Random mRandom = new Random(); + protected final Random random = new Random(); public void testCreate() { ByteArrayImageInputStream stream = new ByteArrayImageInputStream(new byte[0]); @@ -34,9 +35,74 @@ public class ByteArrayImageInputStreamTestCase extends TestCase { } } + public void testCreateNullOffLen() { + try { + new ByteArrayImageInputStream(null, 0, -1); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException expected) { + assertNotNull("Null exception message", expected.getMessage()); + String message = expected.getMessage().toLowerCase(); + assertTrue("Exception message does not contain parameter name", message.contains("data")); + assertTrue("Exception message does not contain null", message.contains("null")); + } + } + + public void testCreateNegativeOff() { + try { + new ByteArrayImageInputStream(new byte[0], -1, 1); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException expected) { + assertNotNull("Null exception message", expected.getMessage()); + String message = expected.getMessage().toLowerCase(); + assertTrue("Exception message does not contain parameter name", message.contains("offset")); + assertTrue("Exception message does not contain -1", message.contains("-1")); + } + } + + public void testCreateBadOff() { + try { + new ByteArrayImageInputStream(new byte[1], 2, 0); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException expected) { + assertNotNull("Null exception message", expected.getMessage()); + String message = expected.getMessage().toLowerCase(); + assertTrue("Exception message does not contain parameter name", message.contains("offset")); + assertTrue("Exception message does not contain 2", message.contains("2")); + } + } + + public void testCreateNegativeLen() { + try { + new ByteArrayImageInputStream(new byte[0], 0, -1); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException expected) { + assertNotNull("Null exception message", expected.getMessage()); + String message = expected.getMessage().toLowerCase(); + assertTrue("Exception message does not contain parameter name", message.contains("length")); + assertTrue("Exception message does not contain -1", message.contains("-1")); + } + } + + public void testCreateBadLen() { + try { + new ByteArrayImageInputStream(new byte[1], 0, 2); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException expected) { + assertNotNull("Null exception message", expected.getMessage()); + String message = expected.getMessage().toLowerCase(); + assertTrue("Exception message does not contain parameter name", message.contains("length")); + assertTrue("Exception message does not contain â„¢", message.contains("2")); + } + } + public void testRead() throws IOException { byte[] data = new byte[1024 * 1024]; - mRandom.nextBytes(data); + random.nextBytes(data); ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data); @@ -47,9 +113,24 @@ public class ByteArrayImageInputStreamTestCase extends TestCase { } } + public void testReadOffsetLen() throws IOException { + byte[] data = new byte[1024 * 1024]; + random.nextBytes(data); + + int offset = random.nextInt(data.length / 10); + int length = random.nextInt(data.length - offset); + ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data, offset, length); + + assertEquals("Data length should be same as stream length", length, stream.length()); + + for (int i = offset; i < offset + length; i++) { + assertEquals("Wrong data read", data[i] & 0xff, stream.read()); + } + } + public void testReadArray() throws IOException { byte[] data = new byte[1024 * 1024]; - mRandom.nextBytes(data); + random.nextBytes(data); ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data); @@ -63,9 +144,27 @@ public class ByteArrayImageInputStreamTestCase extends TestCase { } } + public void testReadArrayOffLen() throws IOException { + byte[] data = new byte[1024 * 1024]; + random.nextBytes(data); + + int offset = random.nextInt(data.length - 10240); + int length = 10240; + ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data, offset, length); + + assertEquals("Data length should be same as stream length", length, stream.length()); + + byte[] result = new byte[1024]; + + for (int i = 0; i < length / result.length; i++) { + stream.readFully(result); + assertTrue("Wrong data read: " + i, rangeEquals(data, offset + i * result.length, result, 0, result.length)); + } + } + public void testReadSkip() throws IOException { byte[] data = new byte[1024 * 14]; - mRandom.nextBytes(data); + random.nextBytes(data); ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data); @@ -82,7 +181,7 @@ public class ByteArrayImageInputStreamTestCase extends TestCase { public void testReadSeek() throws IOException { byte[] data = new byte[1024 * 18]; - mRandom.nextBytes(data); + random.nextBytes(data); ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data); diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/SubImageInputStreamTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/SubImageInputStreamTestCase.java index 665582d8..674afa18 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/SubImageInputStreamTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/SubImageInputStreamTestCase.java @@ -19,12 +19,12 @@ import java.util.Random; */ public class SubImageInputStreamTestCase extends TestCase { // TODO: Extract super test case for all stream tests - private final Random mRandom = new Random(837468l); + private final Random random = new Random(837468l); private ImageInputStream createStream(final int pSize) { byte[] bytes = new byte[pSize]; - mRandom.nextBytes(bytes); + random.nextBytes(bytes); return new MemoryCacheImageInputStream(new ByteArrayInputStream(bytes)) { @Override diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapterTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapterTestCase.java index b010109f..bb725a5f 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapterTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOInputStreamAdapterTestCase.java @@ -29,11 +29,14 @@ package com.twelvemonkeys.imageio.util; import com.twelvemonkeys.io.InputStreamAbstractTestCase; +import org.junit.Test; import javax.imageio.stream.MemoryCacheImageInputStream; -import java.io.InputStream; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; /** * IIOInputStreamAdapter @@ -43,14 +46,17 @@ import java.io.IOException; * @version $Id: IIOInputStreamAdapter.java,v 1.0 Apr 11, 2008 1:04:42 PM haraldk Exp$ */ public class IIOInputStreamAdapterTestCase extends InputStreamAbstractTestCase { - public IIOInputStreamAdapterTestCase(String name) { - super(name); - } protected InputStream makeInputStream(byte[] pBytes) { return new IIOInputStreamAdapter(new MemoryCacheImageInputStream(new ByteArrayInputStream(pBytes)), pBytes.length); } + @Test(expected = IllegalArgumentException.class) + public void testCreateNull() { + new IIOInputStreamAdapter(null); + } + + @Test public void testReadSubstreamOpenEnd() throws IOException { byte[] bytes = new byte[20]; @@ -74,6 +80,7 @@ public class IIOInputStreamAdapterTestCase extends InputStreamAbstractTestCase { input.close(); } + @Test public void testReadSubstream() throws IOException { byte[] bytes = new byte[20]; @@ -92,6 +99,7 @@ public class IIOInputStreamAdapterTestCase extends InputStreamAbstractTestCase { input.close(); } + @Test public void testReadSubstreamRepositionOnClose() throws IOException { byte[] bytes = new byte[20]; @@ -111,6 +119,7 @@ public class IIOInputStreamAdapterTestCase extends InputStreamAbstractTestCase { input.close(); } + @Test public void testSeekBeforeStreamNoEnd() throws IOException { byte[] bytes = new byte[20]; @@ -124,6 +133,7 @@ public class IIOInputStreamAdapterTestCase extends InputStreamAbstractTestCase { assertEquals(10, input.getStreamPosition()); } + @Test public void testSeekBeforeStream() throws IOException { byte[] bytes = new byte[20]; diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapterTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapterTestCase.java new file mode 100644 index 00000000..0fdcf9a4 --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOOutputStreamAdapterTestCase.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.util; + +import com.twelvemonkeys.io.OutputStreamAbstractTestCase; +import org.junit.Test; + +import javax.imageio.stream.MemoryCacheImageOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static org.junit.Assert.*; + +/** + * IIOOutputStreamAdapterTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IIOOutputStreamAdapterTestCase.java,v 1.0 30.11.11 12:21 haraldk Exp$ + */ +public class IIOOutputStreamAdapterTestCase extends OutputStreamAbstractTestCase { + @Override + protected OutputStream makeObject() { + return new IIOOutputStreamAdapter(new MemoryCacheImageOutputStream(new ByteArrayOutputStream())); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNull() { + new IIOOutputStreamAdapter(null); + } + + @Test + public void testFlushOnAdapterDoesNotMoveFlushedPositionInBacking() throws IOException { + MemoryCacheImageOutputStream backing = new MemoryCacheImageOutputStream(new ByteArrayOutputStream()); + IIOOutputStreamAdapter adapter = new IIOOutputStreamAdapter(backing); + + // Sanity check + assertEquals(0, backing.getFlushedPosition()); + + // Write & flush + adapter.write(0xCA); + adapter.write(new byte[8]); + adapter.write(0xFE); + adapter.flush(); + + // Assertions + assertEquals(10, backing.length()); + assertEquals(10, backing.getStreamPosition()); + assertEquals(0, backing.getFlushedPosition()); + + // Just make sure we can safely seek back to start and read data back + backing.seek(0); + assertEquals(0, backing.getStreamPosition()); + + // If this can be read, I think the contract of flush is also fulfilled (kind of) + assertEquals(0xCA, backing.read()); + assertEquals(8, backing.skipBytes(8)); + assertEquals(0xFE, backing.read()); + } +} diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java index 601ff974..cf15f1f0 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java @@ -29,10 +29,11 @@ package com.twelvemonkeys.imageio.util; import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; -import org.jmock.Mock; -import org.jmock.cglib.MockObjectTestCase; -import org.jmock.core.Invocation; -import org.jmock.core.Stub; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import javax.imageio.*; import javax.imageio.event.IIOReadProgressListener; @@ -53,6 +54,9 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + /** * ImageReaderAbstractTestCase * @@ -60,7 +64,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: ImageReaderAbstractTestCase.java,v 1.0 Apr 1, 2008 10:36:46 PM haraldk Exp$ */ -public abstract class ImageReaderAbstractTestCase extends MockObjectTestCase { +public abstract class ImageReaderAbstractTestCase { // TODO: Should we really test if he provider is installed? // - Pro: Tests the META-INF/services config // - Con: Not all providers should be installed at runtime... @@ -116,6 +120,16 @@ public abstract class ImageReaderAbstractTestCase extends protected abstract List getMIMETypes(); + protected boolean allowsNullRawImageType() { + return false; + } + + private static void failBecause(String message, Throwable exception) { + AssertionError error = new AssertionError(message); + error.initCause(exception); + throw error; + } + protected void assertProviderInstalledForName(final String pFormat, final Class pReaderClass) { assertProviderInstalled0(pFormat.toUpperCase(), pReaderClass, ImageIO.getImageReadersByFormatName(pFormat.toUpperCase())); assertProviderInstalled0(pFormat.toLowerCase(), pReaderClass, ImageIO.getImageReadersByFormatName(pFormat.toLowerCase())); @@ -138,9 +152,10 @@ public abstract class ImageReaderAbstractTestCase extends } } - assertTrue(pReaderClass.getSimpleName() + " not installed for " + pFormat, found); + assertTrue(String.format("%s not installed for %s", pReaderClass.getSimpleName(), pFormat), found); } + @Test public void testProviderInstalledForNames() { Class readerClass = getReaderClass(); for (String name : getFormatNames()) { @@ -148,6 +163,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testProviderInstalledForSuffixes() { Class readerClass = getReaderClass(); for (String suffix : getSuffixes()) { @@ -155,6 +171,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testProviderInstalledForMIMETypes() { Class readerClass = getReaderClass(); for (String type : getMIMETypes()) { @@ -162,6 +179,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testProviderCanRead() throws IOException { List testData = getTestData(); @@ -173,31 +191,37 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testProviderCanReadNull() { boolean canRead = false; + try { canRead = createProvider().canDecodeInput(null); } catch (IllegalArgumentException ignore) { } catch (RuntimeException e) { - fail("RuntimeException other than IllegalArgumentException thrown: " + e); + failBecause("RuntimeException other than IllegalArgumentException thrown", e); } catch (IOException e) { - fail("Could not test data for read: " + e); + failBecause("Could not test data for read", e); } + assertFalse("ImageReader can read null input", canRead); } + @Test public void testSetInput() { // Should just pass with no exceptions ImageReader reader = createReader(); assertNotNull(reader); + for (TestData data : getTestData()) { reader.setInput(data.getInputStream()); } } + @Test public void testSetInputNull() { // Should just pass with no exceptions ImageReader reader = createReader(); @@ -205,24 +229,23 @@ public abstract class ImageReaderAbstractTestCase extends reader.setInput(null); } + @Test public void testRead() { ImageReader reader = createReader(); - for (TestData data : getTestData()) { - // TODO: Is it required to call reset before setInput? - reader.setInput(data.getInputStream()); - // TODO: Require count to match? -// System.out.println("reader.getNumImages(true): " + reader.getNumImages(true)); + for (TestData data : getTestData()) { + reader.setInput(data.getInputStream()); for (int i = 0; i < data.getImageCount(); i++) { BufferedImage image = null; + try { image = reader.read(i); } catch (Exception e) { - e.printStackTrace(); - fail(String.format("Image %s index %s could not be read: %s", data.getInput(), i, e)); + failBecause(String.format("Image %s index %s could not be read: %s", data.getInput(), i, e), e); } + assertNotNull(String.format("Image %s index %s was null!", data.getInput(), i), image); assertEquals( @@ -238,6 +261,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testReadIndexNegative() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -251,11 +275,12 @@ public abstract class ImageReaderAbstractTestCase extends catch (IndexOutOfBoundsException ignore) { } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNull(image); } + @Test public void testReadIndexOutOfBounds() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -269,11 +294,12 @@ public abstract class ImageReaderAbstractTestCase extends catch (IndexOutOfBoundsException ignore) { } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNull(image); } + @Test public void testReadNoInput() { ImageReader reader = createReader(); // Do not set input @@ -286,11 +312,12 @@ public abstract class ImageReaderAbstractTestCase extends catch (IllegalStateException ignore) { } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNull(image); } + @Test public void testReRead() throws IOException { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -308,6 +335,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals(first.getHeight(), second.getHeight()); } + @Test public void testReadIndexNegativeWithParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -321,11 +349,13 @@ public abstract class ImageReaderAbstractTestCase extends catch (IndexOutOfBoundsException ignore) { } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNull(image); } + @Test public void testReadIndexOutOfBoundsWithParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -339,11 +369,13 @@ public abstract class ImageReaderAbstractTestCase extends catch (IndexOutOfBoundsException ignore) { } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNull(image); } + @Test public void testReadNoInputWithParam() { ImageReader reader = createReader(); // Do not set input @@ -356,11 +388,13 @@ public abstract class ImageReaderAbstractTestCase extends catch (IllegalStateException ignore) { } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNull(image); } + @Test public void testReadWithNewParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -371,13 +405,15 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, new ImageReadParam()); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), data.getDimension(0).width, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), data.getDimension(0).height, image.getHeight()); } + @Test public void testReadWithDefaultParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -388,13 +424,15 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, reader.getDefaultReadParam()); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), data.getDimension(0).width, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), data.getDimension(0).height, image.getHeight()); } + @Test public void testReadWithNullParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -405,13 +443,15 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, null); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), data.getDimension(0).width, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), data.getDimension(0).height, image.getHeight()); } + @Test public void testReadWithSizeParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -426,14 +466,16 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, param); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight()); } } + @Test public void testReadWithSubsampleParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -447,13 +489,15 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, param); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: ", (double) data.getDimension(0).width / 5.0, image.getWidth(), 1.0); assertEquals("Read image has wrong height: ", (double) data.getDimension(0).height / 5.0, image.getHeight(), 1.0); } + @Test public void testReadWithSourceRegionParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -467,13 +511,14 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, param); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight()); } + @Test public void testReadWithSizeAndSourceRegionParam() { // TODO: Is this test correct??? ImageReader reader = createReader(); @@ -493,7 +538,7 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, param); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth()); @@ -501,6 +546,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testReadWithSubsampleAndSourceRegionParam() { // NOTE: The "standard" (com.sun.imageio.plugin.*) ImageReaders pass // this test, so the test should be correct... @@ -517,7 +563,7 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.read(0, param); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), @@ -527,6 +573,7 @@ public abstract class ImageReaderAbstractTestCase extends } + @Test public void testReadAsRenderedImageIndexNegative() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -541,11 +588,12 @@ public abstract class ImageReaderAbstractTestCase extends // Ignore } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNull(image); } + @Test public void testReadAsRenderedImageIndexOutOfBounds() throws IIOException { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -564,11 +612,12 @@ public abstract class ImageReaderAbstractTestCase extends throw e; } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNull(image); } + @Test public void testReadAsRenderedImageNoInput() { ImageReader reader = createReader(); // Do not set input @@ -582,11 +631,12 @@ public abstract class ImageReaderAbstractTestCase extends // Ignore } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNull(image); } + @Test public void testReadAsRenderedImage() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -597,7 +647,7 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.readAsRenderedImage(0, null); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), @@ -606,6 +656,7 @@ public abstract class ImageReaderAbstractTestCase extends data.getDimension(0).height, image.getHeight()); } + @Test public void testReadAsRenderedImageWithDefaultParam() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -616,7 +667,7 @@ public abstract class ImageReaderAbstractTestCase extends image = reader.readAsRenderedImage(0, reader.getDefaultReadParam()); } catch (IOException e) { - fail("Image could not be read: " + e); + failBecause("Image could not be read", e); } assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), @@ -625,12 +676,14 @@ public abstract class ImageReaderAbstractTestCase extends data.getDimension(0).height, image.getHeight()); } + @Test public void testGetDefaultReadParam() { ImageReader reader = createReader(); ImageReadParam param = reader.getDefaultReadParam(); assertNotNull(param); } + @Test public void testGetFormatName() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -645,6 +698,7 @@ public abstract class ImageReaderAbstractTestCase extends assertNotNull(name); } + @Test public void testGetMinIndex() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -659,6 +713,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals(0, num); } + @Test public void testGetMinIndexNoInput() { ImageReader reader = createReader(); int num = 0; @@ -671,6 +726,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals(0, num); } + @Test public void testGetNumImages() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -690,9 +746,15 @@ public abstract class ImageReaderAbstractTestCase extends catch (IOException e) { fail(e.getMessage()); } + assertTrue(num > 0); + assertTrue(data.getImageCount() <= num); + if (data.getImageCount() != num) { + System.err.println("WARNING: Image count not equal to test data count"); + } } + @Test public void testGetNumImagesNoInput() { ImageReader reader = createReader(); int num = -1; @@ -719,6 +781,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals(-1, num); } + @Test public void testGetWidth() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -734,6 +797,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals("Wrong width reported", data.getDimension(0).width, width); } + @Test public void testGetWidthIndexOutOfBounds() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -753,6 +817,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testGetWidthNoInput() { ImageReader reader = createReader(); @@ -769,6 +834,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals("Wrong width reported", 0, width); } + @Test public void testGetHeight() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -784,6 +850,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals("Wrong height reported", data.getDimension(0).height, height); } + @Test public void testGetHeightNoInput() { ImageReader reader = createReader(); @@ -800,6 +867,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals("Wrong height reported", 0, height); } + @Test public void testGetHeightIndexOutOfBounds() { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -819,84 +887,94 @@ public abstract class ImageReaderAbstractTestCase extends } } - public void testGetAspectratio() { + @Test + public void testGetAspectRatio() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - float aspect = 0f; + float aspectRatio = 0f; try { - aspect = reader.getAspectRatio(0); + aspectRatio = reader.getAspectRatio(0); } catch (IOException e) { - fail("Could not read image aspectratio" + e); + fail("Could not read image aspect ratio" + e); } Dimension d = data.getDimension(0); - assertEquals("Wrong aspect aspectratio", d.getWidth() / d.getHeight(), aspect, 0.001); + assertEquals("Wrong aspect aspect ratio", d.getWidth() / d.getHeight(), aspectRatio, 0.001); } - public void testGetAspectratioNoInput() { + @Test + public void testGetAspectRatioNoInput() { ImageReader reader = createReader(); - float aspect = 0f; + float aspectRatio = 0f; try { - aspect = reader.getAspectRatio(0); - fail("aspect read without imput"); + aspectRatio = reader.getAspectRatio(0); + fail("aspect read without input"); } catch (IllegalStateException ignore) { } catch (IOException e) { - fail("Could not read image aspectratio" + e); + fail("Could not read image aspect ratio" + e); } - assertEquals("Wrong aspect aspectratio", 0f, aspect, 0f); + assertEquals("Wrong aspect aspect ratio", 0f, aspectRatio, 0f); } - public void testGetAspectratioIndexOutOfBounds() { + @Test + public void testGetAspectRatioIndexOutOfBounds() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - //float aspectratio = 0f; + //float aspectRatio = 0f; try { // NOTE: Some readers (like the com.sun.imageio stuff) ignores // index in getWidth/getHeight for formats with only one image... - /*aspectratio =*/ reader.getAspectRatio(-1); - //assertEquals("Wrong aspectratio aspectratio", data.getDimension().width / (float) data.getDimension().height, aspectratio, 0f); + /*aspectRatio =*/ reader.getAspectRatio(-1); + //assertEquals("Wrong aspect ratio", data.getDimension().width / (float) data.getDimension().height, aspectRatio, 0f); } - catch (IndexOutOfBoundsException ignore) { + catch (IndexOutOfBoundsException expected) { } catch (IOException e) { - fail("Could not read image aspectratio" + e); + fail("Could not read image aspect ratio" + e); } } - public void testDispose() { - // TODO: Implement + @Test + public void testDisposeBeforeRead() { + ImageReader reader = createReader(); + reader.dispose(); // Just pass with no exceptions } + @Test + public void testDisposeAfterRead() { + ImageReader reader = createReader(); + TestData data = getTestData().get(0); + reader.setInput(data.getInputStream()); + reader.dispose(); // Just pass with no exceptions + } + + @Test public void testAddIIOReadProgressListener() { ImageReader reader = createReader(); - Mock mockListener = new Mock(IIOReadProgressListener.class); - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListener.proxy()); + reader.addIIOReadProgressListener(mock(IIOReadProgressListener.class)); } + @Test public void testAddIIOReadProgressListenerNull() { ImageReader reader = createReader(); reader.addIIOReadProgressListener(null); } + @Test public void testAddIIOReadProgressListenerCallbacks() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class); - String started = "Started"; - mockListener.expects(once()).method("imageStarted").withAnyArguments().id(started); - mockListener.stubs().method("imageProgress").withAnyArguments().after(started); - mockListener.expects(once()).method("imageComplete").withAnyArguments().after(started); - - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListener.proxy()); + IIOReadProgressListener listener = mock(IIOReadProgressListener.class); + reader.addIIOReadProgressListener(listener); try { reader.read(0); @@ -906,36 +984,25 @@ public abstract class ImageReaderAbstractTestCase extends } // At least imageStarted and imageComplete, plus any number of imageProgress - mockListener.verify(); + InOrder ordered = inOrder(listener); + ordered.verify(listener).imageStarted(reader, 0); + ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt()); + ordered.verify(listener).imageComplete(reader); } + @Test public void testMultipleAddIIOReadProgressListenerCallbacks() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class); - String started = "Started"; - mockListener.expects(once()).method("imageStarted").withAnyArguments().id(started); - mockListener.stubs().method("imageProgress").withAnyArguments().after(started); - mockListener.expects(once()).method("imageComplete").withAnyArguments().after(started); + IIOReadProgressListener listener = mock(IIOReadProgressListener.class); + IIOReadProgressListener listenerToo = mock(IIOReadProgressListener.class); + IIOReadProgressListener listenerThree = mock(IIOReadProgressListener.class); - Mock mockListenerToo = new Mock(IIOReadProgressListener.class); - String startedToo = "Started Two"; - mockListenerToo.expects(once()).method("imageStarted").withAnyArguments().id(startedToo); - mockListenerToo.stubs().method("imageProgress").withAnyArguments().after(startedToo); - mockListenerToo.expects(once()).method("imageComplete").withAnyArguments().after(startedToo); - - Mock mockListenerThree = new Mock(IIOReadProgressListener.class); - String startedThree = "Started Three"; - mockListenerThree.expects(once()).method("imageStarted").withAnyArguments().id(startedThree); - mockListenerThree.stubs().method("imageProgress").withAnyArguments().after(startedThree); - mockListenerThree.expects(once()).method("imageComplete").withAnyArguments().after(startedThree); - - - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListener.proxy()); - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListenerToo.proxy()); - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListenerThree.proxy()); + reader.addIIOReadProgressListener(listener); + reader.addIIOReadProgressListener(listenerToo); + reader.addIIOReadProgressListener(listenerThree); try { reader.read(0); @@ -945,28 +1012,40 @@ public abstract class ImageReaderAbstractTestCase extends } // At least imageStarted and imageComplete, plus any number of imageProgress - mockListener.verify(); - mockListenerToo.verify(); - mockListenerThree.verify(); + InOrder ordered = inOrder(listener, listenerToo, listenerThree); + + ordered.verify(listener).imageStarted(reader, 0); + ordered.verify(listenerToo).imageStarted(reader, 0); + ordered.verify(listenerThree).imageStarted(reader, 0); + + ordered.verify(listener, atLeastOnce()).imageProgress(eq(reader), anyInt()); + ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(reader), anyInt()); + ordered.verify(listenerThree, atLeastOnce()).imageProgress(eq(reader), anyInt()); + + ordered.verify(listener).imageComplete(reader); + ordered.verify(listenerToo).imageComplete(reader); + ordered.verify(listenerThree).imageComplete(reader); } + @Test public void testRemoveIIOReadProgressListenerNull() { ImageReader reader = createReader(); reader.removeIIOReadProgressListener(null); } + @Test public void testRemoveIIOReadProgressListenerNone() { ImageReader reader = createReader(); - Mock mockListener = new Mock(IIOReadProgressListener.class); - reader.removeIIOReadProgressListener((IIOReadProgressListener) mockListener.proxy()); + reader.removeIIOReadProgressListener(mock(IIOReadProgressListener.class)); } + @Test public void testRemoveIIOReadProgressListener() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class); - IIOReadProgressListener listener = (IIOReadProgressListener) mockListener.proxy(); + + IIOReadProgressListener listener = mock(IIOReadProgressListener.class); reader.addIIOReadProgressListener(listener); reader.removeIIOReadProgressListener(listener); @@ -978,23 +1057,20 @@ public abstract class ImageReaderAbstractTestCase extends } // Should not have called any methods... - mockListener.verify(); + verifyZeroInteractions(listener); } + @Test public void testRemoveIIOReadProgressListenerMultiple() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class, "Listener1"); - IIOReadProgressListener listener = (IIOReadProgressListener) mockListener.proxy(); + IIOReadProgressListener listener = mock(IIOReadProgressListener.class, "Listener1"); reader.addIIOReadProgressListener(listener); - Mock mockListenerToo = new Mock(IIOReadProgressListener.class, "Listener2"); - mockListenerToo.expects(once()).method("imageStarted").with(eq(reader), eq(0)); - mockListenerToo.stubs().method("imageProgress").withAnyArguments(); - mockListenerToo.expects(once()).method("imageComplete").with(eq(reader)); - IIOReadProgressListener listenerToo = (IIOReadProgressListener) mockListenerToo.proxy(); + + IIOReadProgressListener listenerToo = mock(IIOReadProgressListener.class, "Listener2"); reader.addIIOReadProgressListener(listenerToo); reader.removeIIOReadProgressListener(listener); @@ -1006,18 +1082,23 @@ public abstract class ImageReaderAbstractTestCase extends fail("Could not read image"); } - // Should not have called any methods... - mockListener.verify(); - mockListenerToo.verify(); + // Should not have called any methods on listener1... + verifyZeroInteractions(listener); + + InOrder ordered = inOrder(listenerToo); + ordered.verify(listenerToo).imageStarted(reader, 0); + ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(reader), anyInt()); + ordered.verify(listenerToo).imageComplete(reader); } + @Test public void testRemoveAllIIOReadProgressListeners() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class); - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListener.proxy()); + IIOReadProgressListener listener = mock(IIOReadProgressListener.class); + reader.addIIOReadProgressListener(listener); reader.removeAllIIOReadProgressListeners(); @@ -1029,19 +1110,20 @@ public abstract class ImageReaderAbstractTestCase extends } // Should not have called any methods... - mockListener.verify(); + verifyZeroInteractions(listener); } + @Test public void testRemoveAllIIOReadProgressListenersMultiple() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class); - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListener.proxy()); + IIOReadProgressListener listener = mock(IIOReadProgressListener.class); + reader.addIIOReadProgressListener(listener); - Mock mockListenerToo = new Mock(IIOReadProgressListener.class); - reader.addIIOReadProgressListener((IIOReadProgressListener) mockListenerToo.proxy()); + IIOReadProgressListener listenerToo = mock(IIOReadProgressListener.class); + reader.addIIOReadProgressListener(listenerToo); reader.removeAllIIOReadProgressListeners(); @@ -1053,90 +1135,80 @@ public abstract class ImageReaderAbstractTestCase extends } // Should not have called any methods... - mockListener.verify(); - mockListenerToo.verify(); + verifyZeroInteractions(listener); + verifyZeroInteractions(listenerToo); } + @Test public void testAbort() { final ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); - Mock mockListener = new Mock(IIOReadProgressListener.class, "Progress1"); - mockListener.stubs().method("imageStarted").withAnyArguments(); - mockListener.stubs().method("imageProgress").withAnyArguments(); - mockListener.expects(once()).method("readAborted").with(eq(reader)); - mockListener.stubs().method("imageComplete").withAnyArguments(); - IIOReadProgressListener listener = (IIOReadProgressListener) mockListener.proxy(); + IIOReadProgressListener listener = mock(IIOReadProgressListener.class, "Progress1"); reader.addIIOReadProgressListener(listener); - Mock mockListenerToo = new Mock(IIOReadProgressListener.class, "Progress2"); - mockListenerToo.stubs().method("imageStarted").withAnyArguments(); - mockListenerToo.stubs().method("imageProgress").withAnyArguments(); - mockListenerToo.expects(once()).method("readAborted").with(eq(reader)); - mockListenerToo.stubs().method("imageComplete").withAnyArguments(); - IIOReadProgressListener listenerToo = (IIOReadProgressListener) mockListenerToo.proxy(); + IIOReadProgressListener listenerToo = mock(IIOReadProgressListener.class, "Progress2"); reader.addIIOReadProgressListener(listenerToo); // Create a listener that just makes the reader abort immediately... - Mock abortingListener = new Mock(IIOReadProgressListener.class, "Aborter"); - abortingListener.stubs().method("readAborted").withAnyArguments(); - abortingListener.stubs().method("imageComplete").withAnyArguments(); - Stub abort = new Stub() { - public Object invoke(Invocation pInvocation) throws Throwable { + IIOReadProgressListener abortingListener = mock(IIOReadProgressListener.class, "Aborter"); + Answer abort = new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { reader.abort(); return null; } - - public StringBuffer describeTo(StringBuffer pStringBuffer) { - pStringBuffer.append("aborting"); - return pStringBuffer; - } }; - abortingListener.stubs().method("imageProgress").will(abort); - abortingListener.stubs().method("imageStarted").will(abort); + doAnswer(abort).when(abortingListener).imageStarted(any(ImageReader.class), anyInt()); + doAnswer(abort).when(abortingListener).imageProgress(any(ImageReader.class), anyInt()); - reader.addIIOReadProgressListener((IIOReadProgressListener) abortingListener.proxy()); + reader.addIIOReadProgressListener(abortingListener); try { reader.read(0); } catch (IOException e) { - fail("Could not read image: " + e.getMessage() ); + failBecause("Image could not be read", e); } - mockListener.verify(); - mockListenerToo.verify(); + verify(listener).readAborted(reader); + verify(listenerToo).readAborted(reader); } + @Test public void testGetTypeSpecifiers() throws IOException { final ImageReader reader = createReader(); - TestData data = getTestData().get(0); - reader.setInput(data.getInputStream()); + for (TestData data : getTestData()) { + reader.setInput(data.getInputStream()); - ImageTypeSpecifier rawType = reader.getRawImageType(0); - assertNotNull(rawType); - - Iterator types = reader.getImageTypes(0); - - assertNotNull(types); - assertTrue(types.hasNext()); - - // TODO: This might fail even though the specifiers are obviously equal, if the - // color spaces they use are not the SAME instance, as ColorSpace uses identity equals - // and Interleaved ImageTypeSpecifiers are only equal if color spaces are equal... - boolean rawFound = false; - while (types.hasNext()) { - ImageTypeSpecifier type = types.next(); - if (type.equals(rawType)) { - rawFound = true; - break; + ImageTypeSpecifier rawType = reader.getRawImageType(0); + if (rawType == null && allowsNullRawImageType()) { + continue; } - } + assertNotNull(rawType); - assertTrue("ImageTypeSepcifier from getRawImageType should be in the iterator from getImageTypes", rawFound); + Iterator types = reader.getImageTypes(0); + + assertNotNull(types); + assertTrue(types.hasNext()); + + // TODO: This might fail even though the specifiers are obviously equal, if the + // color spaces they use are not the SAME instance, as ColorSpace uses identity equals + // and Interleaved ImageTypeSpecifiers are only equal if color spaces are equal... + boolean rawFound = false; + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + if (type.equals(rawType)) { + rawFound = true; + break; + } + } + + assertTrue("ImageTypeSepcifier from getRawImageType should be in the iterator from getImageTypes", rawFound); + } } + @Test public void testSetDestination() throws IOException { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1150,12 +1222,19 @@ public abstract class ImageReaderAbstractTestCase extends BufferedImage destination = type.createBufferedImage(50, 50); param.setDestination(destination); - BufferedImage result = reader.read(0, param); + BufferedImage result = null; + try { + result = reader.read(0, param); + } + catch (Exception e) { + failBecause("Could not read " + data.getInput() + " with explicit destination " + destination, e); + } assertSame(destination, result); } } + @Test public void testSetDestinationRaw() throws IOException { ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1164,14 +1243,27 @@ public abstract class ImageReaderAbstractTestCase extends ImageReadParam param = reader.getDefaultReadParam(); ImageTypeSpecifier type = reader.getRawImageType(0); - BufferedImage destination = type.createBufferedImage(reader.getWidth(0), reader.getHeight(0)); - param.setDestination(destination); - BufferedImage result = reader.read(0, param); + if (type != null) { + BufferedImage destination = type.createBufferedImage(reader.getWidth(0), reader.getHeight(0)); + param.setDestination(destination); - assertSame(destination, result); + BufferedImage result = null; + try { + result = reader.read(0, param); + } + catch (Exception e) { + failBecause("Image could not be read", e); + } + + assertSame(destination, result); + } + else { + System.err.println("WARNING: Test skipped due to reader.getRawImageType(0) returning null"); + } } + @Test public void testSetDestinationIllegal() throws IOException { final ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1189,6 +1281,7 @@ public abstract class ImageReaderAbstractTestCase extends // NOTE: We allow the reader to read, as it's inconvenient to test all possible cases. // However, it may NOT fail with any other exception in that case. + // TODO: Special case for BufferedImage type 2/3 and 6/7 System.err.println("WARNING: Reader does not throw exception with non-declared destination: " + destination); // Test that the destination is really taken into account @@ -1213,6 +1306,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testSetDestinationTypeIllegal() throws IOException { final ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1258,7 +1352,7 @@ public abstract class ImageReaderAbstractTestCase extends boolean removed = illegalTypes.remove(valid); // TODO: 4BYTE_ABGR (6) and 4BYTE_ABGR_PRE (7) is essentially the same type... - // !#$#§%$! ImageTypeSpecifier.equals is not well-defined + // !#$#�%$! ImageTypeSpecifier.equals is not well-defined if (!removed) { for (Iterator iterator = illegalTypes.iterator(); iterator.hasNext();) { ImageTypeSpecifier illegalType = iterator.next(); @@ -1274,6 +1368,7 @@ public abstract class ImageReaderAbstractTestCase extends // TODO: Test dest offset + destination set? // TODO: Test that destination offset is used for image data, not just image dimensions... + @Test public void testSetDestinationOffset() throws IOException { final ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1290,6 +1385,7 @@ public abstract class ImageReaderAbstractTestCase extends assertEquals(reader.getHeight(0) + point.y, image.getHeight()); } + @Test public void testSetDestinationOffsetNull() throws IOException { final ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1305,6 +1401,7 @@ public abstract class ImageReaderAbstractTestCase extends } } + @Test public void testSetDestinationType() throws IOException { final ImageReader reader = createReader(); TestData data = getTestData().get(0); @@ -1317,7 +1414,13 @@ public abstract class ImageReaderAbstractTestCase extends ImageTypeSpecifier type = types.next(); param.setDestinationType(type); - BufferedImage result = reader.read(0, param); + BufferedImage result = null; + try { + result = reader.read(0, param); + } + catch (Exception e) { + failBecause("Could not read " + data.getInput() + " with explicit destination type " + type, e); + } assertEquals(type.getColorModel(), result.getColorModel()); @@ -1338,14 +1441,85 @@ public abstract class ImageReaderAbstractTestCase extends } } -// public void testSetDestinationBands() throws IOException { -// throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement -// } -// -// public void testSetSourceBands() throws IOException { -// throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement -// } + @Test + public void testNotBadCaching() throws IOException { + T reader = createReader(); + TestData data = getTestData().get(0); + reader.setInput(data.getInputStream()); + + BufferedImage one = reader.read(0); + BufferedImage two = reader.read(0); + + assertNotSame("Multiple reads return same (mutable) image", one, two); + Graphics2D g = one.createGraphics(); + try { + g.setColor(Color.WHITE); + g.setXORMode(Color.BLACK); + g.fillRect(0, 0, one.getWidth(), one.getHeight()); + } + finally { + g.dispose(); + } + + assertTrue(one.getRGB(0, 0) != two.getRGB(0, 0)); + } + + @Test + public void testNotBadCachingThumbnails() throws IOException { + T reader = createReader(); + + if (reader.readerSupportsThumbnails()) { + for (TestData data : getTestData()) { + reader.setInput(data.getInputStream()); + + int images = reader.getNumImages(true); + for (int i = 0; i < images; i++) { + int thumbnails = reader.getNumThumbnails(0); + + for (int j = 0; j < thumbnails; j++) { + BufferedImage one = reader.readThumbnail(i, j); + BufferedImage two = reader.readThumbnail(i, j); + + assertNotSame("Multiple reads return same (mutable) image", one, two); + + Graphics2D g = one.createGraphics(); + try { + g.setColor(Color.WHITE); + g.setXORMode(Color.BLACK); + g.fillRect(0, 0, one.getWidth(), one.getHeight()); + } + finally { + g.dispose(); + } + + assertTrue(one.getRGB(0, 0) != two.getRGB(0, 0)); + } + + if (thumbnails > 0) { + // We've tested thumbnails, let's get out of here + return; + } + } + } + + fail("No thumbnails tested for reader that supports thumbnails."); + } + } + + @Ignore("TODO: Implement") + @Test + public void testSetDestinationBands() throws IOException { + throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement + } + + @Ignore("TODO: Implement") + @Test + public void testSetSourceBands() throws IOException { + throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement + } + + @Test public void testProviderAndMetadataFormatNamesMatch() throws IOException { ImageReaderSpi provider = createProvider(); @@ -1363,15 +1537,14 @@ public abstract class ImageReaderAbstractTestCase extends } } - protected URL getClassLoaderResource(final String pName) { return getClass().getResource(pName); } static final protected class TestData { - private final Object mInput; - private final List mSizes; - private final List mImages; + private final Object input; + private final List sizes; + private final List images; public TestData(final Object pInput, final Dimension... pSizes) { this(pInput, Arrays.asList(pSizes), null); @@ -1386,8 +1559,8 @@ public abstract class ImageReaderAbstractTestCase extends throw new IllegalArgumentException("input == null"); } - mSizes = new ArrayList(); - mImages = new ArrayList(); + sizes = new ArrayList(); + images = new ArrayList(); List sizes = pSizes; if (sizes == null) { @@ -1413,46 +1586,47 @@ public abstract class ImageReaderAbstractTestCase extends } } - mSizes.addAll(sizes); + this.sizes.addAll(sizes); if (pImages != null) { - mImages.addAll(pImages); + images.addAll(pImages); } - mInput = pInput; + input = pInput; } public Object getInput() { - return mInput; + return input; } public ImageInputStream getInputStream() { try { - ImageInputStream stream = ImageIO.createImageInputStream(mInput); - assertNotNull("Could not create ImageInputStream for input: " + mInput, stream); + ImageInputStream stream = ImageIO.createImageInputStream(input); + assertNotNull("Could not create ImageInputStream for input: " + input, stream); + return stream; } catch (IOException e) { - fail("Could not create ImageInputStream for input: " + mInput + - "\n caused by: " + e.getMessage()); + failBecause("Could not create ImageInputStream for input: " + input, e); } + return null; } public int getImageCount() { - return mSizes.size(); + return sizes.size(); } public Dimension getDimension(final int pIndex) { - return mSizes.get(pIndex); + return sizes.get(pIndex); } public BufferedImage getImage(final int pIndex) { - return mImages.get(pIndex); + return images.get(pIndex); } @Override public String toString() { - return getClass().getSimpleName() + ": " + String.valueOf(mInput); + return getClass().getSimpleName() + ": " + String.valueOf(input); } } } diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java index 13373a4e..e6fa78ea 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java @@ -28,16 +28,24 @@ package com.twelvemonkeys.imageio.util; -import org.jmock.Mock; -import org.jmock.cglib.MockObjectTestCase; +import org.junit.Test; +import org.mockito.InOrder; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.event.IIOWriteProgressListener; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.*; /** * ImageReaderAbstractTestCase class description. @@ -46,12 +54,38 @@ import java.io.IOException; * @author last modified by $Author: haku $ * @version $Id: ImageReaderAbstractTestCase.java,v 1.0 18.nov.2004 17:38:33 haku Exp $ */ -public abstract class ImageWriterAbstractTestCase extends MockObjectTestCase { +public abstract class ImageWriterAbstractTestCase { protected abstract ImageWriter createImageWriter(); - protected abstract RenderedImage getTestData(); + protected abstract List getTestData(); + protected static BufferedImage drawSomething(final BufferedImage image) { + Graphics2D g = image.createGraphics(); + try { + int width = image.getWidth(); + int height = image.getHeight(); + + g.clearRect(0, 0, width, height); + g.setPaint(new LinearGradientPaint(0, 0, width, 0, new float[] {0.2f, 1}, new Color[] {new Color(0x0, true), Color.BLUE})); + g.fillRect(0, 0, width, height); + g.setPaint(new LinearGradientPaint(0, 0, 0, height, new float[] {0.2f, 1}, new Color[] {new Color(0x0, true), Color.RED})); + g.fillRect(0, 0, width, height); + g.setPaint(new LinearGradientPaint(0, 0, 0, height, new float[] {0, 1}, new Color[] {new Color(0x00ffffff, true), Color.WHITE})); + g.fill(new Polygon(new int[] {0, width, width}, new int[] {0, height, 0}, 3)); + } + finally { + g.dispose(); + } + + return image; + } + + protected final RenderedImage getTestData(final int index) { + return getTestData().get(index); + } + + @Test public void testSetOutput() throws IOException { // Should just pass with no exceptions ImageWriter writer = createImageWriter(); @@ -59,6 +93,7 @@ public abstract class ImageWriterAbstractTestCase extends MockObjectTestCase { writer.setOutput(ImageIO.createImageOutputStream(new ByteArrayOutputStream())); } + @Test public void testSetOutputNull() { // Should just pass with no exceptions ImageWriter writer = createImageWriter(); @@ -66,40 +101,35 @@ public abstract class ImageWriterAbstractTestCase extends MockObjectTestCase { writer.setOutput(null); } + @Test public void testWrite() throws IOException { ImageWriter writer = createImageWriter(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - writer.setOutput(ImageIO.createImageOutputStream(buffer)); - try { - writer.write(getTestData()); + + for (RenderedImage testData : getTestData()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); + writer.setOutput(stream); + + try { + writer.write(drawSomething((BufferedImage) testData)); + } + catch (IOException e) { + fail(e.getMessage()); + } + finally { + stream.close(); // Force data to be written + } + + assertTrue("No image data written", buffer.size() > 0); } - catch (IOException e) { - fail(e.getMessage()); - } - assertTrue("No image data written", buffer.size() > 0); - } - - public void testWrite2() { - // Note: There's a difference between new ImageOutputStreamImpl and - // ImageIO.createImageOutputStream... Make sure writers handle both - // cases - ImageWriter writer = createImageWriter(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - try { - writer.setOutput(ImageIO.createImageOutputStream(buffer)); - writer.write(getTestData()); - } - catch (IOException e) { - fail(e.getMessage()); - } - - assertTrue("No image data written", buffer.size() > 0); } + @Test public void testWriteNull() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); + try { writer.write((RenderedImage) null); } @@ -108,21 +138,23 @@ public abstract class ImageWriterAbstractTestCase extends MockObjectTestCase { catch (IOException e) { fail(e.getMessage()); } + assertTrue("Image data written", buffer.size() == 0); } + @Test(expected = IllegalStateException.class) public void testWriteNoOutput() { ImageWriter writer = createImageWriter(); + try { - writer.write(getTestData()); - } - catch (IllegalStateException ignore) { + writer.write(getTestData(0)); } catch (IOException e) { fail(e.getMessage()); } } + @Test public void testGetDefaultWriteParam() { ImageWriter writer = createImageWriter(); ImageWriteParam param = writer.getDefaultWriteParam(); @@ -132,191 +164,190 @@ public abstract class ImageWriterAbstractTestCase extends MockObjectTestCase { // TODO: Test writing with params // TODO: Source region and subsampling at least + @Test public void testAddIIOWriteProgressListener() { ImageWriter writer = createImageWriter(); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + writer.addIIOWriteProgressListener(mock(IIOWriteProgressListener.class)); } + @Test public void testAddIIOWriteProgressListenerNull() { ImageWriter writer = createImageWriter(); writer.addIIOWriteProgressListener(null); } + @Test public void testAddIIOWriteProgressListenerCallbacks() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - String started = "Started"; - mockListener.expects(once()).method("imageStarted").withAnyArguments().id(started); - mockListener.stubs().method("imageProgress").withAnyArguments().after(started); - mockListener.expects(once()).method("imageComplete").withAnyArguments().after(started); - - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + IIOWriteProgressListener listener = mock(IIOWriteProgressListener.class); + writer.addIIOWriteProgressListener(listener); try { - writer.write(getTestData()); + writer.write(getTestData(0)); } catch (IOException e) { fail("Could not write image"); } // At least imageStarted and imageComplete, plus any number of imageProgress - mockListener.verify(); + InOrder ordered = inOrder(listener); + ordered.verify(listener).imageStarted(writer, 0); + ordered.verify(listener, atLeastOnce()).imageProgress(eq(writer), anyInt()); + ordered.verify(listener).imageComplete(writer); } + @Test public void testMultipleAddIIOWriteProgressListenerCallbacks() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - String started = "Started"; - mockListener.expects(once()).method("imageStarted").withAnyArguments().id(started); - mockListener.stubs().method("imageProgress").withAnyArguments().after(started); - mockListener.expects(once()).method("imageComplete").withAnyArguments().after(started); + IIOWriteProgressListener listener = mock(IIOWriteProgressListener.class); + IIOWriteProgressListener listenerToo = mock(IIOWriteProgressListener.class); + IIOWriteProgressListener listenerThree = mock(IIOWriteProgressListener.class); - Mock mockListenerToo = new Mock(IIOWriteProgressListener.class); - String startedToo = "Started Two"; - mockListenerToo.expects(once()).method("imageStarted").withAnyArguments().id(startedToo); - mockListenerToo.stubs().method("imageProgress").withAnyArguments().after(startedToo); - mockListenerToo.expects(once()).method("imageComplete").withAnyArguments().after(startedToo); - - Mock mockListenerThree = new Mock(IIOWriteProgressListener.class); - String startedThree = "Started Three"; - mockListenerThree.expects(once()).method("imageStarted").withAnyArguments().id(startedThree); - mockListenerThree.stubs().method("imageProgress").withAnyArguments().after(startedThree); - mockListenerThree.expects(once()).method("imageComplete").withAnyArguments().after(startedThree); - - - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListenerToo.proxy()); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListenerThree.proxy()); + writer.addIIOWriteProgressListener(listener); + writer.addIIOWriteProgressListener(listenerToo); + writer.addIIOWriteProgressListener(listenerThree); try { - writer.write(getTestData()); + writer.write(getTestData(0)); } catch (IOException e) { fail("Could not write image"); } // At least imageStarted and imageComplete, plus any number of imageProgress - mockListener.verify(); - mockListenerToo.verify(); - mockListenerThree.verify(); + InOrder ordered = inOrder(listener, listenerToo, listenerThree); + + ordered.verify(listener).imageStarted(writer, 0); + ordered.verify(listenerToo).imageStarted(writer, 0); + ordered.verify(listenerThree).imageStarted(writer, 0); + + ordered.verify(listener, atLeastOnce()).imageProgress(eq(writer), anyInt()); + ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(writer), anyInt()); + ordered.verify(listenerThree, atLeastOnce()).imageProgress(eq(writer), anyInt()); + + ordered.verify(listener).imageComplete(writer); + ordered.verify(listenerToo).imageComplete(writer); + ordered.verify(listenerThree).imageComplete(writer); } - + @Test public void testRemoveIIOWriteProgressListenerNull() { ImageWriter writer = createImageWriter(); writer.removeIIOWriteProgressListener(null); } + @Test public void testRemoveIIOWriteProgressListenerNone() { ImageWriter writer = createImageWriter(); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - writer.removeIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + writer.removeIIOWriteProgressListener(mock(IIOWriteProgressListener.class)); } + @Test public void testRemoveIIOWriteProgressListener() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - IIOWriteProgressListener listener = (IIOWriteProgressListener) mockListener.proxy(); + IIOWriteProgressListener listener = mock(IIOWriteProgressListener.class); writer.addIIOWriteProgressListener(listener); writer.removeIIOWriteProgressListener(listener); try { - writer.write(getTestData()); + writer.write(getTestData(0)); } catch (IOException e) { fail("Could not write image"); } // Should not have called any methods... - mockListener.verify(); + verifyZeroInteractions(listener); } + @Test public void testRemoveIIOWriteProgressListenerMultiple() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); + IIOWriteProgressListener listener = mock(IIOWriteProgressListener.class); + writer.addIIOWriteProgressListener(listener); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + IIOWriteProgressListener listenerToo = mock(IIOWriteProgressListener.class); + writer.addIIOWriteProgressListener(listenerToo); - Mock mockListenerToo = new Mock(IIOWriteProgressListener.class); - mockListenerToo.stubs().method("imageStarted").withAnyArguments(); - mockListenerToo.stubs().method("imageProgress").withAnyArguments(); - mockListenerToo.stubs().method("imageComplete").withAnyArguments(); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListenerToo.proxy()); - - writer.removeIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + writer.removeIIOWriteProgressListener(listener); try { - writer.write(getTestData()); + writer.write(getTestData(0)); } catch (IOException e) { fail("Could not write image"); } // Should not have called any methods... - mockListener.verify(); - mockListenerToo.verify(); + verifyZeroInteractions(listener); + + // At least imageStarted and imageComplete, plus any number of imageProgress + InOrder ordered = inOrder(listenerToo); + ordered.verify(listenerToo).imageStarted(writer, 0); + ordered.verify(listenerToo, atLeastOnce()).imageProgress(eq(writer), anyInt()); + ordered.verify(listenerToo).imageComplete(writer); + } - + @Test public void testRemoveAllIIOWriteProgressListeners() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + IIOWriteProgressListener listener = mock(IIOWriteProgressListener.class); + writer.addIIOWriteProgressListener(listener); writer.removeAllIIOWriteProgressListeners(); try { - writer.write(getTestData()); + writer.write(getTestData(0)); } catch (IOException e) { fail("Could not write image"); } // Should not have called any methods... - mockListener.verify(); + verifyZeroInteractions(listener); } + @Test public void testRemoveAllIIOWriteProgressListenersMultiple() throws IOException { ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); writer.setOutput(ImageIO.createImageOutputStream(buffer)); - Mock mockListener = new Mock(IIOWriteProgressListener.class); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListener.proxy()); + IIOWriteProgressListener listener = mock(IIOWriteProgressListener.class); + writer.addIIOWriteProgressListener(listener); - Mock mockListenerToo = new Mock(IIOWriteProgressListener.class); - writer.addIIOWriteProgressListener((IIOWriteProgressListener) mockListenerToo.proxy()); + IIOWriteProgressListener listenerToo = mock(IIOWriteProgressListener.class); + writer.addIIOWriteProgressListener(listenerToo); writer.removeAllIIOWriteProgressListeners(); try { - writer.write(getTestData()); + writer.write(getTestData(0)); } catch (IOException e) { fail("Could not write image"); } // Should not have called any methods... - mockListener.verify(); - mockListenerToo.verify(); + verifyZeroInteractions(listener); + verifyZeroInteractions(listenerToo); } - } \ No newline at end of file diff --git a/imageio/imageio-core/todo.txt b/imageio/imageio-core/todo.txt index c2644570..4e48a7fa 100755 --- a/imageio/imageio-core/todo.txt +++ b/imageio/imageio-core/todo.txt @@ -1,7 +1,7 @@ - Rename to imageio-common? - Separate modules for more for more plugins - - The BMP reader spports some special formats not supported by Sun reader - - PNM package is pretty complete, but useless, as it's provided by Sun? Licencse? + - The BMP reader supports some special formats not supported by Sun reader + - PNM package is pretty complete, but useless, as it's provided by Sun? License? - WBMP? - XBM? DONE: diff --git a/imageio/imageio-icns/pom.xml b/imageio/imageio-icns/pom.xml new file mode 100644 index 00000000..6fd2a3af --- /dev/null +++ b/imageio/imageio-icns/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.0-SNAPSHOT + + imageio-icns + TwelveMonkeys :: ImageIO :: ICNS plugin + ImageIO plugin for Apple Icon Image (icns) format. + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + tests + + + diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java new file mode 100644 index 00000000..723d7a51 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +/** + * ICNS + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNS.java,v 1.0 25.10.11 19:10 haraldk Exp$ + */ +interface ICNS { + /** Resource header size (8). */ + int RESOURCE_HEADER_SIZE = 8; + + /** ICNS magic identifier ("icns"). */ + int MAGIC = ('i' << 24) + ('c' << 16) + ('n' << 8) + 's'; + + /** 32×32 1-bit mono icon. */ + int ICON = ('I' << 24) + ('C' << 16) + ('O' << 8) + 'N'; + /** 32×32 1-bit mono icon with 1-bit mask. */ + int ICN_ = ('I' << 24) + ('C' << 16) + ('N' << 8) + '#'; + + /** 16×12 1 bit mask. */ + int icm_ = ('i' << 24) + ('c' << 16) + ('m' << 8) + '#'; + /** 16×12 4 bit icon. */ + int icm4 = ('i' << 24) + ('c' << 16) + ('m' << 8) + '4'; + /** 16×12 8 bit icon. */ + int icm8 = ('i' << 24) + ('c' << 16) + ('m' << 8) + '8'; + + /** 16×16 1-bit icon with 1-bit mask. */ + int ics_ = ('i' << 24) + ('c' << 16) + ('s' << 8) + '#'; + /** 16×16 4-bit icon. */ + int ics4 = ('i' << 24) + ('c' << 16) + ('s' << 8) + '4'; + /** 16×16 8-bit icon. */ + int ics8 = ('i' << 24) + ('c' << 16) + ('s' << 8) + '8'; + /** 16×16 24-bit icon, possibly run-length compressed. */ + int is32 = ('i' << 24) + ('s' << 16) + ('3' << 8) + '2'; + /** 16x16 8-bit mask. */ + int s8mk = ('s' << 24) + ('8' << 16) + ('m' << 8) + 'k'; + + /** 32×32 4-bit icon. */ + int icl4 = ('i' << 24) + ('c' << 16) + ('l' << 8) + '4'; + /** 32×32 8-bit icon. */ + int icl8 = ('i' << 24) + ('c' << 16) + ('l' << 8) + '8'; + /** 32×32 24-bit icon, possibly run-length compressed. */ + int il32 = ('i' << 24) + ('l' << 16) + ('3' << 8) + '2'; + /** 32×32 8-bit mask. */ + int l8mk = ('l' << 24) + ('8' << 16) + ('m' << 8) + 'k'; + + /** 48×48 1-bit icon with 1 bit mask. */ + int ich_ = ('i' << 24) + ('c' << 16) + ('h' << 8) + '#'; + /** 48×48 4-bit icon. */ + int ich4 = ('i' << 24) + ('c' << 16) + ('h' << 8) + '4'; + /** 48×48 8-bit icon. */ + int ich8 = ('i' << 24) + ('c' << 16) + ('h' << 8) + '8'; + /** 48×48 24-bit icon, possibly run-length compressed. */ + int ih32 = ('i' << 24) + ('h' << 16) + ('3' << 8) + '2'; + /** 48×48 8-bit mask. */ + int h8mk = ('h' << 24) + ('8' << 16) + ('m' << 8) + 'k'; + + /** 128×128 24-bit icon, possibly run-length compressed. */ + int it32 = ('i' << 24) + ('t' << 16) + ('3' << 8) + '2'; + /** 128×128 8-bit mask. */ + int t8mk = ('t' << 24) + ('8' << 16) + ('m' << 8) + 'k'; + + /** 256×256 JPEG 2000 or PNG icon (10.x+). */ + int ic08 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '8'; + + /** 512×512 JPEG 2000 or PNG icon (10.x+). */ + int ic09 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '9'; + + /** 1024×1024 PNG icon (10.7+). */ + int ic10 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '0'; + + /** Unknown (Version). */ + int icnV = ('i' << 24) + ('c' << 16) + ('n' << 8) + 'V'; + + /** Unknown (Table of Contents). */ + int TOC_ = ('T' << 24) + ('O' << 16) + ('C' << 8) + ' '; + + /** JPEG 2000 magic header. */ + byte[] JPEG_2000_MAGIC = new byte[] {0x00, 0x00, 0x00, 0x0C, 'j', 'P', 0x20, 0x20, 0x0D, 0x0A, (byte) 0x87, 0x0A}; + + /** PNG magic header. */ + byte[] PNG_MAGIC = new byte[] {(byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G', 0x0d, 0x0a, 0x1a, 0x0a}; +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS1BitColorModel.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS1BitColorModel.java new file mode 100644 index 00000000..12a42ff4 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS1BitColorModel.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; + +/** + * ICNS1BitColorModel + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNS1BitColorModel.java,v 1.0 07.11.11 10:49 haraldk Exp$ + */ +final class ICNS1BitColorModel extends IndexColorModel { + private static final int[] CMAP = { + // Inverted from default Java 1 bit... + 0xffffffff, 0xff000000 + }; + + static final IndexColorModel INSTANCE = new ICNS1BitColorModel(); + + private ICNS1BitColorModel() { + super(1, 2, CMAP, 0, false, -1, DataBuffer.TYPE_BYTE); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS4BitColorModel.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS4BitColorModel.java new file mode 100644 index 00000000..10a90f18 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS4BitColorModel.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; + +/** + * ICNS4BitColorModel + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNS4BitColorModel.java,v 1.0 07.11.11 10:49 haraldk Exp$ + */ +final class ICNS4BitColorModel extends IndexColorModel { + private static final int[] CMAP = { + 0xffffffff, 0xfffcf305, 0xffff6402, 0xffdd0806, 0xfff20884, 0xff4600a5, 0xff0000d4, 0xff02abea, + 0xff1fb714, 0xff006411, 0xff562c05, 0xff90713a, 0xffc0c0c0, 0xff808080, 0xff404040, 0xff000000 + }; + + static final IndexColorModel INSTANCE = new ICNS4BitColorModel(); + + private ICNS4BitColorModel() { + super(4, 16, CMAP, 0, false, -1, DataBuffer.TYPE_BYTE); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS8BitColorModel.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS8BitColorModel.java new file mode 100644 index 00000000..19d12f86 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNS8BitColorModel.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; + +/** + * ICNS8BitColorModel + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNS8BitColorModel.java,v 1.0 07.11.11 10:49 haraldk Exp$ + */ +final class ICNS8BitColorModel extends IndexColorModel { + private static final int[] CMAP = { + 0xffffffff, 0xffffffcc, 0xffffff99, 0xffffff66, 0xffffff33, 0xffffff00, 0xffffccff, 0xffffcccc, + 0xffffcc99, 0xffffcc66, 0xffffcc33, 0xffffcc00, 0xffff99ff, 0xffff99cc, 0xffff9999, 0xffff9966, + 0xffff9933, 0xffff9900, 0xffff66ff, 0xffff66cc, 0xffff6699, 0xffff6666, 0xffff6633, 0xffff6600, + 0xffff33ff, 0xffff33cc, 0xffff3399, 0xffff3366, 0xffff3333, 0xffff3300, 0xffff00ff, 0xffff00cc, + 0xffff0099, 0xffff0066, 0xffff0033, 0xffff0000, 0xffccffff, 0xffccffcc, 0xffccff99, 0xffccff66, + 0xffccff33, 0xffccff00, 0xffccccff, 0xffcccccc, 0xffcccc99, 0xffcccc66, 0xffcccc33, 0xffcccc00, + 0xffcc99ff, 0xffcc99cc, 0xffcc9999, 0xffcc9966, 0xffcc9933, 0xffcc9900, 0xffcc66ff, 0xffcc66cc, + 0xffcc6699, 0xffcc6666, 0xffcc6633, 0xffcc6600, 0xffcc33ff, 0xffcc33cc, 0xffcc3399, 0xffcc3366, + 0xffcc3333, 0xffcc3300, 0xffcc00ff, 0xffcc00cc, 0xffcc0099, 0xffcc0066, 0xffcc0033, 0xffcc0000, + 0xff99ffff, 0xff99ffcc, 0xff99ff99, 0xff99ff66, 0xff99ff33, 0xff99ff00, 0xff99ccff, 0xff99cccc, + 0xff99cc99, 0xff99cc66, 0xff99cc33, 0xff99cc00, 0xff9999ff, 0xff9999cc, 0xff999999, 0xff999966, + 0xff999933, 0xff999900, 0xff9966ff, 0xff9966cc, 0xff996699, 0xff996666, 0xff996633, 0xff996600, + 0xff9933ff, 0xff9933cc, 0xff993399, 0xff993366, 0xff993333, 0xff993300, 0xff9900ff, 0xff9900cc, + 0xff990099, 0xff990066, 0xff990033, 0xff990000, 0xff66ffff, 0xff66ffcc, 0xff66ff99, 0xff66ff66, + 0xff66ff33, 0xff66ff00, 0xff66ccff, 0xff66cccc, 0xff66cc99, 0xff66cc66, 0xff66cc33, 0xff66cc00, + 0xff6699ff, 0xff6699cc, 0xff669999, 0xff669966, 0xff669933, 0xff669900, 0xff6666ff, 0xff6666cc, + 0xff666699, 0xff666666, 0xff666633, 0xff666600, 0xff6633ff, 0xff6633cc, 0xff663399, 0xff663366, + 0xff663333, 0xff663300, 0xff6600ff, 0xff6600cc, 0xff660099, 0xff660066, 0xff660033, 0xff660000, + 0xff33ffff, 0xff33ffcc, 0xff33ff99, 0xff33ff66, 0xff33ff33, 0xff33ff00, 0xff33ccff, 0xff33cccc, + 0xff33cc99, 0xff33cc66, 0xff33cc33, 0xff33cc00, 0xff3399ff, 0xff3399cc, 0xff339999, 0xff339966, + 0xff339933, 0xff339900, 0xff3366ff, 0xff3366cc, 0xff336699, 0xff336666, 0xff336633, 0xff336600, + 0xff3333ff, 0xff3333cc, 0xff333399, 0xff333366, 0xff333333, 0xff333300, 0xff3300ff, 0xff3300cc, + 0xff330099, 0xff330066, 0xff330033, 0xff330000, 0xff00ffff, 0xff00ffcc, 0xff00ff99, 0xff00ff66, + 0xff00ff33, 0xff00ff00, 0xff00ccff, 0xff00cccc, 0xff00cc99, 0xff00cc66, 0xff00cc33, 0xff00cc00, + 0xff0099ff, 0xff0099cc, 0xff009999, 0xff009966, 0xff009933, 0xff009900, 0xff0066ff, 0xff0066cc, + 0xff006699, 0xff006666, 0xff006633, 0xff006600, 0xff0033ff, 0xff0033cc, 0xff003399, 0xff003366, + 0xff003333, 0xff003300, 0xff0000ff, 0xff0000cc, 0xff000099, 0xff000066, 0xff000033, 0xffee0000, + 0xffdd0000, 0xffbb0000, 0xffaa0000, 0xff880000, 0xff770000, 0xff550000, 0xff440000, 0xff220000, + 0xff110000, 0xff00ee00, 0xff00dd00, 0xff00bb00, 0xff00aa00, 0xff008800, 0xff007700, 0xff005500, + 0xff004400, 0xff002200, 0xff001100, 0xff0000ee, 0xff0000dd, 0xff0000bb, 0xff0000aa, 0xff000088, + 0xff000077, 0xff000055, 0xff000044, 0xff000022, 0xff000011, 0xffeeeeee, 0xffdddddd, 0xffbbbbbb, + 0xffaaaaaa, 0xff888888, 0xff777777, 0xff555555, 0xff444444, 0xff222222, 0xff111111, 0xff000000 + }; + + static final IndexColorModel INSTANCE = new ICNS8BitColorModel(); + + private ICNS8BitColorModel() { + super(8, 256, CMAP, 0, false, -1, DataBuffer.TYPE_BYTE); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java new file mode 100644 index 00000000..752499f7 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReader.java @@ -0,0 +1,619 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; + +import javax.imageio.*; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * ImageReader for Apple Icon Image (ICNS) format. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNSImageReader.java,v 1.0 25.10.11 18:42 haraldk Exp$ + * + * @see Macintosh Icons + * @see Apple Icon Image format (Wikipedia) + */ +public final class ICNSImageReader extends ImageReaderBase { + // TODO: Support ToC resource for faster parsing/faster determine number of icons? + // TODO: Subsampled reading for completeness, even if never used? + private List icons = new ArrayList(); + private List masks = new ArrayList(); + private IconResource lastResourceRead; + + private int length; + + public ICNSImageReader() { + this(new ICNSImageReaderSpi()); + } + + ICNSImageReader(final ImageReaderSpi provider) { + super(provider); + } + + @Override + protected void resetMembers() { + length = 0; + + lastResourceRead = null; + icons.clear(); + masks.clear(); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + return readIconResource(imageIndex).size().width; + } + + @Override + public int getHeight(int imageIndex) throws IOException { + return readIconResource(imageIndex).size().height; + } + + @Override + public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException { + IconResource resource = readIconResource(imageIndex); + + switch (resource.depth()) { + case 1: + return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS1BitColorModel.INSTANCE); + case 4: + return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS4BitColorModel.INSTANCE); + case 8: + return IndexedImageTypeSpecifier.createFromIndexColorModel(ICNS8BitColorModel.INSTANCE); + case 32: + if (resource.isCompressed()) { + return ImageTypeSpecifier.createBanded( + ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[]{0, 1, 2, 3}, createBandOffsets(resource.size().width * resource.size().height), + DataBuffer.TYPE_BYTE, true, false + ); + } + else { + return ImageTypeSpecifier.createInterleaved( + ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[]{1, 2, 3, 0}, + DataBuffer.TYPE_BYTE, true, false + ); + } + default: + throw new IllegalStateException(String.format("Unknown bit depth: %d", resource.depth())); + } + } + + private static int[] createBandOffsets(int bandLen) { + return new int[]{0, bandLen, 2 * bandLen, 3 * bandLen}; + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + IconResource resource = readIconResource(imageIndex); + + List specifiers = new ArrayList(); + + switch (resource.depth()) { + case 1: + case 4: + case 8: + // Fall through & convert during read + case 32: + specifiers.add(ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB), 0xff0000, 0x00ff00, 0x0000ff, 0xff000000, DataBuffer.TYPE_INT, false)); + specifiers.add(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false)); + break; + default: + throw new IllegalStateException(String.format("Unknown bit depth: %d", resource.depth())); + } + + specifiers.add(rawType); + + return specifiers.iterator(); + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + assertInput(); + + if (!allowSearch) { + // Return icons.size if we know we have read all? + // TODO: If the first resource is a TOC_ resource, we don't need to perform a search. + return -1; + } + + int num = icons.size(); + while (true) { + try { + readIconResource(num++); + } + catch (IndexOutOfBoundsException expected) { + break; + } + } + + return icons.size(); + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + IconResource resource = readIconResource(imageIndex); + + imageInput.seek(resource.start + ICNS.RESOURCE_HEADER_SIZE); + + // Special handling of PNG/JPEG 2000 icons + if (resource.isForeignFormat()) { + return readForeignFormat(imageIndex, param, resource); + } + + return readICNSFormat(imageIndex, param, resource); + } + + private BufferedImage readICNSFormat(final int imageIndex, final ImageReadParam param, final IconResource resource) throws IOException { + Dimension size = resource.size(); + + int width = size.width; + int height = size.height; + + BufferedImage image = getDestination(param, getImageTypes(imageIndex), width, height); + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + + if (rawType instanceof IndexedImageTypeSpecifier && rawType.getBufferedImageType() != image.getType()) { + checkReadParamBandSettings(param, 4, image.getSampleModel().getNumBands()); + } + else { + checkReadParamBandSettings(param, rawType.getNumBands(), image.getSampleModel().getNumBands()); + } + + final Rectangle source = new Rectangle(); + final Rectangle dest = new Rectangle(); + computeRegions(param, width, height, image, source, dest); + + processImageStarted(imageIndex); + + // Read image data + byte[] data; + if (resource.isCompressed()) { + // Only 32 bit icons may be compressed + data = new byte[width * height * resource.depth() / 8]; + + int packedSize = resource.length - ICNS.RESOURCE_HEADER_SIZE; + + if (width >= 128 && height >= 128) { + // http://www.macdisk.com/maciconen.php: + // "In some icon sizes, there is a 32bit integer at the beginning of the run, whose role remains unknown." + imageInput.skipBytes(4); // Seems to be 4 byte 0-pad + packedSize -= 4; + } + + InputStream input = IIOUtil.createStreamAdapter(imageInput, packedSize); + + try { + ICNSUtil.decompress(new DataInputStream(input), data, 0, (data.length * 24) / 32); // 24 bit data + } + finally { + input.close(); + } + } + else { + data = new byte[resource.length - ICNS.RESOURCE_HEADER_SIZE]; + imageInput.readFully(data); + } + + if (resource.depth() == 1) { + // Binary + DataBufferByte buffer = new DataBufferByte(data, data.length / 2, 0); + WritableRaster raster = Raster.createPackedRaster(buffer, width, height, resource.depth(), null); + + if (image.getType() == rawType.getBufferedImageType() && ((IndexColorModel) image.getColorModel()).getMapSize() == 2) { + // Preserve raw data as read (binary), discard mask + image.setData(raster); + } + else { + // Convert to 32 bit ARGB + DataBufferByte maskBuffer = new DataBufferByte(data, data.length / 2, data.length / 2); + WritableRaster mask = Raster.createPackedRaster(maskBuffer, width, height, resource.depth(), null); + + Graphics2D graphics = image.createGraphics(); + + try { + // Apply image data + BufferedImage temp = new BufferedImage(rawType.getColorModel(), raster, false, null); + graphics.drawImage(temp, 0, 0, null); + + // Apply mask + temp = new BufferedImage(ICNSBitMaskColorModel.INSTANCE, mask, false, null); + temp.setData(mask); + graphics.setComposite(AlphaComposite.DstIn); + graphics.drawImage(temp, 0, 0, null); + } + finally { + graphics.dispose(); + } + } + } + else if (resource.depth() <= 8) { + // Indexed + DataBufferByte buffer = new DataBufferByte(data, data.length); + WritableRaster raster = Raster.createPackedRaster(buffer, width, height, resource.depth(), null); + + if (image.getType() == rawType.getBufferedImageType()) { + // Preserve raw data as read (indexed), discard mask + image.setData(raster); + } + else { + // Convert to 32 bit ARGB + Graphics2D graphics = image.createGraphics(); + + try { + BufferedImage temp = new BufferedImage(rawType.getColorModel(), raster, false, null); + graphics.drawImage(temp, 0, 0, null); + } + finally { + graphics.dispose(); + } + + processImageProgress(50f); + + // Read mask and apply + IconResource maskResource = findMaskResource(resource); + + if (maskResource != null) { + Raster mask = readMask(maskResource); + image.getAlphaRaster().setRect(mask); + } + } + } + else { + // 32 bit ARGB (true color) + int bandLen = data.length / 4; + + DataBufferByte buffer = new DataBufferByte(data, data.length); + + WritableRaster raster; + + if (resource.isCompressed()) { + raster = Raster.createBandedRaster(buffer, width, height, width, new int[]{0, 0, 0, 0}, createBandOffsets(bandLen), null); + } + else { + // NOTE: Uncompressed 32bit is interleaved RGBA, not banded... + raster = Raster.createInterleavedRaster(buffer, width, height, width * 4, 4, new int[]{1, 2, 3, 0}, null); + } + + image.setData(raster); + + processImageProgress(75f); + + // Read mask and apply + IconResource maskResource = findMaskResource(resource); + + if (maskResource != null) { + Raster mask = readMask(maskResource); + image.getAlphaRaster().setRect(mask); + } + else { + // TODO: This is simply stupid. Rewrite to use no alpha instead? + byte[] solid = new byte[width * height]; + Arrays.fill(solid, (byte) -1); + WritableRaster mask = Raster.createBandedRaster(new DataBufferByte(solid, solid.length), width, height, width, new int[]{0}, new int[]{0}, null); + image.getAlphaRaster().setRect(mask); + } + } + + // For now: Make listener tests happy + // TODO: Implement more sophisticated reading + processImageProgress(100f); + + if (abortRequested()) { + processReadAborted(); + } + else { + processImageComplete(); + } + + return image; + } + + private Raster readMask(final IconResource resource) throws IOException { + Dimension size = resource.size(); + + int width = size.width; + int height = size.height; + + byte[] mask = new byte[width * height]; + imageInput.seek(resource.start + ICNS.RESOURCE_HEADER_SIZE); + + if (resource.isMaskType()) { + // 8 bit mask + imageInput.readFully(mask, 0, resource.length - ICNS.RESOURCE_HEADER_SIZE); + } + else if (resource.hasMask()) { + // Embedded 1bit mask + byte[] maskData = new byte[(resource.length - ICNS.RESOURCE_HEADER_SIZE) / 2]; + imageInput.skipBytes(maskData.length); // Skip the 1 bit image data + imageInput.readFully(maskData); + + // Unpack 1bit mask to 8 bit + int bitPos = 0x80; + + for (int i = 0, maskLength = mask.length; i < maskLength; i++) { + mask[i] = (byte) ((maskData[i / 8] & bitPos) != 0 ? 0xff : 0x00); + + if ((bitPos >>= 1) == 0) { + bitPos = 0x80; + } + } + } + else { + throw new IllegalArgumentException(String.format("Not a mask resource: %s", resource)); + } + + return Raster.createBandedRaster(new DataBufferByte(mask, mask.length), width, height, width, new int[]{0}, new int[]{0}, null); + } + + private IconResource findMaskResource(final IconResource iconResource) throws IOException { + // Find 8 bit mask + try { + int i = 0; + + while (true) { + IconResource mask = i < masks.size() ? masks.get(i++) : readNextIconResource(); + + if (mask.isMaskType() && mask.size().equals(iconResource.size())) { + return mask; + } + } + } + catch (IndexOutOfBoundsException ignore) { + } + + // Fall back to mask from 1 bit resource if no 8 bit mask + for (IconResource resource : icons) { + if (resource.hasMask() && resource.size().equals(iconResource.size())) { + return resource; + } + } + + return null; + } + + private BufferedImage readForeignFormat(int imageIndex, final ImageReadParam param, final IconResource resource) throws IOException { + // TODO: Optimize by caching readers that work? + ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); + + try { + // Try first using ImageIO + Iterator readers = ImageIO.getImageReaders(stream); + + while (readers.hasNext()) { + ImageReader reader = readers.next(); + reader.setInput(stream); + + try { + return reader.read(0, param); + } + catch (IOException ignore) { + if (stream.getFlushedPosition() <= 0) { + stream.seek(0); + } + else { + stream.close(); + stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); + } + } + finally { + reader.dispose(); + } + } + + String format = getForeignFormat(stream); + + // OS X quick fix + if ("JPEG 2000".equals(format) && SipsJP2Reader.isAvailable()) { + SipsJP2Reader reader = new SipsJP2Reader(); + reader.setInput(stream); + BufferedImage image = reader.read(0, param); + + if (image != null) { + return image; + } + } + + // There's no JPEG 2000 reader installed in ImageIO by default (requires JAI ImageIO installed). + // Return blank icon + issue warning. We know the image dimensions, we just can't read the data. + processWarningOccurred(String.format( + "Cannot read %s format in type '%s' icon (no reader; installed: %s)", + format, ICNSUtil.intToStr(resource.type), Arrays.toString(IIOUtil.getNormalizedReaderFormatNames()) + )); + + Dimension size = resource.size(); + + return getDestination(param, getImageTypes(imageIndex), size.width, size.height); + } + finally { + stream.close(); + } + } + + private String getForeignFormat(final ImageInputStream stream) throws IOException { + byte[] magic = new byte[12]; // Length of JPEG 2000 magic + + try { + stream.readFully(magic); + } + finally { + stream.seek(0); + } + + String format; + + if (Arrays.equals(ICNS.PNG_MAGIC, magic)) { + format = "PNG"; + } + else if (Arrays.equals(ICNS.JPEG_2000_MAGIC, magic)) { + format = "JPEG 2000"; + } + else { + format = "unknown"; + } + + return format; + } + + private IconResource readIconResource(final int imageIndex) throws IOException { + checkBounds(imageIndex); + readeFileHeader(); + + while (icons.size() <= imageIndex) { + readNextIconResource(); + } + + return icons.get(imageIndex); + } + + private IconResource readNextIconResource() throws IOException { + long lastReadPos = lastResourceRead == null ? ICNS.RESOURCE_HEADER_SIZE : lastResourceRead.start + lastResourceRead.length; + + imageInput.seek(lastReadPos); + + if (imageInput.getStreamPosition() >= length) { + throw new IndexOutOfBoundsException(); + } + + IconResource resource = IconResource.read(imageInput); +// System.err.println("resource: " + resource); + + lastResourceRead = resource; + + // Filter out special cases like 'icnV' or 'TOC ' resources + if (resource.isMaskType()) { + masks.add(resource); + } + else if (!resource.isUnknownType()) { + icons.add(resource); + } + + return resource; + } + + private void readeFileHeader() throws IOException { + assertInput(); + + if (length <= 0) { + imageInput.seek(0); + + if (imageInput.readInt() != ICNS.MAGIC) { + throw new IIOException("Not an Apple Icon Image"); + } + + length = imageInput.readInt(); + } + } + + private static final class ICNSBitMaskColorModel extends IndexColorModel { + static final IndexColorModel INSTANCE = new ICNSBitMaskColorModel(); + + private ICNSBitMaskColorModel() { + super(1, 2, new int[]{0, 0xffffffff}, 0, true, 0, DataBuffer.TYPE_BYTE); + } + } + + @SuppressWarnings({"UnusedAssignment"}) + public static void main(String[] args) throws IOException { + int argIndex = 0; + + int requested = -1; + if (args[argIndex].charAt(0) == '-') { + argIndex++; + requested = Integer.parseInt(args[argIndex++]); + } + + int imagesRead = 0; + int imagesSkipped = 0; + ImageReader reader = new ICNSImageReader(); + + while(argIndex < args.length) { + File input = new File(args[argIndex++]); + ImageInputStream stream = ImageIO.createImageInputStream(input); + + if (stream == null) { + System.err.printf("Cannot read: %s\n", input.getAbsolutePath()); + continue; + } + + try { + reader.setInput(stream); + + int start = requested != -1 ? requested : 0; + int numImages = requested != -1 ? requested + 1 : reader.getNumImages(true); + for (int i = start; i < numImages; i++) { + try { + long begin = System.currentTimeMillis(); + BufferedImage image = reader.read(i); + imagesRead++; +// System.err.println("image: " + image); + System.err.println(System.currentTimeMillis() - begin + "ms"); + showIt(image, String.format("%s - %d", input.getName(), i)); + } + catch (IOException e) { + imagesSkipped++; + if (e.getMessage().contains("JPEG 2000")) { + System.err.printf("%s: %s\n", input, e.getMessage()); + } + else { + System.err.printf("%s: ", input); + e.printStackTrace(); + } + } + } + } + catch (Exception e) { + System.err.printf("%s: ", input); + e.printStackTrace(); + } + } + + System.err.printf("Read %s images (%d skipped) in %d files\n", imagesRead, imagesSkipped, args.length); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java new file mode 100644 index 00000000..5b023276 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderSpi.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import com.twelvemonkeys.imageio.spi.ProviderInfo; +import com.twelvemonkeys.imageio.util.IIOUtil; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.util.Locale; + +/** + * ICNSImageReaderSpi + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNSImageReaderSpi.java,v 1.0 25.10.11 18:41 haraldk Exp$ + */ +public final class ICNSImageReaderSpi extends ImageReaderSpi{ + public ICNSImageReaderSpi() { + this(IIOUtil.getProviderInfo(ICNSImageReaderSpi.class)); + } + + private ICNSImageReaderSpi(final ProviderInfo pProviderInfo) { + super( + pProviderInfo.getVendorName(), + pProviderInfo.getVersion(), + new String[]{"icns", "ICNS"}, + new String[]{"icns"}, + new String[]{ + "image/x-apple-icons", // Common extension MIME + }, + "com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader", + STANDARD_INPUT_TYPE, + null, + true, null, null, null, null, + true, + null, null, + null, null + ); + } + + @Override + public boolean canDecodeInput(Object source) throws IOException { + return source instanceof ImageInputStream && canDecode((ImageInputStream) source); + } + + private static boolean canDecode(ImageInputStream input) throws IOException { + try { + input.mark(); + return input.readInt() == ICNS.MAGIC; + } + finally { + input.reset(); + } + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new ICNSImageReader(this); + } + + @Override + public String getDescription(Locale locale) { + return "Apple Icon Image (icns) format Reader"; + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSUtil.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSUtil.java new file mode 100644 index 00000000..71fb6b06 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/ICNSUtil.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * ICNSUtil + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNSUtil.java,v 1.0 26.10.11 11:49 haraldk Exp$ + */ +final class ICNSUtil { + + private ICNSUtil() {} + + // TODO: Duplicated code from IFF plugin, move to some common util? + static String intToStr(int pChunkId) { + return new String( + new byte[]{ + (byte) ((pChunkId & 0xff000000) >> 24), + (byte) ((pChunkId & 0x00ff0000) >> 16), + (byte) ((pChunkId & 0x0000ff00) >> 8), + (byte) ((pChunkId & 0x000000ff)) + } + ); + } + + /* + * http://www.macdisk.com/maciconen.php: + * "For [...] (width * height of the icon), read a byte. + * if bit 8 of the byte is set: + * This is a compressed run, for some value (next byte). + * The length is byte - 125. (* + * Put so many copies of the byte in the current color channel. + * Else: + * This is an uncompressed run, whose values follow. + * The length is byte + 1. + * Read the bytes and put them in the current color channel." + * + * *): With signed bytes, byte is always negative in this case, so it's actually -byte - 125, + * which is the same as byte + 131. + */ + // NOTE: This is very close to PackBits (as described by the Wikipedia article), but it is not PackBits! + static void decompress(final DataInputStream input, final byte[] result, int offset, int length) throws IOException { + int resultPos = offset; + int remaining = length; + + while (remaining > 0) { + byte run = input.readByte(); + int runLength; + + if ((run & 0x80) != 0) { + // Compressed run + runLength = run + 131; // PackBits: -run + 1 and run == 0x80 is no-op... This allows 1 byte longer runs... + + byte runData = input.readByte(); + + for (int i = 0; i < runLength; i++) { + result[resultPos++] = runData; + } + } + else { + // Uncompressed run + runLength = run + 1; + + input.readFully(result, resultPos, runLength); + resultPos += runLength; + } + + remaining -= runLength; + } + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java new file mode 100644 index 00000000..8eb05ef2 --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/IconResource.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.io.IOException; + +/** + * IconResource + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IconResource.java,v 1.0 23.11.11 13:35 haraldk Exp$ + */ +final class IconResource { + // TODO: Rewrite using subclasses/instances! + + protected final long start; + protected final int type; + protected final int length; + + private IconResource(long start, int type, int length) { + validate(type, length); + + this.start = start; + this.type = type; + this.length = length; + } + + public static IconResource read(ImageInputStream input) throws IOException { + return new IconResource(input.getStreamPosition(), input.readInt(), input.readInt()); + } + + private void validate(int type, int length) { + switch (type) { + case ICNS.ICON: + validateLengthForType(type, length, 128); + break; + case ICNS.ICN_: + validateLengthForType(type, length, 256); + break; + case ICNS.icm_: + validateLengthForType(type, length, 48); + break; + case ICNS.icm4: + validateLengthForType(type, length, 96); + break; + case ICNS.icm8: + validateLengthForType(type, length, 192); + break; + case ICNS.ics_: + validateLengthForType(type, length, 64); + break; + case ICNS.ics4: + validateLengthForType(type, length, 128); + break; + case ICNS.ics8: + case ICNS.s8mk: + validateLengthForType(type, length, 256); + break; + case ICNS.icl4: + validateLengthForType(type, length, 512); + break; + case ICNS.icl8: + case ICNS.l8mk: + validateLengthForType(type, length, 1024); + break; + case ICNS.ich_: + validateLengthForType(type, length, 576); + break; + case ICNS.ich4: + validateLengthForType(type, length, 1152); + break; + case ICNS.ich8: + case ICNS.h8mk: + validateLengthForType(type, length, 2304); + break; + case ICNS.t8mk: + validateLengthForType(type, length, 16384); + break; + case ICNS.ih32: + case ICNS.is32: + case ICNS.il32: + case ICNS.it32: + case ICNS.ic08: + case ICNS.ic09: + case ICNS.ic10: + if (length > ICNS.RESOURCE_HEADER_SIZE) { + break; + } + throw new IllegalArgumentException(String.format("Wrong combination of icon type '%s' and length: %d", ICNSUtil.intToStr(type), length)); + case ICNS.icnV: + validateLengthForType(type, length, 4); + break; + case ICNS.TOC_: + default: + if (length > ICNS.RESOURCE_HEADER_SIZE) { + break; + } + throw new IllegalStateException(String.format("Unknown icon type: '%s' length: %d", ICNSUtil.intToStr(type), length)); + } + } + + private void validateLengthForType(int type, int length, final int expectedLength) { + Validate.isTrue( + length == expectedLength + ICNS.RESOURCE_HEADER_SIZE, // Compute to make lengths more logical + String.format( + "Wrong combination of icon type '%s' and length: %d (expected: %d)", + ICNSUtil.intToStr(type), length - ICNS.RESOURCE_HEADER_SIZE, expectedLength + ) + ); + } + + public Dimension size() { + switch (type) { + case ICNS.ICON: + case ICNS.ICN_: + return new Dimension(32, 32); + case ICNS.icm_: + case ICNS.icm4: + case ICNS.icm8: + return new Dimension(16, 12); + case ICNS.ics_: + case ICNS.ics4: + case ICNS.ics8: + case ICNS.is32: + case ICNS.s8mk: + return new Dimension(16, 16); + case ICNS.icl4: + case ICNS.icl8: + case ICNS.il32: + case ICNS.l8mk: + return new Dimension(32, 32); + case ICNS.ich_: + case ICNS.ich4: + case ICNS.ich8: + case ICNS.ih32: + case ICNS.h8mk: + return new Dimension(48, 48); + case ICNS.it32: + case ICNS.t8mk: + return new Dimension(128, 128); + case ICNS.ic08: + return new Dimension(256, 256); + case ICNS.ic09: + return new Dimension(512, 512); + case ICNS.ic10: + return new Dimension(1024, 1024); + default: + throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type))); + } + } + + public int depth() { + switch (type) { + case ICNS.ICON: + case ICNS.ICN_: + case ICNS.icm_: + case ICNS.ics_: + case ICNS.ich_: + return 1; + case ICNS.icm4: + case ICNS.ics4: + case ICNS.icl4: + case ICNS.ich4: + return 4; + case ICNS.icm8: + case ICNS.ics8: + case ICNS.icl8: + case ICNS.ich8: + case ICNS.s8mk: + case ICNS.l8mk: + case ICNS.h8mk: + case ICNS.t8mk: + return 8; + case ICNS.is32: + case ICNS.il32: + case ICNS.ih32: + case ICNS.it32: + case ICNS.ic08: + case ICNS.ic09: + case ICNS.ic10: + return 32; + default: + throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type))); + } + } + + public boolean isUnknownType() { + // Unknown types should simply be skipped when reading + switch (type) { + case ICNS.ICON: + case ICNS.ICN_: + case ICNS.icm_: + case ICNS.ics_: + case ICNS.ich_: + case ICNS.icm4: + case ICNS.ics4: + case ICNS.icl4: + case ICNS.ich4: + case ICNS.icm8: + case ICNS.ics8: + case ICNS.icl8: + case ICNS.ich8: + case ICNS.s8mk: + case ICNS.l8mk: + case ICNS.h8mk: + case ICNS.t8mk: + case ICNS.is32: + case ICNS.il32: + case ICNS.ih32: + case ICNS.it32: + case ICNS.ic08: + case ICNS.ic09: + case ICNS.ic10: + return false; + } + + return true; + } + + public boolean hasMask() { + switch (type) { + case ICNS.ICN_: + case ICNS.icm_: + case ICNS.ics_: + case ICNS.ich_: + return true; + } + + return false; + } + + public boolean isMaskType() { + switch (type) { + case ICNS.s8mk: + case ICNS.l8mk: + case ICNS.h8mk: + case ICNS.t8mk: + return true; + } + + return false; + } + + public boolean isCompressed() { + switch (type) { + case ICNS.is32: + case ICNS.il32: + case ICNS.ih32: + case ICNS.it32: + // http://www.macdisk.com/maciconen.php + // "One should check whether the data length corresponds to the theoretical length (width * height)." + Dimension size = size(); + if (length != (size.width * size.height * depth() / 8 + ICNS.RESOURCE_HEADER_SIZE)) { + return true; + } + } + + return false; + } + + public boolean isForeignFormat() { + // Recent entries contains full JPEG 2000 or PNG streams + switch (type) { + case ICNS.ic08: + case ICNS.ic09: + case ICNS.ic10: + return true; + } + + return false; + } + + @Override + public int hashCode() { + return (int) start ^ type; + } + + @Override + public boolean equals(Object other) { + return other == this || other != null && other.getClass() == getClass() && isEqual((IconResource) other); + } + + private boolean isEqual(IconResource other) { + // This isn't strictly true, as resource must reside in same stream as well, but good enough for now + return start == other.start && type == other.type && length == other.length; + } + + @Override + public String toString() { + return String.format("%s['%s' start: %d, length: %d%s]", getClass().getSimpleName(), ICNSUtil.intToStr(type), start, length, isCompressed() ? " (compressed)" : ""); + } +} diff --git a/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java new file mode 100644 index 00000000..c5834abb --- /dev/null +++ b/imageio/imageio-icns/src/main/java/com/twelvemonkeys/imageio/plugins/icns/SipsJP2Reader.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.io.FileUtil; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.Iterator; + +/** + * QuickFix for OS X (where ICNS are most useful) and JPEG 2000. + * Dumps the stream to disk and converts using sips command line tool: + * {@code sips -s format png }. + * Reads image back using ImageIO and known format (png). + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEG2000Reader.java,v 1.0 25.11.11 14:17 haraldk Exp$ + */ +final class SipsJP2Reader { + + private static final File SIPS_COMMAND = new File("/usr/bin/sips"); + private static final boolean SIPS_EXISTS_AND_EXECUTES = existsAndExecutes(SIPS_COMMAND); + + private static boolean existsAndExecutes(final File cmd) { + try { + return cmd.exists() && cmd.canExecute(); + } + catch (SecurityException ignore) { + } + + return false; + } + + private ImageInputStream input; + + public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + // Test if we have sips before dumping to be fail-fast + if (SIPS_EXISTS_AND_EXECUTES) { + File tempFile = dumpToFile(input); + + if (convertToPNG(tempFile)) { + ImageInputStream stream = ImageIO.createImageInputStream(tempFile); + Iterator readers = ImageIO.getImageReaders(stream); + + while (readers.hasNext()) { + ImageReader reader = readers.next(); + reader.setInput(stream); + + try { + return reader.read(imageIndex, param); + } + catch (IOException ignore) { + if (stream.getFlushedPosition() <= 0) { + stream.seek(0); + } + else { + stream.close(); + stream = ImageIO.createImageInputStream(tempFile); + } + } + finally { + reader.dispose(); + } + } + } + } + + return null; + } + + public void setInput(final ImageInputStream input) { + this.input = input; + } + + private static boolean convertToPNG(final File tempFile) throws IIOException { + try { + Process process = Runtime.getRuntime().exec(buildCommand(SIPS_COMMAND, tempFile)); + + // NOTE: sips return status is 0, even if error, need to check error message + int status = process.waitFor(); + String message = checkErrorMessage(process); + + if (status == 0 && message == null) { + return true; + } + else { + throw new IOException(message); + } + } + catch (InterruptedException e) { + throw new IIOException("Interrupted converting JPEG 2000 format", e); + } + catch (SecurityException e) { + // Exec might need permissions in sandboxed environment + throw new IIOException("Cannot convert JPEG 2000 format without file permissions", e); + } + catch (IOException e) { + throw new IIOException("Error converting JPEG 2000 format: " + e.getMessage(), e); + } + } + + private static String checkErrorMessage(final Process process) throws IOException { + InputStream stream = process.getErrorStream(); + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String message = reader.readLine(); + + return message != null && message.startsWith("Error: ") ? message.substring(7) : null; + } + finally { + stream.close(); + } + } + + private static String[] buildCommand(final File sipsCommand, final File tempFile) { + return new String[]{ + sipsCommand.getAbsolutePath(), "-s", "format", "png", tempFile.getAbsolutePath() + }; + } + + + private static File dumpToFile(final ImageInputStream stream) throws IOException { + File tempFile = File.createTempFile("imageio-icns-", ".png"); + tempFile.deleteOnExit(); + + FileOutputStream out = new FileOutputStream(tempFile); + + try { + FileUtil.copy(IIOUtil.createStreamAdapter(stream), out); + } + finally { + out.close(); + } + + return tempFile; + } + + static boolean isAvailable() { + return SIPS_EXISTS_AND_EXECUTES; + } +} diff --git a/imageio/imageio-icns/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-icns/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100644 index 00000000..8e2e73e4 --- /dev/null +++ b/imageio/imageio-icns/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1,28 @@ +# +# Copyright (c) 2011, Harald Kuhr +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name "TwelveMonkeys" nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. +# +com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi \ No newline at end of file diff --git a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java new file mode 100644 index 00000000..527a0ba0 --- /dev/null +++ b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.icns; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.util.Arrays; +import java.util.List; + +/** + * ICNSImageReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ICNSImageReaderTest.java,v 1.0 25.10.11 18:44 haraldk Exp$ + */ +public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { + @Override + protected List getTestData() { + return Arrays.asList( + new TestData( + getClassLoaderResource("/icns/GenericJavaApp.icns"), + new Dimension(16, 16), // 1 bit + 1 bit mask + new Dimension(16, 16), new Dimension(16, 16), // 8 bit CMAP, 24 bit + 8 bit mask + new Dimension(32, 32), // 1 bit + 1 bit mask + new Dimension(32, 32), new Dimension(32, 32), // 8 bit CMAP, 24 bit + 8 bit mask + new Dimension(128, 128) // 24 bit + 8 bit mask + ), + new TestData( + getClassLoaderResource("/icns/Apple Retro.icns"), + new Dimension(16, 16), // 24 bit + 8 bit mask + new Dimension(32, 32), // 24 bit + 8 bit mask + new Dimension(48, 48), // 24 bit + 8 bit mask + new Dimension(128, 128), // 24 bit + 8 bit mask + new Dimension(256, 256), // JPEG 2000 ic08 + new Dimension(512, 512) // JPEG 2000 ic09 + ), + new TestData( + getClassLoaderResource("/icns/7zIcon.icns"), // Contains the icnV resource, that isn't an icon + new Dimension(16, 16), // 24 bit + 8 bit mask + new Dimension(32, 32), // 24 bit + 8 bit mask + new Dimension(128, 128), // 24 bit + 8 bit mask + new Dimension(256, 256), // JPEG 2000 ic08 + new Dimension(512, 512) // JPEG 2000 ic09 + ), + new TestData( + getClassLoaderResource("/icns/appStore.icns"), // Contains the 'TOC ' and icnV resources + PNGs in ic08-10 + new Dimension(16, 16), // 24 bit + 8 bit mask + new Dimension(32, 32), // 24 bit + 8 bit mask + new Dimension(128, 128), // 24 bit + 8 bit mask + new Dimension(256, 256), // PNG ic08 + new Dimension(512, 512), // PNG ic09 + new Dimension(1024, 1024) // PNG ic10 + ), + new TestData( + getClassLoaderResource("/icns/XLW.icns"), // No 8 bit mask for 16x16 & 32x32, fall back to 1 bit mask + new Dimension(16, 16), // 1 bit + 1 bit mask + new Dimension(16, 16), new Dimension(16, 16), // 4 bit CMAP, 8 bit CMAP (no 8 bit mask) + new Dimension(32, 32), // 1 bit + 1 bit mask + new Dimension(32, 32), new Dimension(32, 32), // 4 bit CMAP, 8 bit CMAP (no 8 bit mask) + new Dimension(128, 128) // 24 bit + 8 bit mask + ), + new TestData( + getClassLoaderResource("/icns/XMLExport.icns"), // No masks at all, uncompressed 32 bit data + new Dimension(128, 128), // 32 bit interleaved + new Dimension(48, 48), // 32 bit interleaved + new Dimension(32, 32), // 32 bit interleaved + new Dimension(16, 16) // 32 bit interleaved + ) + ); + } + + @Override + protected ImageReaderSpi createProvider() { + return new ICNSImageReaderSpi(); + } + + @Override + protected ImageReader createReader() { + return new ICNSImageReader(); + } + + @Override + protected Class getReaderClass() { + return ICNSImageReader.class; + } + + @Override + protected List getFormatNames() { + return Arrays.asList("icns"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("icns"); + } + + @Override + protected List getMIMETypes() { + return Arrays.asList("image/x-apple-icons"); + } +} diff --git a/imageio/imageio-icns/src/test/resources/icns/7zIcon.icns b/imageio/imageio-icns/src/test/resources/icns/7zIcon.icns new file mode 100755 index 00000000..32f1cd6a Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/7zIcon.icns differ diff --git a/imageio/imageio-icns/src/test/resources/icns/Apple Retro.icns b/imageio/imageio-icns/src/test/resources/icns/Apple Retro.icns new file mode 100644 index 00000000..39fcfc0a Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/Apple Retro.icns differ diff --git a/imageio/imageio-icns/src/test/resources/icns/GenericJavaApp.icns b/imageio/imageio-icns/src/test/resources/icns/GenericJavaApp.icns new file mode 100644 index 00000000..8ffb8d7d Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/GenericJavaApp.icns differ diff --git a/imageio/imageio-icns/src/test/resources/icns/XLW.icns b/imageio/imageio-icns/src/test/resources/icns/XLW.icns new file mode 100644 index 00000000..00e0d3e6 Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/XLW.icns differ diff --git a/imageio/imageio-icns/src/test/resources/icns/XMLExport.icns b/imageio/imageio-icns/src/test/resources/icns/XMLExport.icns new file mode 100644 index 00000000..573e504a Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/XMLExport.icns differ diff --git a/imageio/imageio-icns/src/test/resources/icns/appStore.icns b/imageio/imageio-icns/src/test/resources/icns/appStore.icns new file mode 100644 index 00000000..0a64672c Binary files /dev/null and b/imageio/imageio-icns/src/test/resources/icns/appStore.icns differ diff --git a/imageio/imageio-icns/src/test/resources/icns/fair-use.txt b/imageio/imageio-icns/src/test/resources/icns/fair-use.txt new file mode 100644 index 00000000..dd4fa4a1 --- /dev/null +++ b/imageio/imageio-icns/src/test/resources/icns/fair-use.txt @@ -0,0 +1,5 @@ +The icon files in this folder may contain copyrighted artwork. However, I believe that using them for test purposes +(without actually displaying the artwork) must be considered fair use. +If you disagree for any reason, please send me a note, and I will remove your icon from the distribution. + + -- harald.kuhr@gmail.com diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapDescriptor.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapDescriptor.java index 36c67748..de3645b4 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapDescriptor.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapDescriptor.java @@ -39,34 +39,34 @@ import java.awt.image.BufferedImage; * @version $Id: Bitmap.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ abstract class BitmapDescriptor { - protected final DirectoryEntry mEntry; - protected final DIBHeader mHeader; + protected final DirectoryEntry entry; + protected final DIBHeader header; - protected BufferedImage mImage; + protected BufferedImage image; public BitmapDescriptor(final DirectoryEntry pEntry, final DIBHeader pHeader) { Validate.notNull(pEntry, "entry"); Validate.notNull(pHeader, "header"); - mEntry = pEntry; - mHeader = pHeader; + entry = pEntry; + header = pHeader; } abstract public BufferedImage getImage(); public final int getWidth() { - return mEntry.getWidth(); + return entry.getWidth(); } public final int getHeight() { - return mEntry.getHeight(); + return entry.getHeight(); } protected final int getColorCount() { - return mEntry.getColorCount() != 0 ? mEntry.getColorCount() : 1 << getBitCount(); + return entry.getColorCount() != 0 ? entry.getColorCount() : 1 << getBitCount(); } protected final int getBitCount() { - return mEntry.getBitCount() != 0 ? mEntry.getBitCount() : mHeader.getBitCount(); + return entry.getBitCount() != 0 ? entry.getBitCount() : header.getBitCount(); } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapIndexed.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapIndexed.java index bd73dd75..17fdc28f 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapIndexed.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapIndexed.java @@ -43,17 +43,17 @@ import java.util.Hashtable; * @version $Id: BitmapIndexed.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ class BitmapIndexed extends BitmapDescriptor { - protected final int[] mBits; - protected final int[] mColors; + protected final int[] bits; + protected final int[] colors; - private BitmapMask mMask; + private BitmapMask mask; public BitmapIndexed(final DirectoryEntry pEntry, final DIBHeader pHeader) { super(pEntry, pHeader); - mBits = new int[getWidth() * getHeight()]; + bits = new int[getWidth() * getHeight()]; // NOTE: We're adding space for one extra color, for transparency - mColors = new int[getColorCount() + 1]; + colors = new int[getColorCount() + 1]; } public BufferedImage createImageIndexed() { @@ -64,10 +64,9 @@ class BitmapIndexed extends BitmapDescriptor { // This is slightly obscure, and should probably be moved.. Hashtable properties = null; - if (mEntry instanceof DirectoryEntry.CUREntry) { - DirectoryEntry.CUREntry entry = (DirectoryEntry.CUREntry) mEntry; + if (entry instanceof DirectoryEntry.CUREntry) { properties = new Hashtable(1); - properties.put("cursor_hotspot", entry.getHotspot()); + properties.put("cursor_hotspot", ((DirectoryEntry.CUREntry) this.entry).getHotspot()); } BufferedImage image = new BufferedImage( @@ -78,17 +77,17 @@ class BitmapIndexed extends BitmapDescriptor { WritableRaster raster = image.getRaster(); - // Make pixels transparant according to mask + // Make pixels transparent according to mask final int trans = icm.getTransparentPixel(); for (int y = 0; y < getHeight(); y++) { for (int x = 0; x < getWidth(); x++) { - if (mMask.isTransparent(x, y)) { - mBits[x + getWidth() * y] = trans; + if (mask.isTransparent(x, y)) { + bits[x + getWidth() * y] = trans; } } } - raster.setSamples(0, 0, getWidth(), getHeight(), 0, mBits); + raster.setSamples(0, 0, getWidth(), getHeight(), 0, bits); //System.out.println("Image: " + image); @@ -102,18 +101,18 @@ class BitmapIndexed extends BitmapDescriptor { // NOTE: This is a hack to make room for transparent pixel for mask int bits = getBitCount(); - int colors = mColors.length; + int colors = this.colors.length; int trans = -1; // Try to avoid USHORT transfertype, as it results in BufferedImage TYPE_CUSTOM // NOTE: This code assumes icons are small, and is NOT optimized for performance... if (colors > (1 << getBitCount())) { - int index = BitmapIndexed.findTransIndexMaybeRemap(mColors, mBits); + int index = BitmapIndexed.findTransIndexMaybeRemap(this.colors, this.bits); if (index == -1) { // No duplicate found, increase bitcount bits++; - trans = mColors.length - 1; + trans = this.colors.length - 1; } else { // Found a duplicate, use it as trans @@ -124,7 +123,7 @@ class BitmapIndexed extends BitmapDescriptor { // NOTE: Setting hasAlpha to true, makes things work on 1.2 return new InverseColorMapIndexColorModel( - bits, colors, mColors, 0, true, trans, + bits, colors, this.colors, 0, true, trans, bits <= 8 ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT ); } @@ -170,13 +169,13 @@ class BitmapIndexed extends BitmapDescriptor { } public BufferedImage getImage() { - if (mImage == null) { - mImage = createImageIndexed(); + if (image == null) { + image = createImageIndexed(); } - return mImage; + return image; } public void setMask(final BitmapMask pMask) { - mMask = pMask; + mask = pMask; } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapMask.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapMask.java index e5e11c1c..874dcf56 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapMask.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapMask.java @@ -38,19 +38,19 @@ import java.awt.image.BufferedImage; * @version $Id: BitmapMask.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ class BitmapMask extends BitmapDescriptor { - protected final BitmapIndexed mMask; + protected final BitmapIndexed mask; public BitmapMask(final DirectoryEntry pParent, final DIBHeader pHeader) { super(pParent, pHeader); - mMask = new BitmapIndexed(pParent, pHeader); + mask = new BitmapIndexed(pParent, pHeader); } boolean isTransparent(final int pX, final int pY) { // NOTE: 1: Fully transparent, 0: Opaque... - return mMask.mBits[pX + pY * getWidth()] != 0; + return mask.bits[pX + pY * getWidth()] != 0; } public BufferedImage getImage() { - return mMask.getImage(); + return mask.getImage(); } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapRGB.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapRGB.java index 774caf48..aaa75359 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapRGB.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapRGB.java @@ -43,6 +43,6 @@ class BitmapRGB extends BitmapDescriptor { } public BufferedImage getImage() { - return mImage; + return image; } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapUnsupported.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapUnsupported.java index 73d7be5b..84078090 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapUnsupported.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/BitmapUnsupported.java @@ -38,15 +38,15 @@ import java.awt.image.BufferedImage; * @version $Id: BitmapUnsupported.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ class BitmapUnsupported extends BitmapDescriptor { - private String mMessage; + private String message; public BitmapUnsupported(final DirectoryEntry pEntry, final String pMessage) { super(pEntry, null); - mMessage = pMessage; + message = pMessage; } public BufferedImage getImage() { - throw new IllegalStateException(mMessage); + throw new IllegalStateException(message); } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReader.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReader.java index 68519bf5..c58aca14 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReader.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReader.java @@ -39,13 +39,11 @@ import java.io.IOException; * @author last modified by $Author: haraldk$ * @version $Id: CURImageReader.java,v 1.0 Apr 20, 2009 11:54:28 AM haraldk Exp$ * - * @see com.twelvemonkeys.imageio.plugins.ico.ICOImageReader + * @see ICOImageReader */ -public class CURImageReader extends ICOImageReader { - // NOTE: All implementation is part of the ICOImageReader - +public final class CURImageReader extends DIBImageReader { public CURImageReader() { - super(DIB.TYPE_CUR); + super(new CURImageReaderSpi()); } protected CURImageReader(final ImageReaderSpi pProvider) { diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java index 9b38ea85..43f127e8 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderSpi.java @@ -43,7 +43,7 @@ import java.util.Locale; * @author Harald Kuhr * @version $Id: CURImageReaderSpi.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ -public class CURImageReaderSpi extends ImageReaderSpi { +public final class CURImageReaderSpi extends ImageReaderSpi { public CURImageReaderSpi() { this(IIOUtil.getProviderInfo(CURImageReaderSpi.class)); diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBHeader.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBHeader.java index af4ef23f..974fba5d 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBHeader.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBHeader.java @@ -41,16 +41,16 @@ import java.io.IOException; * @see BMP file format (Wikipedia) */ abstract class DIBHeader { - protected int mSize; + protected int size; - protected int mWidth; + protected int width; // NOTE: If a bitmask is present, this value includes the height of the mask // (so often header.height = entry.height * 2) - protected int mHeight; + protected int height; - protected int mPlanes; - protected int mBitCount; + protected int planes; + protected int bitCount; /** * 0 = BI_RGB: No compression @@ -58,18 +58,18 @@ abstract class DIBHeader { * 2 = BI_RLE4: 4 bit RLE Compression (4 bit only) * 3 = BI_BITFIELDS: No compression (16 & 32 bit only) */ - protected int mCompression; + protected int compression; // May be 0 if not known - protected int mImageSize; + protected int imageSize; - protected int mXPixelsPerMeter; - protected int mYPixelsPerMeter; + protected int xPixelsPerMeter; + protected int yPixelsPerMeter; - protected int mColorsUsed; + protected int colorsUsed; // 0 means all colors are important - protected int mColorsImportant; + protected int colorsImportant; protected DIBHeader() { } @@ -102,47 +102,47 @@ abstract class DIBHeader { protected abstract void read(int pSize, DataInput pStream) throws IOException; public final int getSize() { - return mSize; + return size; } public final int getWidth() { - return mWidth; + return width; } public final int getHeight() { - return mHeight; + return height; } public final int getPlanes() { - return mPlanes; + return planes; } public final int getBitCount() { - return mBitCount; + return bitCount; } public int getCompression() { - return mCompression; + return compression; } public int getImageSize() { - return mImageSize; + return imageSize; } public int getXPixelsPerMeter() { - return mXPixelsPerMeter; + return xPixelsPerMeter; } public int getYPixelsPerMeter() { - return mYPixelsPerMeter; + return yPixelsPerMeter; } public int getColorsUsed() { - return mColorsUsed; + return colorsUsed; } public int getColorsImportant() { - return mColorsImportant; + return colorsImportant; } public String toString() { @@ -176,22 +176,22 @@ abstract class DIBHeader { throw new IIOException(String.format("Size: %s !=: %s", pSize, DIB.WINDOWS_V3_HEADER_SIZE)); } - mSize = pSize; + size = pSize; - mWidth = pStream.readInt(); - mHeight = pStream.readInt(); + width = pStream.readInt(); + height = pStream.readInt(); - mPlanes = pStream.readUnsignedShort(); - mBitCount = pStream.readUnsignedShort(); - mCompression = pStream.readInt(); + planes = pStream.readUnsignedShort(); + bitCount = pStream.readUnsignedShort(); + compression = pStream.readInt(); - mImageSize = pStream.readInt(); + imageSize = pStream.readInt(); - mXPixelsPerMeter = pStream.readInt(); - mYPixelsPerMeter = pStream.readInt(); + xPixelsPerMeter = pStream.readInt(); + yPixelsPerMeter = pStream.readInt(); - mColorsUsed = pStream.readInt(); - mColorsImportant = pStream.readInt(); + colorsUsed = pStream.readInt(); + colorsImportant = pStream.readInt(); } } } \ No newline at end of file diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBImageReader.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBImageReader.java new file mode 100644 index 00000000..11be7c15 --- /dev/null +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DIBImageReader.java @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2009, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.ico; + +import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; +import com.twelvemonkeys.util.WeakWeakMap; + +import javax.imageio.*; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import javax.swing.*; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.util.*; +import java.util.List; + +/** + * ImageReader for Microsoft Windows ICO (icon) format. + * 1, 4, 8 bit palette support with bitmask transparency, and 16, 24 and 32 bit + * true color support with alpha. Also supports Windows Vista PNG encoded icons. + *

    + * + * @author Harald Kuhr + * @version $Id: ICOImageReader.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ + * + * @see BMP file format (Wikipedia) + * @see ICO file format (Wikipedia) + */ +// SEE http://en.wikipedia.org/wiki/ICO_(icon_image_file_format) +// TODO: Decide whether DirectoryEntry or DIBHeader should be primary source for color count/bit count +// TODO: Support loading icons from DLLs, see +// MSDN +// Known issue: 256x256 PNG encoded icons does not have IndexColorModel even if stated in DirectoryEntry (seem impossible as the PNGs are all true color) +abstract class DIBImageReader extends ImageReaderBase { + // TODO: Consider moving the reading to inner classes (subclasses of BitmapDescriptor) + private Directory directory; + + // TODO: Review these, make sure we don't have a memory leak + private Map headers = new WeakHashMap(); + private Map descriptors = new WeakWeakMap(); + + private ImageReader pngImageReader; + + protected DIBImageReader(final ImageReaderSpi pProvider) { + super(pProvider); + } + + protected void resetMembers() { + directory = null; + + headers.clear(); + descriptors.clear(); + + if (pngImageReader != null) { + pngImageReader.dispose(); + pngImageReader = null; + } + } + + public Iterator getImageTypes(final int pImageIndex) throws IOException { + DirectoryEntry entry = getEntry(pImageIndex); + + // NOTE: Delegate to PNG reader + if (isPNG(entry)) { + return getImageTypesPNG(entry); + } + + List types = new ArrayList(); + DIBHeader header = getHeader(entry); + + // Use data from header to create specifier + ImageTypeSpecifier specifier; + switch (header.getBitCount()) { + case 1: + case 2: + case 4: + case 8: + // TODO: This is slightly QnD... + int offset = entry.getOffset() + header.getSize(); + if (offset != imageInput.getStreamPosition()) { + imageInput.seek(offset); + } + BitmapIndexed indexed = new BitmapIndexed(entry, header); + readColorMap(indexed); + specifier = IndexedImageTypeSpecifier.createFromIndexColorModel(indexed.createColorModel()); + break; + case 16: + specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB); + break; + case 24: + specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); + break; + case 32: + specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB); + break; + default: + throw new IIOException(String.format("Unknown bit depth: %d", header.getBitCount())); + } + + types.add(specifier); + + return types.iterator(); + } + + @Override + public int getNumImages(final boolean allowSearch) throws IOException { + return getDirectory().count(); + } + + public int getWidth(final int pImageIndex) throws IOException { + return getEntry(pImageIndex).getWidth(); + } + + public int getHeight(final int pImageIndex) throws IOException { + return getEntry(pImageIndex).getHeight(); + } + + public BufferedImage read(final int pImageIndex, final ImageReadParam pParam) throws IOException { + checkBounds(pImageIndex); + + processImageStarted(pImageIndex); + + DirectoryEntry entry = getEntry(pImageIndex); + + BufferedImage destination; + + if (isPNG(entry)) { + // NOTE: Special case for Windows Vista, 256x256 PNG encoded images, with no DIB header... + destination = readPNG(entry, pParam); + } + else { + // NOTE: If param does not have explicit destination, we'll try to create a BufferedImage later, + // to allow for storing the cursor hotspot for CUR images + destination = hasExplicitDestination(pParam) ? + getDestination(pParam, getImageTypes(pImageIndex), getWidth(pImageIndex), getHeight(pImageIndex)) : null; + + BufferedImage image = readBitmap(entry); + + // TODO: Handle AOI and subsampling inline, probably not of big importance... + if (pParam != null) { + image = fakeAOI(image, pParam); + image = ImageUtil.toBuffered(fakeSubsampling(image, pParam)); + } + + if (destination == null) { + // This is okay, as long as the client did not request explicit destination image/type + destination = image; + } + else { + Graphics2D g = destination.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.drawImage(image, 0, 0, null); + } + finally { + g.dispose(); + } + } + } + + processImageProgress(100); + processImageComplete(); + + return destination; + } + + private boolean isPNG(final DirectoryEntry pEntry) throws IOException { + long magic; + + imageInput.seek(pEntry.getOffset()); + imageInput.setByteOrder(ByteOrder.BIG_ENDIAN); + + try { + magic = imageInput.readLong(); + } + finally { + imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + return magic == DIB.PNG_MAGIC; + } + + private BufferedImage readPNG(final DirectoryEntry pEntry, final ImageReadParam pParam) throws IOException { + // TODO: Consider delegating listener calls + return initPNGReader(pEntry).read(0, pParam); + } + + private Iterator getImageTypesPNG(final DirectoryEntry pEntry) throws IOException { + return initPNGReader(pEntry).getImageTypes(0); + } + + private ImageReader initPNGReader(final DirectoryEntry pEntry) throws IOException { + ImageReader pngReader = getPNGReader(); + + imageInput.seek(pEntry.getOffset()); + InputStream inputStream = IIOUtil.createStreamAdapter(imageInput, pEntry.getSize()); + ImageInputStream stream = ImageIO.createImageInputStream(inputStream); + + // NOTE: Will throw IOException on later reads if input is not PNG + pngReader.setInput(stream); + + return pngReader; + } + + private ImageReader getPNGReader() throws IIOException { + // TODO: Prefer Sun's std JDK PNGImagerReader, because it has known behaviour? + if (pngImageReader == null) { + Iterator readers = ImageIO.getImageReadersByFormatName("PNG"); + + if (readers.hasNext()) { + pngImageReader = readers.next(); + } + else { + throw new IIOException("No PNGImageReader found using ImageIO, can't read PNG encoded ICO format."); + } + } + else { + pngImageReader.reset(); + } + + return pngImageReader; + } + + private DIBHeader getHeader(final DirectoryEntry pEntry) throws IOException { + if (!headers.containsKey(pEntry)) { + imageInput.seek(pEntry.getOffset()); + DIBHeader header = DIBHeader.read(imageInput); + headers.put(pEntry, header); + } + + return headers.get(pEntry); + } + + private BufferedImage readBitmap(final DirectoryEntry pEntry) throws IOException { + // TODO: Get rid of the caching, as the images are mutable + BitmapDescriptor descriptor = descriptors.get(pEntry); + + if (descriptor == null || !descriptors.containsKey(pEntry)) { + DIBHeader header = getHeader(pEntry); + + int offset = pEntry.getOffset() + header.getSize(); + if (offset != imageInput.getStreamPosition()) { + imageInput.seek(offset); + } + + // TODO: Support this, it's already in the BMP reader, spec allows RLE4 and RLE8 + if (header.getCompression() != 0) { + descriptor = new BitmapUnsupported(pEntry, String.format("Unsupported compression: %d", header.getCompression())); + } + else { + int bitCount = header.getBitCount(); + + switch (bitCount) { + // Palette style + case 1: + case 4: + case 8: + descriptor = new BitmapIndexed(pEntry, header); + readBitmapIndexed((BitmapIndexed) descriptor); + break; + // RGB style + case 16: + descriptor = new BitmapRGB(pEntry, header); + readBitmap16(descriptor); + break; + case 24: + descriptor = new BitmapRGB(pEntry, header); + readBitmap24(descriptor); + break; + case 32: + descriptor = new BitmapRGB(pEntry, header); + readBitmap32(descriptor); + break; + + default: + descriptor = new BitmapUnsupported(pEntry, String.format("Unsupported bit count %d", bitCount)); + } + } + + descriptors.put(pEntry, descriptor); + } + + return descriptor.getImage(); + } + + private void readBitmapIndexed(final BitmapIndexed pBitmap) throws IOException { + readColorMap(pBitmap); + + switch (pBitmap.getBitCount()) { + case 1: + readBitmapIndexed1(pBitmap, false); + break; + case 4: + readBitmapIndexed4(pBitmap); + break; + case 8: + readBitmapIndexed8(pBitmap); + break; + } + + BitmapMask mask = new BitmapMask(pBitmap.entry, pBitmap.header); + readBitmapIndexed1(mask.mask, true); + pBitmap.setMask(mask); + } + + private void readColorMap(final BitmapIndexed pBitmap) throws IOException { + int colorCount = pBitmap.getColorCount(); + + for (int i = 0; i < colorCount; i++) { + // aRGB (a is "Reserved") + pBitmap.colors[i] = (imageInput.readInt() & 0xffffff) | 0xff000000; + } + } + + private void readBitmapIndexed1(final BitmapIndexed pBitmap, final boolean pAsMask) throws IOException { + int width = adjustToPadding(pBitmap.getWidth() >> 3); + byte[] row = new byte[width]; + + for (int y = 0; y < pBitmap.getHeight(); y++) { + imageInput.readFully(row, 0, width); + int rowPos = 0; + int xOrVal = 0x80; + int pos = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); + + for (int x = 0; x < pBitmap.getWidth(); x++) { + pBitmap.bits[pos++] = ((row[rowPos] & xOrVal) / xOrVal) & 0xFF; + + if (xOrVal == 1) { + xOrVal = 0x80; + rowPos++; + } + else { + xOrVal >>= 1; + } + } + + // NOTE: If we are reading the mask, we don't abort or progress + if (!pAsMask) { + if (abortRequested()) { + processReadAborted(); + break; + } + + processImageProgress(100 * y / (float) pBitmap.getHeight()); + } + } + } + + private void readBitmapIndexed4(final BitmapIndexed pBitmap) throws IOException { + int width = adjustToPadding(pBitmap.getWidth() >> 1); + byte[] row = new byte[width]; + + for (int y = 0; y < pBitmap.getHeight(); y++) { + imageInput.readFully(row, 0, width); + int rowPos = 0; + boolean high4 = true; + int pos = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); + + for (int x = 0; x < pBitmap.getWidth(); x++) { + int value; + + if (high4) { + value = (row[rowPos] & 0xF0) >> 4; + } + else { + value = row[rowPos] & 0x0F; + rowPos++; + } + + pBitmap.bits[pos++] = value & 0xFF; + high4 = !high4; + } + + if (abortRequested()) { + processReadAborted(); + break; + } + + processImageProgress(100 * y / (float) pBitmap.getHeight()); + } + } + + private void readBitmapIndexed8(final BitmapIndexed pBitmap) throws IOException { + int width = adjustToPadding(pBitmap.getWidth()); + + byte[] row = new byte[width]; + + for (int y = 0; y < pBitmap.getHeight(); y++) { + imageInput.readFully(row, 0, width); + int rowPos = 0; + int pos = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); + + for (int x = 0; x < pBitmap.getWidth(); x++) { + pBitmap.bits[pos++] = row[rowPos++] & 0xFF; + } + + if (abortRequested()) { + processReadAborted(); + break; + } + + processImageProgress(100 * y / (float) pBitmap.getHeight()); + } + } + + /** + * @param pWidth Bytes per scan line (i.e., 1BPP, width = 9 -> bytes = 1) + * @return padded width + */ + private static int adjustToPadding(final int pWidth) { + if ((pWidth & 0x03) != 0) { + return (pWidth & ~0x03) + 4; + } + return pWidth; + } + + private void readBitmap16(final BitmapDescriptor pBitmap) throws IOException { + // TODO: No idea if this actually works.. + short[] pixels = new short[pBitmap.getWidth() * pBitmap.getHeight()]; + + // Will create TYPE_USHORT_555; + DirectColorModel cm = new DirectColorModel(16, 0x7C00, 0x03E0, 0x001F); + DataBuffer buffer = new DataBufferShort(pixels, pixels.length); + WritableRaster raster = Raster.createPackedRaster( + buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), cm.getMasks(), null + ); + pBitmap.image = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + + for (int y = 0; y < pBitmap.getHeight(); y++) { + int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); + imageInput.readFully(pixels, offset, pBitmap.getWidth()); + + + // Skip to 32 bit boundary + if (pBitmap.getWidth() % 2 != 0) { + imageInput.readShort(); + } + + if (abortRequested()) { + processReadAborted(); + break; + } + + processImageProgress(100 * y / (float) pBitmap.getHeight()); + } + } + + private void readBitmap24(final BitmapDescriptor pBitmap) throws IOException { + byte[] pixels = new byte[pBitmap.getWidth() * pBitmap.getHeight() * 3]; + + // Create TYPE_3BYTE_BGR + DataBuffer buffer = new DataBufferByte(pixels, pixels.length); + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + int[] nBits = {8, 8, 8}; + int[] bOffs = {2, 1, 0}; + ComponentColorModel cm = new ComponentColorModel( + cs, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE + ); + + WritableRaster raster = Raster.createInterleavedRaster( + buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), 3, bOffs, null + ); + pBitmap.image = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + + for (int y = 0; y < pBitmap.getHeight(); y++) { + int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); + imageInput.readFully(pixels, offset, pBitmap.getWidth() * 3); + + // TODO: Possibly read padding byte here! + + if (abortRequested()) { + processReadAborted(); + break; + } + + processImageProgress(100 * y / (float) pBitmap.getHeight()); + } + } + + private void readBitmap32(final BitmapDescriptor pBitmap) throws IOException { + int[] pixels = new int[pBitmap.getWidth() * pBitmap.getHeight()]; + + // Will create TYPE_INT_ARGB + DirectColorModel cm = (DirectColorModel) ColorModel.getRGBdefault(); + DataBuffer buffer = new DataBufferInt(pixels, pixels.length); + WritableRaster raster = Raster.createPackedRaster( + buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), cm.getMasks(), null + ); + pBitmap.image = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + + for (int y = 0; y < pBitmap.getHeight(); y++) { + int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); + imageInput.readFully(pixels, offset, pBitmap.getWidth()); + + if (abortRequested()) { + processReadAborted(); + break; + } + processImageProgress(100 * y / (float) pBitmap.getHeight()); + } + } + + private Directory getDirectory() throws IOException { + assertInput(); + + if (directory == null) { + readFileHeader(); + } + + return directory; + } + + private void readFileHeader() throws IOException { + imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + // Read file header + imageInput.readUnsignedShort(); // Reserved + + // Should be same as type as the provider + int type = imageInput.readUnsignedShort(); + int imageCount = imageInput.readUnsignedShort(); + + // Read directory + directory = Directory.read(type, imageCount, imageInput); + } + + final DirectoryEntry getEntry(final int pImageIndex) throws IOException { + Directory directory = getDirectory(); + if (pImageIndex < 0 || pImageIndex >= directory.count()) { + throw new IndexOutOfBoundsException(String.format("Index: %d, ImageCount: %d", pImageIndex, directory.count())); + } + + return directory.getEntry(pImageIndex); + } + + /// Test code below, ignore.. :-) + public static void main(final String[] pArgs) throws IOException { + if (pArgs.length == 0) { + System.err.println("Please specify the icon file name"); + System.exit(1); + } + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception e) { + // Ignore + } + + String title = new File(pArgs[0]).getName(); + JFrame frame = createWindow(title); + JPanel root = new JPanel(new FlowLayout()); + JScrollPane scroll = + new JScrollPane(root, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scroll.setBorder(BorderFactory.createEmptyBorder()); + frame.setContentPane(scroll); + + Iterator readers = ImageIO.getImageReadersByFormatName("ico"); + if (!readers.hasNext()) { + System.err.println("No reader for format 'ico' found"); + System.exit(1); + } + + ImageReader reader = readers.next(); + + for (String arg : pArgs) { + JPanel panel = new JPanel(null); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + readImagesInFile(arg, reader, panel); + root.add(panel); + } + + frame.pack(); + frame.setVisible(true); + } + + private static void readImagesInFile(String pFileName, ImageReader pReader, final Container pContainer) throws IOException { + File file = new File(pFileName); + if (!file.isFile()) { + System.err.println(pFileName + " not found, or is no file"); + } + + pReader.setInput(ImageIO.createImageInputStream(file)); + int imageCount = pReader.getNumImages(true); + for (int i = 0; i < imageCount; i++) { + try { + addImage(pContainer, pReader, i); + } + catch (Exception e) { + System.err.println("FileName: " + pFileName); + System.err.println("Icon: " + i); + e.printStackTrace(); + } + } + } + + private static JFrame createWindow(final String pTitle) { + JFrame frame = new JFrame(pTitle); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + public void windowClosed(WindowEvent e) { + System.exit(0); + } + }); + return frame; + } + + private static void addImage(final Container pParent, final ImageReader pReader, final int pImageNo) throws IOException { + final JButton button = new JButton(); + + BufferedImage image = pReader.read(pImageNo); + button.setIcon(new ImageIcon(image) { + TexturePaint texture; + + private void createTexture(final GraphicsConfiguration pGraphicsConfiguration) { + BufferedImage pattern = pGraphicsConfiguration.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(); + } + + texture = new TexturePaint(pattern, new Rectangle(pattern.getWidth(), pattern.getHeight())); + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + if (texture == null) { + createTexture(c.getGraphicsConfiguration()); + } + + Graphics2D gr = (Graphics2D) g; + gr.setPaint(texture); + gr.fillRect(x, y, getIconWidth(), getIconHeight()); + super.paintIcon(c, g, x, y); + } + }); + + button.setText("" + image.getWidth() + "x" + + image.getHeight() + ": " + + ((image.getColorModel() instanceof IndexColorModel) ? + "" + ((IndexColorModel) image.getColorModel()).getMapSize() : + "TrueColor")); + + pParent.add(button); + } +} diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/Directory.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/Directory.java index 9cafc0c4..861224cd 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/Directory.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/Directory.java @@ -41,10 +41,10 @@ import java.util.List; * @version $Id: Directory.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ class Directory { - private final List mEntries; + private final List entries; private Directory(int pImageCount) { - mEntries = Arrays.asList(new DirectoryEntry[pImageCount]); + entries = Arrays.asList(new DirectoryEntry[pImageCount]); } public static Directory read(final int pType, final int pImageCount, final DataInput pStream) throws IOException { @@ -54,21 +54,21 @@ class Directory { } private void readEntries(final int pType, final DataInput pStream) throws IOException { - for (int i = 0; i < mEntries.size(); i++) { - mEntries.set(i, DirectoryEntry.read(pType, pStream)); + for (int i = 0; i < entries.size(); i++) { + entries.set(i, DirectoryEntry.read(pType, pStream)); } } public DirectoryEntry getEntry(final int pEntryIndex) { - return mEntries.get(pEntryIndex); + return entries.get(pEntryIndex); } public int count() { - return mEntries.size(); + return entries.size(); } @Override public String toString() { - return String.format("%s%s", getClass().getSimpleName(), mEntries); + return String.format("%s%s", getClass().getSimpleName(), entries); } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DirectoryEntry.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DirectoryEntry.java index 163c3acd..6b023762 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DirectoryEntry.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/DirectoryEntry.java @@ -31,7 +31,6 @@ package com.twelvemonkeys.imageio.plugins.ico; import javax.imageio.IIOException; import java.io.DataInput; import java.io.IOException; -import java.awt.image.BufferedImage; import java.awt.*; /** @@ -43,13 +42,13 @@ import java.awt.*; * @see Wikipedia */ abstract class DirectoryEntry { - private int mWidth; - private int mHeight; - private int mColorCount; - int mPlanes; - int mBitCount; - private int mSize; - private int mOffset; + private int width; + private int height; + private int colorCount; + int planes; + int bitCount; + private int size; + private int offset; private DirectoryEntry() { } @@ -79,81 +78,81 @@ abstract class DirectoryEntry { protected void read(final DataInput pStream) throws IOException { // Width/height = 0, means 256 int w = pStream.readUnsignedByte(); - mWidth = w == 0 ? 256 : w; + width = w == 0 ? 256 : w; int h = pStream.readUnsignedByte(); - mHeight = h == 0 ? 256 : h; + height = h == 0 ? 256 : h; // Color count = 0, means 256 or more colors - mColorCount = pStream.readUnsignedByte(); + colorCount = pStream.readUnsignedByte(); // Ignore. Should be 0, but .NET (System.Drawing.Icon.Save) sets this value to 255, according to Wikipedia pStream.readUnsignedByte(); - mPlanes = pStream.readUnsignedShort(); // Should be 0 or 1 for ICO, x hotspot for CUR - mBitCount = pStream.readUnsignedShort(); // bit count for ICO, y hotspot for CUR + planes = pStream.readUnsignedShort(); // Should be 0 or 1 for ICO, x hotspot for CUR + bitCount = pStream.readUnsignedShort(); // bit count for ICO, y hotspot for CUR // Size of bitmap in bytes - mSize = pStream.readInt(); - mOffset = pStream.readInt(); + size = pStream.readInt(); + offset = pStream.readInt(); } public String toString() { return String.format( "%s: width: %d, height: %d, colors: %d, planes: %d, bit count: %d, size: %d, offset: %d", getClass().getSimpleName(), - mWidth, mHeight, mColorCount, mPlanes, mBitCount, mSize, mOffset + width, height, colorCount, planes, bitCount, size, offset ); } public int getBitCount() { - return mBitCount; + return bitCount; } public int getColorCount() { - return mColorCount; + return colorCount; } public int getHeight() { - return mHeight; + return height; } public int getOffset() { - return mOffset; + return offset; } public int getPlanes() { - return mPlanes; + return planes; } public int getSize() { - return mSize; + return size; } public int getWidth() { - return mWidth; + return width; } /** * Cursor directory entry. */ static class CUREntry extends DirectoryEntry { - private int mXHotspot; - private int mYHotspot; + private int xHotspot; + private int yHotspot; @Override protected void read(final DataInput pStream) throws IOException { super.read(pStream); // NOTE: This is a hack... - mXHotspot = mPlanes; - mYHotspot = mBitCount; + xHotspot = planes; + yHotspot = bitCount; - mPlanes = 1; // Always 1 for all BMP types - mBitCount = 0; + planes = 1; // Always 1 for all BMP types + bitCount = 0; } public Point getHotspot() { - return new Point(mXHotspot, mYHotspot); + return new Point(xHotspot, yHotspot); } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReader.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReader.java index 8bf47ebd..9d228565 100644 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReader.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, Harald Kuhr + * Copyright (c) 2011, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,682 +28,24 @@ package com.twelvemonkeys.imageio.plugins.ico; -import com.twelvemonkeys.image.ImageUtil; -import com.twelvemonkeys.imageio.ImageReaderBase; -import com.twelvemonkeys.imageio.util.IIOUtil; -import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; -import com.twelvemonkeys.util.WeakWeakMap; - -import javax.imageio.*; import javax.imageio.spi.ImageReaderSpi; -import javax.imageio.stream.ImageInputStream; -import javax.swing.*; -import java.awt.*; -import java.awt.color.ColorSpace; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.awt.image.*; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteOrder; -import java.util.*; -import java.util.List; /** - * ImageReader for Microsoft Windows ICO (icon) format. - * 1, 4, 8 bit palette support with bitmask transparency, and 16, 24 and 32 bit - * true color support with alpha. Also supports Windows Vista PNG encoded icons. - *

    + * ImageReader for Microsoft Windows CUR (cursor) format. * * @author Harald Kuhr - * @version $Id: ICOImageReader.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ + * @author last modified by $Author: haraldk$ + * @version $Id: CURImageReader.java,v 1.0 Apr 20, 2009 11:54:28 AM haraldk Exp$ * - * @see BMP file format (Wikipedia) - * @see ICO file format (Wikipedia) + * @see CURImageReader */ -// SEE http://en.wikipedia.org/wiki/ICO_(icon_image_file_format) -// TODO: Decide wether DirectoryEntry or DIBHeader should be primary source for color count/bit count -// TODO: Support loading icons from DLLs, see -// MSDN -// Known issue: 256x256 PNG encoded icons does not have IndexColorModel even if stated in DirectoryEntry (seem impossible as the PNGs are all true color) -public class ICOImageReader extends ImageReaderBase { - // TODO: Consider moving the reading to inner classes (subclasses of BitmapDescriptor) - private Directory mDirectory; - - // TODO: Review these, make sure we don't have a memory leak - private Map mHeaders = new WeakHashMap(); - private Map mDescriptors = new WeakWeakMap(); - - private ImageReader mPNGImageReader; - +public final class ICOImageReader extends DIBImageReader { public ICOImageReader() { - this(DIB.TYPE_ICO); - } - - ICOImageReader(final int pType) { - this(createProviderForConstructor(pType)); + super(new ICOImageReaderSpi()); } protected ICOImageReader(final ImageReaderSpi pProvider) { super(pProvider); } - private static ImageReaderSpi createProviderForConstructor(final int pType) { - switch (pType) { - case DIB.TYPE_ICO: - return new ICOImageReaderSpi(); - case DIB.TYPE_CUR: - return new CURImageReaderSpi(); - default: - throw new IllegalArgumentException(String.format("Unsupported ICO/CUR type: %d", pType)); - } - } - - protected void resetMembers() { - mDirectory = null; - - mHeaders.clear(); - mDescriptors.clear(); - - if (mPNGImageReader != null) { - mPNGImageReader.dispose(); - mPNGImageReader = null; - } - } - - public Iterator getImageTypes(final int pImageIndex) throws IOException { - DirectoryEntry entry = getEntry(pImageIndex); - - // NOTE: Delegate to PNG reader - if (isPNG(entry)) { - return getImageTypesPNG(entry); - } - - List types = new ArrayList(); - DIBHeader header = getHeader(entry); - - // Use data from header to create specifier - ImageTypeSpecifier specifier; - switch (header.getBitCount()) { - case 1: - case 2: - case 4: - case 8: - // TODO: This is slightly QnD... - int offset = entry.getOffset() + header.getSize(); - if (offset != mImageInput.getStreamPosition()) { - mImageInput.seek(offset); - } - BitmapIndexed indexed = new BitmapIndexed(entry, header); - readColorMap(indexed); - specifier = IndexedImageTypeSpecifier.createFromIndexColorModel(indexed.createColorModel()); - break; - case 16: - specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB); - break; - case 24: - specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); - break; - case 32: - specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB); - break; - default: - throw new IIOException(String.format("Unknown bit depth: %d", header.getBitCount())); - } - - types.add(specifier); - - return types.iterator(); - } - - @Override - public int getNumImages(final boolean pAllowSearch) throws IOException { - return getDirectory().count(); - } - - public int getWidth(final int pImageIndex) throws IOException { - return getEntry(pImageIndex).getWidth(); - } - - public int getHeight(final int pImageIndex) throws IOException { - return getEntry(pImageIndex).getHeight(); - } - - public BufferedImage read(final int pImageIndex, final ImageReadParam pParam) throws IOException { - checkBounds(pImageIndex); - - processImageStarted(pImageIndex); - - DirectoryEntry entry = getEntry(pImageIndex); - - BufferedImage destination; - - if (isPNG(entry)) { - // NOTE: Special case for Windows Vista, 256x256 PNG encoded images, with no DIB header... - destination = readPNG(entry, pParam); - } - else { - // NOTE: If param does not have explicit destination, we'll try to create a BufferedImage later, - // to allow for storing the cursor hotspot for CUR images - destination = hasExplicitDestination(pParam) ? - getDestination(pParam, getImageTypes(pImageIndex), getWidth(pImageIndex), getHeight(pImageIndex)) : - null; - - BufferedImage image = readBitmap(entry); - - // TODO: Handle AOI and subsampling inline, probably not of big importance... - if (pParam != null) { - image = fakeAOI(image, pParam); - image = ImageUtil.toBuffered(fakeSubsampling(image, pParam)); - } - - if (destination == null) { - // This is okay, as long as the client did not request explicit destination image/type - destination = image; - } - else { - Graphics2D g = destination.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); - g.drawImage(image, 0, 0, null); - } - finally { - g.dispose(); - } - } - } - - processImageProgress(100); - processImageComplete(); - - return destination; - } - - private boolean hasExplicitDestination(final ImageReadParam pParam) { - return (pParam != null && (pParam.getDestination() != null || pParam.getDestinationType() != null || pParam.getDestinationOffset() != null)); - } - - private boolean isPNG(final DirectoryEntry pEntry) throws IOException { - long magic; - - mImageInput.seek(pEntry.getOffset()); - mImageInput.setByteOrder(ByteOrder.BIG_ENDIAN); - - try { - magic = mImageInput.readLong(); - } - finally { - mImageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); - } - - return magic == DIB.PNG_MAGIC; - } - - private BufferedImage readPNG(final DirectoryEntry pEntry, final ImageReadParam pParam) throws IOException { - // TODO: Consider delegating listener calls - return initPNGReader(pEntry).read(0, pParam); - } - - private Iterator getImageTypesPNG(final DirectoryEntry pEntry) throws IOException { - return initPNGReader(pEntry).getImageTypes(0); - } - - private ImageReader initPNGReader(final DirectoryEntry pEntry) throws IOException { - ImageReader pngReader = getPNGReader(); - - mImageInput.seek(pEntry.getOffset()); - InputStream inputStream = IIOUtil.createStreamAdapter(mImageInput, pEntry.getSize()); - ImageInputStream stream = ImageIO.createImageInputStream(inputStream); - - // NOTE: Will throw IOException on later reads if input is not PNG - pngReader.setInput(stream); - - return pngReader; - } - - private ImageReader getPNGReader() throws IIOException { - // TODO: Prefer Sun's std JDK PNGImagerReader, because it has known behaviour? - if (mPNGImageReader == null) { - Iterator readers = ImageIO.getImageReadersByFormatName("PNG"); - - if (readers.hasNext()) { - mPNGImageReader = readers.next(); - } - else { - throw new IIOException("No PNGImageReader found using ImageIO, can't read PNG encoded ICO format."); - } - } - else { - mPNGImageReader.reset(); - } - - return mPNGImageReader; - } - - private DIBHeader getHeader(final DirectoryEntry pEntry) throws IOException { - if (!mHeaders.containsKey(pEntry)) { - mImageInput.seek(pEntry.getOffset()); - DIBHeader header = DIBHeader.read(mImageInput); - mHeaders.put(pEntry, header); - } - - return mHeaders.get(pEntry); - } - - private BufferedImage readBitmap(final DirectoryEntry pEntry) throws IOException { - // TODO: Get rid of the caching, as the images are mutable - BitmapDescriptor descriptor = mDescriptors.get(pEntry); - - if (descriptor == null || !mDescriptors.containsKey(pEntry)) { - DIBHeader header = getHeader(pEntry); - - int offset = pEntry.getOffset() + header.getSize(); - if (offset != mImageInput.getStreamPosition()) { - mImageInput.seek(offset); - } - - // TODO: Support this, it's already in the BMP reader, spec allows RLE4 and RLE8 - if (header.getCompression() != 0) { - descriptor = new BitmapUnsupported(pEntry, String.format("Unsupported compression: %d", header.getCompression())); - } - else { - int bitCount = header.getBitCount(); - - switch (bitCount) { - // Palette style - case 1: - case 4: - case 8: - descriptor = new BitmapIndexed(pEntry, header); - readBitmapIndexed((BitmapIndexed) descriptor); - break; - // RGB style - case 16: - descriptor = new BitmapRGB(pEntry, header); - readBitmap16(descriptor); - break; - case 24: - descriptor = new BitmapRGB(pEntry, header); - readBitmap24(descriptor); - break; - case 32: - descriptor = new BitmapRGB(pEntry, header); - readBitmap32(descriptor); - break; - - default: - descriptor = new BitmapUnsupported(pEntry, String.format("Unsupported bit count %d", bitCount)); - } - } - - mDescriptors.put(pEntry, descriptor); - } - - return descriptor.getImage(); - } - - private void readBitmapIndexed(final BitmapIndexed pBitmap) throws IOException { - readColorMap(pBitmap); - - switch (pBitmap.getBitCount()) { - case 1: - readBitmapIndexed1(pBitmap, false); - break; - case 4: - readBitmapIndexed4(pBitmap); - break; - case 8: - readBitmapIndexed8(pBitmap); - break; - } - - BitmapMask mask = new BitmapMask(pBitmap.mEntry, pBitmap.mHeader); - readBitmapIndexed1(mask.mMask, true); - pBitmap.setMask(mask); - } - - private void readColorMap(final BitmapIndexed pBitmap) throws IOException { - int colorCount = pBitmap.getColorCount(); - - for (int i = 0; i < colorCount; i++) { - // aRGB (a is "Reserved") - pBitmap.mColors[i] = (mImageInput.readInt() & 0xffffff) | 0xff000000; - } - } - - private void readBitmapIndexed1(final BitmapIndexed pBitmap, final boolean pAsMask) throws IOException { - int width = adjustToPadding(pBitmap.getWidth() >> 3); - byte[] row = new byte[width]; - - for (int y = 0; y < pBitmap.getHeight(); y++) { - mImageInput.readFully(row, 0, width); - int rowPos = 0; - int xOrVal = 0x80; - int pos = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - - for (int x = 0; x < pBitmap.getWidth(); x++) { - pBitmap.mBits[pos++] = ((row[rowPos] & xOrVal) / xOrVal) & 0xFF; - - if (xOrVal == 1) { - xOrVal = 0x80; - rowPos++; - } - else { - xOrVal >>= 1; - } - } - - // NOTE: If we are reading the mask, we don't abort or progress - if (!pAsMask) { - if (abortRequested()) { - processReadAborted(); - break; - } - - processImageProgress(100 * y / (float) pBitmap.getHeight()); - } - } - } - - private void readBitmapIndexed4(final BitmapIndexed pBitmap) throws IOException { - int width = adjustToPadding(pBitmap.getWidth() >> 1); - byte[] row = new byte[width]; - - for (int y = 0; y < pBitmap.getHeight(); y++) { - mImageInput.readFully(row, 0, width); - int rowPos = 0; - boolean high4 = true; - int pos = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - - for (int x = 0; x < pBitmap.getWidth(); x++) { - int value; - - if (high4) { - value = (row[rowPos] & 0xF0) >> 4; - } - else { - value = row[rowPos] & 0x0F; - rowPos++; - } - - pBitmap.mBits[pos++] = value & 0xFF; - high4 = !high4; - } - - if (abortRequested()) { - processReadAborted(); - break; - } - - processImageProgress(100 * y / (float) pBitmap.getHeight()); - } - } - - private void readBitmapIndexed8(final BitmapIndexed pBitmap) throws IOException { - int width = adjustToPadding(pBitmap.getWidth()); - - byte[] row = new byte[width]; - - for (int y = 0; y < pBitmap.getHeight(); y++) { - mImageInput.readFully(row, 0, width); - int rowPos = 0; - int pos = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - - for (int x = 0; x < pBitmap.getWidth(); x++) { - pBitmap.mBits[pos++] = row[rowPos++] & 0xFF; - } - - if (abortRequested()) { - processReadAborted(); - break; - } - - processImageProgress(100 * y / (float) pBitmap.getHeight()); - } - } - - /** - * @param pWidth Bytes per scan line (i.e., 1BPP, width = 9 -> bytes = 1) - * @return padded width - */ - private static int adjustToPadding(final int pWidth) { - if ((pWidth & 0x03) != 0) { - return (pWidth & ~0x03) + 4; - } - return pWidth; - } - - private void readBitmap16(final BitmapDescriptor pBitmap) throws IOException { - // TODO: No idea if this actually works.. - short[] pixels = new short[pBitmap.getWidth() * pBitmap.getHeight()]; - - // Will create TYPE_USHORT_555; - DirectColorModel cm = new DirectColorModel(16, 0x7C00, 0x03E0, 0x001F); - DataBuffer buffer = new DataBufferShort(pixels, pixels.length); - WritableRaster raster = Raster.createPackedRaster( - buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), cm.getMasks(), null - ); - pBitmap.mImage = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - - for (int y = 0; y < pBitmap.getHeight(); y++) { - int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - mImageInput.readFully(pixels, offset, pBitmap.getWidth()); - - - // Skip to 32 bit boundary - if (pBitmap.getWidth() % 2 != 0) { - mImageInput.readShort(); - } - - if (abortRequested()) { - processReadAborted(); - break; - } - - processImageProgress(100 * y / (float) pBitmap.getHeight()); - } - } - - private void readBitmap24(final BitmapDescriptor pBitmap) throws IOException { - byte[] pixels = new byte[pBitmap.getWidth() * pBitmap.getHeight() * 3]; - - // Create TYPE_3BYTE_BGR - DataBuffer buffer = new DataBufferByte(pixels, pixels.length); - ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); - int[] nBits = {8, 8, 8}; - int[] bOffs = {2, 1, 0}; - ComponentColorModel cm = new ComponentColorModel( - cs, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE - ); - - WritableRaster raster = Raster.createInterleavedRaster( - buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), 3, bOffs, null - ); - pBitmap.mImage = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - - for (int y = 0; y < pBitmap.getHeight(); y++) { - int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - mImageInput.readFully(pixels, offset, pBitmap.getWidth() * 3); - - // TODO: Possibly read padding byte here! - - if (abortRequested()) { - processReadAborted(); - break; - } - - processImageProgress(100 * y / (float) pBitmap.getHeight()); - } - } - - private void readBitmap32(final BitmapDescriptor pBitmap) throws IOException { - int[] pixels = new int[pBitmap.getWidth() * pBitmap.getHeight()]; - - // Will create TYPE_INT_ARGB - DirectColorModel cm = (DirectColorModel) ColorModel.getRGBdefault(); - DataBuffer buffer = new DataBufferInt(pixels, pixels.length); - WritableRaster raster = Raster.createPackedRaster( - buffer, pBitmap.getWidth(), pBitmap.getHeight(), pBitmap.getWidth(), cm.getMasks(), null - ); - pBitmap.mImage = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - - for (int y = 0; y < pBitmap.getHeight(); y++) { - int offset = (pBitmap.getHeight() - y - 1) * pBitmap.getWidth(); - mImageInput.readFully(pixels, offset, pBitmap.getWidth()); - - if (abortRequested()) { - processReadAborted(); - break; - } - processImageProgress(100 * y / (float) pBitmap.getHeight()); - } - } - - private Directory getDirectory() throws IOException { - assertInput(); - - if (mDirectory == null) { - readFileHeader(); - } - - return mDirectory; - } - - private void readFileHeader() throws IOException { - mImageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); - - // Read file header - mImageInput.readUnsignedShort(); // Reserved - - // Should be same as type as the provider - int type = mImageInput.readUnsignedShort(); - int imageCount = mImageInput.readUnsignedShort(); - - // Read directory - mDirectory = Directory.read(type, imageCount, mImageInput); - } - - final DirectoryEntry getEntry(final int pImageIndex) throws IOException { - Directory directory = getDirectory(); - if (pImageIndex < 0 || pImageIndex >= directory.count()) { - throw new IndexOutOfBoundsException(String.format("Index: %d, ImageCount: %d", pImageIndex, directory.count())); - } - - return directory.getEntry(pImageIndex); - } - - /// Test code below, ignore.. :-) - public static void main(final String[] pArgs) throws IOException { - if (pArgs.length == 0) { - System.err.println("Please specify the icon file name"); - System.exit(1); - } - - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } - catch (Exception e) { - // Ignore - } - - String title = new File(pArgs[0]).getName(); - JFrame frame = createWindow(title); - JPanel root = new JPanel(new FlowLayout()); - JScrollPane scroll = - new JScrollPane(root, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scroll.setBorder(BorderFactory.createEmptyBorder()); - frame.setContentPane(scroll); - - Iterator readers = ImageIO.getImageReadersByFormatName("ico"); - if (!readers.hasNext()) { - System.err.println("No reader for format 'ico' found"); - System.exit(1); - } - - ImageReader reader = readers.next(); - - for (String arg : pArgs) { - JPanel panel = new JPanel(null); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - readImagesInFile(arg, reader, panel); - root.add(panel); - } - - frame.pack(); - frame.setVisible(true); - } - - private static void readImagesInFile(String pFileName, ImageReader pReader, final Container pContainer) throws IOException { - File file = new File(pFileName); - if (!file.isFile()) { - System.err.println(pFileName + " not found, or is no file"); - } - - pReader.setInput(ImageIO.createImageInputStream(file)); - int imageCount = pReader.getNumImages(true); - for (int i = 0; i < imageCount; i++) { - try { - addImage(pContainer, pReader, i); - } - catch (Exception e) { - System.err.println("FileName: " + pFileName); - System.err.println("Icon: " + i); - e.printStackTrace(); - } - } - } - - private static JFrame createWindow(final String pTitle) { - JFrame frame = new JFrame(pTitle); - frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - frame.addWindowListener(new WindowAdapter() { - public void windowClosed(WindowEvent e) { - System.exit(0); - } - }); - return frame; - } - - private static void addImage(final Container pParent, final ImageReader pReader, final int pImageNo) throws IOException { - final JButton button = new JButton(); - BufferedImage image = pReader.read(pImageNo); - button.setIcon(new ImageIcon(image) { - TexturePaint mTexture; - - private void createTexture(final GraphicsConfiguration pGraphicsConfiguration) { - BufferedImage pattern = pGraphicsConfiguration.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(); - } - - mTexture = new TexturePaint(pattern, new Rectangle(pattern.getWidth(), pattern.getHeight())); - } - - @Override - public void paintIcon(Component c, Graphics g, int x, int y) { - if (mTexture == null) { - createTexture(c.getGraphicsConfiguration()); - } - Graphics2D gr = (Graphics2D) g; - gr.setPaint(mTexture); - gr.fillRect(x, y, getIconWidth(), getIconHeight()); - super.paintIcon(c, g, x, y); - } - }); - button.setText("" + image.getWidth() + "x" + - image.getHeight() + ": " - + ((image.getColorModel() instanceof IndexColorModel) ? - "" + ((IndexColorModel) image.getColorModel()).getMapSize() : - "TrueColor")); - pParent.add(button); - } } diff --git a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java index f75cbfb9..eebeda17 100755 --- a/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java +++ b/imageio/imageio-ico/src/main/java/com/twelvemonkeys/imageio/plugins/ico/ICOImageReaderSpi.java @@ -43,7 +43,7 @@ import java.util.Locale; * @author Harald Kuhr * @version $Id: ICOImageReaderSpi.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ */ -public class ICOImageReaderSpi extends ImageReaderSpi { +public final class ICOImageReaderSpi extends ImageReaderSpi { public ICOImageReaderSpi() { this(IIOUtil.getProviderInfo(ICOImageReaderSpi.class)); diff --git a/imageio/imageio-ico/src/test/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderTestCase.java b/imageio/imageio-ico/src/test/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderTestCase.java index d8aea766..8a6fc296 100755 --- a/imageio/imageio-ico/src/test/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderTestCase.java +++ b/imageio/imageio-ico/src/test/java/com/twelvemonkeys/imageio/plugins/ico/CURImageReaderTestCase.java @@ -1,6 +1,8 @@ package com.twelvemonkeys.imageio.plugins.ico; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import org.junit.Ignore; +import org.junit.Test; import javax.imageio.ImageReadParam; import javax.imageio.spi.ImageReaderSpi; @@ -10,6 +12,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import static org.junit.Assert.*; + /** * CURImageReaderTestCase * @@ -55,35 +59,42 @@ public class CURImageReaderTestCase extends ImageReaderAbstractTestCase getMIMETypes() { return Arrays.asList("image/vnd.microsoft.icon", "image/ico", "image/x-icon"); } + + @Test + @Ignore("Known issue") + @Override + public void testNotBadCaching() throws IOException { + super.testNotBadCaching(); + } } \ No newline at end of file diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/AbstractMultipaletteChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/AbstractMultipaletteChunk.java new file mode 100644 index 00000000..372589c0 --- /dev/null +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/AbstractMultipaletteChunk.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ +/* + * Parts of this code is based on ilbmtoppm.c + * + * Copyright (C) 1989 by Jef Poskanzer. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, provided + * that the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. This software is provided "as is" without express or + * implied warranty. + * + * Multipalette-support by Ingo Wilken (Ingo.Wilken@informatik.uni-oldenburg.de) + */ + +package com.twelvemonkeys.imageio.plugins.iff; + +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.lang.ref.WeakReference; + +/** + * AbstractMultiPaletteChunk + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractMultiPaletteChunk.java,v 1.0 30.03.12 15:57 haraldk Exp$ + */ +abstract class AbstractMultiPaletteChunk extends IFFChunk implements MultiPalette { + /* scale factor maxval 15 -> maxval 255 */ + static final int FACTOR_4BIT = 17; + + protected MutableIndexColorModel.PaletteChange[] initialChanges; + protected MutableIndexColorModel.PaletteChange[][] changes; + + protected int lastRow; + protected WeakReference originalPalette; + protected MutableIndexColorModel mutablePalette; + + public AbstractMultiPaletteChunk(int pChunkId, int pChunkLength) { + super(pChunkId, pChunkLength); + } + + @Override + void readChunk(final DataInput pInput) throws IOException { + if (chunkId == IFF.CHUNK_SHAM) { + pInput.readUnsignedShort(); // Version, typically 0, skipped + } + + int rows = chunkLength / 32; /* sizeof(word) * 16 */ + + changes = new MutableIndexColorModel.PaletteChange[rows][]; + + for (int row = 0; row < rows; row++) { + changes[row] = new MutableIndexColorModel.PaletteChange[16]; + + for (int i = 0; i < 16; i++) { + changes[row][i] = new MutableIndexColorModel.PaletteChange(); + } + + for (int i = 0; i < 16; i++ ) { + int data = pInput.readUnsignedShort(); + + changes[row][i].index = i; + changes[row][i].r = (byte) (((data & 0x0f00) >> 8) * FACTOR_4BIT); + changes[row][i].g = (byte) (((data & 0x00f0) >> 4) * FACTOR_4BIT); + changes[row][i].b = (byte) (((data & 0x000f) ) * FACTOR_4BIT); + } + } + } + + @Override + void writeChunk(DataOutput pOutput) throws IOException { + throw new UnsupportedOperationException("Method writeChunk not implemented"); + } + + + public ColorModel getColorModel(final IndexColorModel colorModel, final int rowIndex, final boolean laced) { + if (rowIndex < lastRow || mutablePalette == null || originalPalette != null && originalPalette.get() != colorModel) { + originalPalette = new WeakReference(colorModel); + mutablePalette = new MutableIndexColorModel(colorModel); + + if (initialChanges != null) { + mutablePalette.adjustColorMap(initialChanges); + } + } + + for (int i = lastRow + 1; i <= rowIndex; i++) { + int row; + + if (laced && skipLaced()) { + if (i % 2 != 0) { + continue; + } + + row = i / 2; + } + else { + row = i; + } + + if (row < changes.length && changes[row] != null) { + mutablePalette.adjustColorMap(changes[row]); + } + } + + return mutablePalette; + } + + protected boolean skipLaced() { + return false; + } +} diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BMHDChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BMHDChunk.java index c02cb3a8..a784699f 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BMHDChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BMHDChunk.java @@ -40,7 +40,7 @@ import java.io.IOException; * @author Harald Kuhr * @version $Id: BMHDChunk.java,v 1.0 28.feb.2006 00:04:32 haku Exp$ */ -class BMHDChunk extends IFFChunk { +final class BMHDChunk extends IFFChunk { // // typedef UBYTE Masking; /* Choice of masking technique. */ // @@ -81,100 +81,98 @@ class BMHDChunk extends IFFChunk { // words. The number of words per row is words=((w+15)/16) // Dimensions of raster - int mWidth; - int mHeight; + int width; + int height; // Source offsets // Hmm.. Consider making these Image.properties? - int mXPos; - int mYPos; + int xPos; + int yPos; // The number of source bitplanes in the BODY chunk (see below) is stored in // nPlanes. An ILBM with a CMAP but no BODY and nPlanes = 0 is the // recommended way to store a color map. - int mBitplanes; + int bitplanes; - int mMaskType; - int mCompressionType; + int maskType; + int compressionType; - int mTransparentIndex; + int transparentIndex; // NOTE: Typical values are 10:11 (320 x 200) - int mXAspect; - int mYAspect; + int xAspect; + int yAspect; // Source page dimension // NOTE: The image may be larger than the page, probably ignore these - int mPageWidth; - int mPageHeight; + int pageWidth; + int pageHeight; protected BMHDChunk(int pChunkLength) { super(IFF.CHUNK_BMHD, pChunkLength); } - protected BMHDChunk(int pWidth, int pHeight, int pBitplanes, - int pMaskType, int pCompressionType, - int pTransparentIndex) { + protected BMHDChunk(int pWidth, int pHeight, int pBitplanes, int pMaskType, int pCompressionType, int pTransparentIndex) { super(IFF.CHUNK_BMHD, 20); - mWidth = pWidth; - mHeight = pHeight; - mXPos = 0; - mYPos = 0; - mBitplanes = pBitplanes; - mMaskType = pMaskType; - mCompressionType = pCompressionType; - mTransparentIndex = pTransparentIndex; - mXAspect = 1; - mYAspect = 1; - mPageWidth = Math.min(pWidth, Short.MAX_VALUE); // For some reason, these are signed? - mPageHeight = Math.min(pHeight, Short.MAX_VALUE); + width = pWidth; + height = pHeight; + xPos = 0; + yPos = 0; + bitplanes = pBitplanes; + maskType = pMaskType; + compressionType = pCompressionType; + transparentIndex = pTransparentIndex; + xAspect = 1; + yAspect = 1; + pageWidth = Math.min(pWidth, Short.MAX_VALUE); // For some reason, these are signed? + pageHeight = Math.min(pHeight, Short.MAX_VALUE); } void readChunk(DataInput pInput) throws IOException { - if (mChunkLength != 20) { - throw new IIOException("Unknown BMHD chunk length: " + mChunkLength); + if (chunkLength != 20) { + throw new IIOException("Unknown BMHD chunk length: " + chunkLength); } - mWidth = pInput.readUnsignedShort(); - mHeight = pInput.readUnsignedShort(); - mXPos = pInput.readShort(); - mYPos = pInput.readShort(); - mBitplanes = pInput.readUnsignedByte(); - mMaskType = pInput.readUnsignedByte(); - mCompressionType = pInput.readUnsignedByte(); + width = pInput.readUnsignedShort(); + height = pInput.readUnsignedShort(); + xPos = pInput.readShort(); + yPos = pInput.readShort(); + bitplanes = pInput.readUnsignedByte(); + maskType = pInput.readUnsignedByte(); + compressionType = pInput.readUnsignedByte(); pInput.readByte(); // PAD - mTransparentIndex = pInput.readUnsignedShort(); - mXAspect = pInput.readUnsignedByte(); - mYAspect = pInput.readUnsignedByte(); - mPageWidth = pInput.readShort(); - mPageHeight = pInput.readShort(); + transparentIndex = pInput.readUnsignedShort(); + xAspect = pInput.readUnsignedByte(); + yAspect = pInput.readUnsignedByte(); + pageWidth = pInput.readShort(); + pageHeight = pInput.readShort(); } void writeChunk(DataOutput pOutput) throws IOException { - pOutput.writeInt(mChunkId); - pOutput.writeInt(mChunkLength); + pOutput.writeInt(chunkId); + pOutput.writeInt(chunkLength); - pOutput.writeShort(mWidth); - pOutput.writeShort(mHeight); - pOutput.writeShort(mXPos); - pOutput.writeShort(mYPos); - pOutput.writeByte(mBitplanes); - pOutput.writeByte(mMaskType); - pOutput.writeByte(mCompressionType); + pOutput.writeShort(width); + pOutput.writeShort(height); + pOutput.writeShort(xPos); + pOutput.writeShort(yPos); + pOutput.writeByte(bitplanes); + pOutput.writeByte(maskType); + pOutput.writeByte(compressionType); pOutput.writeByte(0); // PAD - pOutput.writeShort(mTransparentIndex); - pOutput.writeByte(mXAspect); - pOutput.writeByte(mYAspect); - pOutput.writeShort(mPageWidth); - pOutput.writeShort(mPageHeight); + pOutput.writeShort(transparentIndex); + pOutput.writeByte(xAspect); + pOutput.writeByte(yAspect); + pOutput.writeShort(pageWidth); + pOutput.writeShort(pageHeight); } public String toString() { return super.toString() - + " {w=" + mWidth + ", h=" + mHeight - + ", x=" + mXPos + ", y=" + mYPos - + ", planes=" + mBitplanes + ", mask=" + mMaskType - + ", compression=" + mCompressionType + ", trans=" + mTransparentIndex - + ", xAspect=" + mXAspect + ", yAspect=" + mYAspect - + ", pageWidth=" + mPageWidth + ", pageHeight=" + mPageHeight + "}"; + + " {w=" + width + ", h=" + height + + ", x=" + xPos + ", y=" + yPos + + ", planes=" + bitplanes + ", mask=" + maskType + + ", compression=" + compressionType + ", trans=" + transparentIndex + + ", xAspect=" + xAspect + ", yAspect=" + yAspect + + ", pageWidth=" + pageWidth + ", pageHeight=" + pageHeight + "}"; } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BODYChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BODYChunk.java index 9cbb9497..42a5bbf7 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BODYChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/BODYChunk.java @@ -39,8 +39,7 @@ import java.io.DataOutput; * @author Harald Kuhr * @version $Id: BODYChunk.java,v 1.0 28.feb.2006 01:25:49 haku Exp$ */ -class BODYChunk extends IFFChunk { - +final class BODYChunk extends IFFChunk { protected BODYChunk(int pChunkLength) { super(IFF.CHUNK_BODY, pChunkLength); } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CAMGChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CAMGChunk.java index 98ecb5f8..a2e6818f 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CAMGChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CAMGChunk.java @@ -40,35 +40,43 @@ import java.io.IOException; * @author Harald Kuhr * @version $Id: CAMGChunk.java,v 1.0 28.feb.2006 02:10:07 haku Exp$ */ -class CAMGChunk extends IFFChunk { - +final class CAMGChunk extends IFFChunk { // HIRES=0x8000, LACE=0x4 // #define CAMG_HAM 0x800 /* hold and modify */ // #define CAMG_EHB 0x80 /* extra halfbrite */ - private int mCAMG; + private int camg; public CAMGChunk(int pLength) { super(IFF.CHUNK_CAMG, pLength); } void readChunk(DataInput pInput) throws IOException { - if (mChunkLength != 4) { - throw new IIOException("Unknown CAMG chunk length: " + mChunkLength); + if (chunkLength != 4) { + throw new IIOException("Unknown CAMG chunk length: " + chunkLength); } - mCAMG = pInput.readInt(); + + camg = pInput.readInt(); } void writeChunk(DataOutput pOutput) throws IOException { throw new InternalError("Not implemented: writeChunk()"); } + boolean isHires() { + return (camg & 0x8000) != 0; + } + + boolean isLaced() { + return (camg & 0x4) != 0; + } + boolean isHAM() { - return (mCAMG & 0x800) != 0; + return (camg & 0x800) != 0; } boolean isEHB() { - return (mCAMG & 0x80) != 0; + return (camg & 0x80) != 0; } public String toString() { diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java index 4c0fa687..ed79b5bf 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CMAPChunk.java @@ -37,6 +37,7 @@ import java.awt.image.WritableRaster; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import java.util.Arrays; /** * CMAPChunk @@ -45,7 +46,7 @@ import java.io.IOException; * @author Harald Kuhr * @version $Id: CMAPChunk.java,v 1.0 28.feb.2006 00:38:05 haku Exp$ */ -class CMAPChunk extends IFFChunk { +final class CMAPChunk extends IFFChunk { // typedef struct { // UBYTE red, green, blue; /* color intensities 0..255 */ @@ -53,61 +54,32 @@ class CMAPChunk extends IFFChunk { // // typedef ColorRegister ColorMap[n]; /* size = 3n bytes */ + byte[] reds; + byte[] greens; + byte[] blues; - byte[] mReds; - byte[] mGreens; - byte[] mBlues; + private IndexColorModel model; - boolean mEHB; - - final private BMHDChunk mHeader; - final private CAMGChunk mCamg; - private IndexColorModel mModel; - - protected CMAPChunk(int pChunkLength, BMHDChunk pHeader, CAMGChunk pCamg) { + protected CMAPChunk(final int pChunkLength) { super(IFF.CHUNK_CMAP, pChunkLength); - mHeader = pHeader; - mCamg = pCamg; } - public CMAPChunk(IndexColorModel pModel) { + public CMAPChunk(final IndexColorModel pModel) { super(IFF.CHUNK_CMAP, pModel.getMapSize() * 3); - mModel = pModel; - mHeader = null; - mCamg = null; + model = pModel; } - void readChunk(DataInput pInput) throws IOException { - int numColors = mChunkLength / 3; - int paletteSize = numColors; + void readChunk(final DataInput pInput) throws IOException { + int numColors = chunkLength / 3; - boolean isEHB = mCamg != null && mCamg.isEHB(); - if (isEHB) { - if (numColors == 32) { - paletteSize = 64; - } - else if (numColors != 64) { - throw new IIOException("Unknown number of colors for EHB: " + numColors); - } - } - - mReds = new byte[paletteSize]; - mGreens = mReds.clone(); - mBlues = mReds.clone(); + reds = new byte[numColors]; + greens = reds.clone(); + blues = reds.clone(); for (int i = 0; i < numColors; i++) { - mReds[i] = pInput.readByte(); - mGreens[i] = pInput.readByte(); - mBlues[i] = pInput.readByte(); - } - - if (isEHB && numColors == 32) { - // Create the half-brite colors - for (int i = 0; i < numColors; i++) { - mReds[i + numColors] = (byte) ((mReds[i] & 0xff) / 2); - mGreens[i + numColors] = (byte) ((mGreens[i] & 0xff) / 2); - mBlues[i + numColors] = (byte) ((mBlues[i] & 0xff) / 2); - } + reds[i] = pInput.readByte(); + greens[i] = pInput.readByte(); + blues[i] = pInput.readByte(); } // TODO: When reading in a CMAP for 8-bit-per-gun display or @@ -116,52 +88,41 @@ class CMAPChunk extends IFFChunk { // rather than scaled, and provide your own scaling. // Use defaults if the color map is absent or has fewer color registers // than you need. Ignore any extra color registers. - // R8 := (Rn x 255 ) / maxColor // All chunks are WORD aligned (even sized), may need to read pad... - if (mChunkLength % 2 != 0) { + if (chunkLength % 2 != 0) { pInput.readByte(); } - - // TODO: Bitmask transparency - // Would it work to double to numbers of colors, and create an indexcolormodel, - // with alpha, where all colors above the original color is all transparent? - // This is a waste of time and space, of course... - int trans = mHeader.mMaskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? mHeader.mTransparentIndex : -1; - mModel = new InverseColorMapIndexColorModel(mHeader.mBitplanes, mReds.length, mReds, mGreens, mBlues, trans); } - void writeChunk(DataOutput pOutput) throws IOException { - pOutput.writeInt(mChunkId); - pOutput.writeInt(mChunkLength); + void writeChunk(final DataOutput pOutput) throws IOException { + pOutput.writeInt(chunkId); + pOutput.writeInt(chunkLength); - final int length = mModel.getMapSize(); + final int length = model.getMapSize(); for (int i = 0; i < length; i++) { - pOutput.writeByte(mModel.getRed(i)); - pOutput.writeByte(mModel.getGreen(i)); - pOutput.writeByte(mModel.getBlue(i)); + pOutput.writeByte(model.getRed(i)); + pOutput.writeByte(model.getGreen(i)); + pOutput.writeByte(model.getBlue(i)); } - if (mChunkLength % 2 != 0) { + if (chunkLength % 2 != 0) { pOutput.writeByte(0); // PAD } } public String toString() { - return super.toString() + " {colorMap=" + mModel + "}"; + return super.toString() + " {colorMap=" + model + "}"; } - IndexColorModel getIndexColorModel() { - return mModel; - } - - public BufferedImage createPaletteImage() { + BufferedImage createPaletteImage(final BMHDChunk header, boolean isEHB) throws IIOException { // Create a 1 x colors.length image - IndexColorModel cm = getIndexColorModel(); + IndexColorModel cm = getIndexColorModel(header, isEHB); WritableRaster raster = cm.createCompatibleWritableRaster(cm.getMapSize(), 1); byte[] pixel = null; + for (int x = 0; x < cm.getMapSize(); x++) { pixel = (byte[]) cm.getDataElements(cm.getRGB(x), pixel); raster.setDataElements(x, 0, pixel); @@ -169,4 +130,38 @@ class CMAPChunk extends IFFChunk { return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); } + + public IndexColorModel getIndexColorModel(final BMHDChunk header, boolean isEHB) throws IIOException { + if (model == null) { + int numColors = reds.length; // All arrays are same size + + if (isEHB) { + if (numColors == 32) { + reds = Arrays.copyOf(reds, numColors * 2); + blues = Arrays.copyOf(blues, numColors * 2); + greens = Arrays.copyOf(greens, numColors * 2); + } + else if (numColors != 64) { + throw new IIOException("Unknown number of colors for EHB: " + numColors); + } + + // Create the half-brite colors + // We do this regardless of the colors read, as the color map may contain trash values + for (int i = 0; i < 32; i++) { + reds[i + 32] = (byte) ((reds[i] & 0xff) / 2); + greens[i + 32] = (byte) ((greens[i] & 0xff) / 2); + blues[i + 32] = (byte) ((blues[i] & 0xff) / 2); + } + } + + // TODO: Bitmask transparency + // Would it work to double to numbers of colors, and create an indexcolormodel, + // with alpha, where all colors above the original color is all transparent? + // This is a waste of time and space, of course... + int transparent = header.maskType == BMHDChunk.MASK_TRANSPARENT_COLOR ? header.transparentIndex : -1; + model = new InverseColorMapIndexColorModel(header.bitplanes, reds.length, reds, greens, blues, transparent); + } + + return model; + } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CTBLChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CTBLChunk.java new file mode 100644 index 00000000..6062518f --- /dev/null +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/CTBLChunk.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.iff; + +/** + * CTBLChunk + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CTBLChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$ + */ +final class CTBLChunk extends AbstractMultiPaletteChunk { + protected CTBLChunk(int pChunkLength) { + super(IFF.CHUNK_CTBL, pChunkLength); + } +} diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GRABChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GRABChunk.java index bb4cc03b..bf69b27b 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GRABChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GRABChunk.java @@ -42,12 +42,12 @@ import java.io.IOException; * @author Harald Kuhr * @version $Id: GRABChunk.java,v 1.0 28.feb.2006 01:55:05 haku Exp$ */ -class GRABChunk extends IFFChunk { +final class GRABChunk extends IFFChunk { // typedef struct { // WORD x, y; /* relative coordinates (pixels) */ // } Point2D; - Point2D mPoint; + Point2D point; protected GRABChunk(int pChunkLength) { super(IFF.CHUNK_GRAB, pChunkLength); @@ -55,22 +55,22 @@ class GRABChunk extends IFFChunk { protected GRABChunk(Point2D pPoint) { super(IFF.CHUNK_GRAB, 4); - mPoint = pPoint; + point = pPoint; } void readChunk(DataInput pInput) throws IOException { - if (mChunkLength != 4) { - throw new IIOException("Unknown GRAB chunk size: " + mChunkLength); + if (chunkLength != 4) { + throw new IIOException("Unknown GRAB chunk size: " + chunkLength); } - mPoint = new Point(pInput.readShort(), pInput.readShort()); + point = new Point(pInput.readShort(), pInput.readShort()); } void writeChunk(DataOutput pOutput) throws IOException { - pOutput.writeShort((int) mPoint.getX()); - pOutput.writeShort((int) mPoint.getY()); + pOutput.writeShort((int) point.getX()); + pOutput.writeShort((int) point.getY()); } public String toString() { - return super.toString() + " {point=" + mPoint + "}"; + return super.toString() + " {point=" + point + "}"; } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GenericChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GenericChunk.java index 0f14ff90..78b5f3a1 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GenericChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/GenericChunk.java @@ -39,47 +39,39 @@ import java.io.DataOutput; * @author Harald Kuhr * @version $Id: UnknownChunk.java,v 1.0 28.feb.2006 00:53:47 haku Exp$ */ -class GenericChunk extends IFFChunk { +final class GenericChunk extends IFFChunk { - byte[] mData; + byte[] data; protected GenericChunk(int pChunkId, int pChunkLength) { super(pChunkId, pChunkLength); - mData = new byte[pChunkLength <= 50 ? pChunkLength : 47]; + data = new byte[pChunkLength <= 50 ? pChunkLength : 47]; } protected GenericChunk(int pChunkId, byte[] pChunkData) { super(pChunkId, pChunkData.length); - mData = pChunkData; + data = pChunkData; } void readChunk(DataInput pInput) throws IOException { - pInput.readFully(mData, 0, mData.length); + pInput.readFully(data, 0, data.length); - int toSkip = mChunkLength - mData.length; - while (toSkip > 0) { - toSkip -= pInput.skipBytes(toSkip); - } - - // Read pad - if (mChunkLength % 2 != 0) { - pInput.readByte(); - } + skipData(pInput, chunkLength, data.length); } void writeChunk(DataOutput pOutput) throws IOException { - pOutput.writeInt(mChunkId); - pOutput.writeInt(mChunkLength); - pOutput.write(mData, 0, mData.length); + pOutput.writeInt(chunkId); + pOutput.writeInt(chunkLength); + pOutput.write(data, 0, data.length); - if (mData.length % 2 != 0) { + if (data.length % 2 != 0) { pOutput.writeByte(0); // PAD } } public String toString() { return super.toString() + " {value=\"" - + new String(mData, 0, mData.length <= 50 ? mData.length : 47) - + (mChunkLength <= 50 ? "" : "...") + "\"}"; + + new String(data, 0, data.length <= 50 ? data.length : 47) + + (chunkLength <= 50 ? "" : "...") + "\"}"; } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFF.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFF.java index 504d5aa9..c9185d79 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFF.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFF.java @@ -39,12 +39,22 @@ interface IFF { /** IFF FORM group chunk */ int CHUNK_FORM = ('F' << 24) + ('O' << 16) + ('R' << 8) + 'M'; - /** IFF ILBM form type */ + /** IFF ILBM form type (Interleaved bitmap) */ int TYPE_ILBM = ('I' << 24) + ('L' << 16) + ('B' << 8) + 'M'; - /** IFF PBM form type */ + /** IFF PBM form type (Packed bitmap) */ int TYPE_PBM = ('P' << 24) + ('B' << 16) + ('M' << 8) + ' '; + // TODO: + /** IFF DEEP form type (TVPaint) */ + int TYPE_DEEP = ('D' << 24) + ('E' << 16) + ('E' << 8) + 'P'; + /** IFF RGB8 form type (TurboSilver) */ + int TYPE_RGB8 = ('R' << 24) + ('G' << 16) + ('B' << 8) + '8'; + /** IFF RGBN form type (TurboSilver) */ + int TYPE_RGBN = ('R' << 24) + ('G' << 16) + ('B' << 8) + 'N'; + /** IFF ACBM form type (Amiga Basic) */ + int TYPE_ACBM = ('A' << 24) + ('C' << 16) + ('B' << 8) + 'M'; + /** Bitmap Header chunk */ int CHUNK_BMHD = ('B' << 24) + ('M' << 16) + ('H' << 8) + 'D'; @@ -65,4 +75,53 @@ interface IFF { /** Main data (body) chunk */ int CHUNK_BODY = ('B' << 24) + ('O' << 16) + ('D' << 8) + 'Y'; + + /** Junk (to allow garbage data in files, without re-writing the entire file) */ + int CHUNK_JUNK = ('J' << 24) + ('U' << 16) + ('N' << 8) + 'K'; + + /** EA IFF 85 Generic Author chunk */ + int CHUNK_AUTH = ('A' << 24) + ('U' << 16) + ('T' << 8) + 'H'; + /** EA IFF 85 Generic character string chunk */ + int CHUNK_CHRS = ('C' << 24) + ('H' << 16) + ('R' << 8) + 'S'; + /** EA IFF 85 Generic Name of art, music, etc. chunk */ + int CHUNK_NAME = ('N' << 24) + ('A' << 16) + ('M' << 8) + 'E'; + /** EA IFF 85 Generic unformatted ASCII text chunk */ + int CHUNK_TEXT = ('T' << 24) + ('E' << 16) + ('X' << 8) + 'T'; + /** EA IFF 85 Generic Copyright text chunk */ + int CHUNK_COPY = ('(' << 24) + ('c' << 16) + (')' << 8) + ' '; + + /** color cycling */ + int CHUNK_CRNG = ('C' << 24) + ('R' << 16) + ('N' << 8) + 'G'; + /** color cycling */ + int CHUNK_CCRT = ('C' << 24) + ('C' << 16) + ('R' << 8) + 'T'; + /** Color Lookup Table chunk */ + int CHUNK_CLUT = ('C' << 24) + ('L' << 16) + ('U' << 8) + 'T'; + /** Dots per inch chunk */ + int CHUNK_DPI = ('D' << 24) + ('P' << 16) + ('I' << 8) + ' '; + /** DPaint perspective chunk (EA) */ + int CHUNK_DPPV = ('D' << 24) + ('P' << 16) + ('P' << 8) + 'V'; + /** DPaint IV enhanced color cycle chunk (EA) */ + int CHUNK_DRNG = ('D' << 24) + ('R' << 16) + ('N' << 8) + 'G'; + /** Encapsulated Postscript chunk */ + int CHUNK_EPSF = ('E' << 24) + ('P' << 16) + ('S' << 8) + 'F'; + /** Cyan, Magenta, Yellow, & Black color map (Soft-Logik) */ + int CHUNK_CMYK = ('C' << 24) + ('M' << 16) + ('Y' << 8) + 'K'; + /** Color naming chunk (Soft-Logik) */ + int CHUNK_CNAM = ('C' << 24) + ('N' << 16) + ('A' << 8) + 'M'; + /** Line by line palette control information (Sebastiano Vigna) */ + int CHUNK_PCHG = ('P' << 24) + ('C' << 16) + ('H' << 8) + 'G'; + /** A mini duplicate ILBM used for preview (Gary Bonham) */ + int CHUNK_PRVW = ('P' << 24) + ('R' << 16) + ('V' << 8) + 'W'; + /** eXtended BitMap Information (Soft-Logik) */ + int CHUNK_XBMI = ('X' << 24) + ('B' << 16) + ('M' << 8) + 'I'; + /** Newtek Dynamic Ham color chunk */ + int CHUNK_CTBL = ('C' << 24) + ('T' << 16) + ('B' << 8) + 'L'; + /** Newtek Dynamic Ham chunk */ + int CHUNK_DYCP = ('D' << 24) + ('Y' << 16) + ('C' << 8) + 'P'; + /** Sliced HAM color chunk */ + int CHUNK_SHAM = ('S' << 24) + ('H' << 16) + ('A' << 8) + 'M'; + /** ACBM body chunk */ + int CHUNK_ABIT = ('A' << 24) + ('B' << 16) + ('I' << 8) + 'T'; + /** unofficial direct color */ + int CHUNK_DCOL = ('D' << 24) + ('C' << 16) + ('O' << 8) + 'L'; } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFChunk.java index c5517611..1c6b3982 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFChunk.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFChunk.java @@ -40,19 +40,32 @@ import java.io.DataOutput; * @version $Id: IFFChunk.java,v 1.0 28.feb.2006 00:00:45 haku Exp$ */ abstract class IFFChunk { - int mChunkId; - int mChunkLength; + int chunkId; + int chunkLength; protected IFFChunk(int pChunkId, int pChunkLength) { - mChunkId = pChunkId; - mChunkLength = pChunkLength; + chunkId = pChunkId; + chunkLength = pChunkLength; } abstract void readChunk(DataInput pInput) throws IOException; abstract void writeChunk(DataOutput pOutput) throws IOException; + protected static void skipData(final DataInput pInput, final int chunkLength, final int dataReadSoFar) throws IOException { + int toSkip = chunkLength - dataReadSoFar; + + while (toSkip > 0) { + toSkip -= pInput.skipBytes(toSkip); + } + + // Read pad + if (chunkLength % 2 != 0) { + pInput.readByte(); + } + } + public String toString() { - return IFFUtil.toChunkStr(mChunkId) + " chunk (" + mChunkLength + " bytes)"; + return IFFUtil.toChunkStr(chunkId) + " chunk (" + chunkLength + " bytes)"; } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java index dd06892f..53f2fc8c 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java @@ -28,6 +28,7 @@ package com.twelvemonkeys.imageio.plugins.iff; +import com.twelvemonkeys.image.ResampleOp; import com.twelvemonkeys.imageio.ImageReaderBase; import com.twelvemonkeys.imageio.stream.BufferedImageInputStream; import com.twelvemonkeys.imageio.util.IIOUtil; @@ -94,18 +95,22 @@ public class IFFImageReader extends ImageReaderBase { // http://home.comcast.net/~erniew/lwsdk/docs/filefmts/ilbm.html // http://www.fileformat.info/format/iff/spec/7866a9f0e53c42309af667c5da3bd426/view.htm // - Contains definitions of some "new" chunks, as well as alternative FORM types - // http://amigan.1emu.net/reg/iff.html + // http://amigan.1emu.net/index/iff.html - private BMHDChunk mHeader; - private CMAPChunk mColorMap; - private BODYChunk mBody; - private GRABChunk mGrab; - private CAMGChunk mViewPort; - private int mFormType; - private long mBodyStart; + // TODO: Allow reading rasters for HAM6/HAM8 and multipalette images that are expanded to RGB (24 bit) during read. - private BufferedImage mImage; - private DataInputStream mByteRunStream; + private BMHDChunk header; + private CMAPChunk colorMap; + private BODYChunk body; + @SuppressWarnings({"FieldCanBeLocal"}) + private GRABChunk grab; + private CAMGChunk viewPort; + private MultiPalette paletteChange; + private int formType; + private long bodyStart; + + private BufferedImage image; + private DataInputStream byteRunStream; public IFFImageReader() { super(IFFImageReaderSpi.sharedProvider()); @@ -118,42 +123,44 @@ public class IFFImageReader extends ImageReaderBase { private void init(int pIndex) throws IOException { checkBounds(pIndex); - if (mHeader == null) { + if (header == null) { readMeta(); } } protected void resetMembers() { - mHeader = null; - mColorMap = null; - mBody = null; - mViewPort = null; - mFormType = 0; + header = null; + colorMap = null; + paletteChange = null; + body = null; + viewPort = null; + formType = 0; - mImage = null; - mByteRunStream = null; + image = null; + byteRunStream = null; } private void readMeta() throws IOException { - if (mImageInput.readInt() != IFF.CHUNK_FORM) { - throw new IIOException("Unknown file format for IFFImageReader"); + int chunkType = imageInput.readInt(); + if (chunkType != IFF.CHUNK_FORM) { + throw new IIOException(String.format("Unknown file format for IFFImageReader, expected 'FORM': %s", IFFUtil.toChunkStr(chunkType))); } - int remaining = mImageInput.readInt() - 4; // We'll read 4 more in a sec + int remaining = imageInput.readInt() - 4; // We'll read 4 more in a sec - mFormType = mImageInput.readInt(); - if (mFormType != IFF.TYPE_ILBM && mFormType != IFF.TYPE_PBM) { - throw new IIOException("Only IFF (FORM) type ILBM and PBM supported: " + IFFUtil.toChunkStr(mFormType)); + formType = imageInput.readInt(); + if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM) { + throw new IIOException(String.format("Only IFF FORM types 'ILBM' and 'PBM ' supported: %s", IFFUtil.toChunkStr(formType))); } //System.out.println("IFF type FORM " + toChunkStr(type)); - mGrab = null; - mViewPort = null; + grab = null; + viewPort = null; while (remaining > 0) { - int chunkId = mImageInput.readInt(); - int length = mImageInput.readInt(); + int chunkId = imageInput.readInt(); + int length = imageInput.readInt(); remaining -= 8; remaining -= length % 2 == 0 ? length : length + 1; @@ -163,61 +170,112 @@ public class IFFImageReader extends ImageReaderBase { switch (chunkId) { case IFF.CHUNK_BMHD: - if (mHeader != null) { + if (header != null) { throw new IIOException("Multiple BMHD chunks not allowed"); } - mHeader = new BMHDChunk(length); - mHeader.readChunk(mImageInput); + header = new BMHDChunk(length); + header.readChunk(imageInput); - //System.out.println(mHeader); + //System.out.println(header); break; case IFF.CHUNK_CMAP: - if (mColorMap != null) { + if (colorMap != null) { throw new IIOException("Multiple CMAP chunks not allowed"); } - mColorMap = new CMAPChunk(length, mHeader, mViewPort); - mColorMap.readChunk(mImageInput); - //System.out.println(mColorMap); + colorMap = new CMAPChunk(length); + colorMap.readChunk(imageInput); + + //System.out.println(colorMap); break; case IFF.CHUNK_GRAB: - if (mGrab != null) { + if (grab != null) { throw new IIOException("Multiple GRAB chunks not allowed"); } - mGrab = new GRABChunk(length); - mGrab.readChunk(mImageInput); + grab = new GRABChunk(length); + grab.readChunk(imageInput); - //System.out.println(mGrab); + //System.out.println(grab); break; case IFF.CHUNK_CAMG: - if (mViewPort != null) { + if (viewPort != null) { throw new IIOException("Multiple CAMG chunks not allowed"); } - mViewPort = new CAMGChunk(length); - mViewPort.readChunk(mImageInput); + viewPort = new CAMGChunk(length); + viewPort.readChunk(imageInput); - //System.out.println(mViewPort); +// System.out.println(viewPort); break; + case IFF.CHUNK_PCHG: + if (paletteChange instanceof PCHGChunk) { + throw new IIOException("Multiple PCHG chunks not allowed"); + } + + PCHGChunk pchg = new PCHGChunk(length); + pchg.readChunk(imageInput); + + // Always prefer PCHG style palette changes + paletteChange = pchg; + +// System.out.println(pchg); + break; + + case IFF.CHUNK_SHAM: + if (paletteChange instanceof SHAMChunk) { + throw new IIOException("Multiple SHAM chunks not allowed"); + } + + SHAMChunk sham = new SHAMChunk(length); + sham.readChunk(imageInput); + + // NOTE: We prefer PHCG to SHAM style palette changes, if both are present + if (paletteChange == null) { + paletteChange = sham; + } + +// System.out.println(sham); + break; + + case IFF.CHUNK_CTBL: + if (paletteChange instanceof CTBLChunk) { + throw new IIOException("Multiple CTBL chunks not allowed"); + } + + CTBLChunk ctbl = new CTBLChunk(length); + ctbl.readChunk(imageInput); + + // NOTE: We prefer PHCG to CTBL style palette changes, if both are present + if (paletteChange == null) { + paletteChange = ctbl; + } + +// System.out.println(ctbl); + break; + + case IFF.CHUNK_JUNK: + // Always skip junk chunks + IFFChunk.skipData(imageInput, length, 0); + break; + case IFF.CHUNK_BODY: - if (mBody != null) { + if (body != null) { throw new IIOException("Multiple BODY chunks not allowed"); } - mBody = new BODYChunk(length); - mBodyStart = mImageInput.getStreamPosition(); + body = new BODYChunk(length); + bodyStart = imageInput.getStreamPosition(); // NOTE: We don't read the body here, it's done later in the read(int, ImageReadParam) method - // Done reading meta return; default: - // TODO: We probably want to store anno chunks as Metadata - // ANNO, DEST, SPRT and more + // TODO: We probably want to store ANNO, TEXT, AUTH, COPY etc chunks as Metadata + // SHAM, ANNO, DEST, SPRT and more IFFChunk generic = new GenericChunk(chunkId, length); - generic.readChunk(mImageInput); + generic.readChunk(imageInput); - //System.out.println(generic); +// System.out.println(generic); break; } } @@ -228,23 +286,23 @@ public class IFFImageReader extends ImageReaderBase { processImageStarted(pIndex); - mImage = getDestination(pParam, getImageTypes(pIndex), mHeader.mWidth, mHeader.mHeight); - //System.out.println(mBody); - if (mBody != null) { + image = getDestination(pParam, getImageTypes(pIndex), header.width, header.height); + //System.out.println(body); + if (body != null) { //System.out.println("Read body"); readBody(pParam); } else { // TODO: Remove this hack when we have metadata // In the rare case of an ILBM containing nothing but a CMAP - //System.out.println(mColorMap); - if (mColorMap != null) { + //System.out.println(colorMap); + if (colorMap != null) { //System.out.println("Creating palette!"); - mImage = mColorMap.createPaletteImage(); + image = colorMap.createPaletteImage(header, isEHB()); } } - BufferedImage result = mImage; + BufferedImage result = image; processImageComplete(); @@ -253,12 +311,12 @@ public class IFFImageReader extends ImageReaderBase { public int getWidth(int pIndex) throws IOException { init(pIndex); - return mHeader.mWidth; + return header.width; } public int getHeight(int pIndex) throws IOException { init(pIndex); - return mHeader.mHeight; + return header.height; } public Iterator getImageTypes(int pIndex) throws IOException { @@ -266,8 +324,8 @@ public class IFFImageReader extends ImageReaderBase { List types = Arrays.asList( getRawImageType(pIndex), - ImageTypeSpecifier.createFromBufferedImageType(mHeader.mBitplanes == 32 ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR) -// TODO: ImageTypeSpecifier.createFromBufferedImageType(mHeader.mBitplanes == 32 ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB), + ImageTypeSpecifier.createFromBufferedImageType(header.bitplanes == 32 ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR) + // TODO: ImageTypeSpecifier.createFromBufferedImageType(header.bitplanes == 32 ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB), // TODO: Allow 32 bit always. Allow RGB and discard alpha, if present? ); return types.iterator(); @@ -278,9 +336,9 @@ public class IFFImageReader extends ImageReaderBase { init(pIndex); // TODO: Stay DRY... // TODO: Use this for creating the Image/Buffer in the read code below... - // NOTE: mColorMap may be null for 8 bit (gray), 24 bit or 32 bit only + // NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only ImageTypeSpecifier specifier; - switch (mHeader.mBitplanes) { + switch (header.bitplanes) { case 1: // 1 bit case 2: @@ -296,9 +354,9 @@ public class IFFImageReader extends ImageReaderBase { case 8: // 8 bit // May be HAM8 - if (!isHAM()) { - if (mColorMap != null) { - IndexColorModel cm = mColorMap.getIndexColorModel(); + if (!isConvertToRGB()) { + if (colorMap != null) { + IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB()); specifier = IndexedImageTypeSpecifier.createFromIndexColorModel(cm); break; } @@ -317,29 +375,33 @@ public class IFFImageReader extends ImageReaderBase { specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); break; default: - throw new IIOException(String.format("Bit depth not implemented: %d", mHeader.mBitplanes)); + throw new IIOException(String.format("Bit depth not implemented: %d", header.bitplanes)); } return specifier; } - private void readBody(final ImageReadParam pParam) throws IOException { - mImageInput.seek(mBodyStart); - mByteRunStream = null; + private boolean isConvertToRGB() { + return isHAM() || isPCHG() || isSHAM(); + } - // NOTE: mColorMap may be null for 8 bit (gray), 24 bit or 32 bit only - if (mColorMap != null) { - IndexColorModel cm = mColorMap.getIndexColorModel(); - readIndexed(pParam, mImageInput, cm); + private void readBody(final ImageReadParam pParam) throws IOException { + imageInput.seek(bodyStart); + byteRunStream = null; + + // NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only + if (colorMap != null) { + IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB()); + readIndexed(pParam, imageInput, cm); } else { - readTrueColor(pParam, mImageInput); + readTrueColor(pParam, imageInput); } } private void readIndexed(final ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException { - final int width = mHeader.mWidth; - final int height = mHeader.mHeight; + final int width = header.width; + final int height = header.height; final Rectangle aoi = getSourceRegion(pParam, width, height); final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset(); @@ -360,9 +422,9 @@ public class IFFImageReader extends ImageReaderBase { } // Ensure band settings from param are compatible with images - checkReadParamBandSettings(pParam, isHAM() ? 3 : 1, mImage.getSampleModel().getNumBands()); + checkReadParamBandSettings(pParam, isConvertToRGB() ? 3 : 1, image.getSampleModel().getNumBands()); - WritableRaster destination = mImage.getRaster(); + WritableRaster destination = image.getRaster(); if (destinationBands != null || offset.x != 0 || offset.y != 0) { destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); } @@ -375,7 +437,7 @@ public class IFFImageReader extends ImageReaderBase { ColorModel cm; WritableRaster raster; - if (isHAM()) { + if (isConvertToRGB()) { // TODO: If HAM6, use type USHORT_444_RGB or 2BYTE_444_RGB? // Or create a HAMColorModel, if at all possible? // TYPE_3BYTE_BGR @@ -397,13 +459,12 @@ public class IFFImageReader extends ImageReaderBase { final byte[] row = new byte[width * 8]; - //System.out.println("Data length: " + data.length); - //System.out.println("PlaneData length: " + planeData.length * planeData[0].length); - //System.out.println("Row length: " + row.length); +// System.out.println("PlaneData length: " + planeData.length); +// System.out.println("Row length: " + row.length); final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - final int planes = mHeader.mBitplanes; + final int planes = header.bitplanes; Object dataElements = null; Object outDataElements = null; @@ -422,7 +483,7 @@ public class IFFImageReader extends ImageReaderBase { return; } - if (mFormType == IFF.TYPE_ILBM) { + if (formType == IFF.TYPE_ILBM) { int pixelPos = 0; for (int planePos = 0; planePos < planeWidth; planePos++) { IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1); @@ -432,23 +493,24 @@ public class IFFImageReader extends ImageReaderBase { if (isHAM()) { hamToRGB(row, pModel, data, 0); } + else if (isConvertToRGB()) { + multiPaletteToRGB(srcY, row, pModel, data, 0); + } else { raster.setDataElements(0, 0, width, 1, row); } } - else if (mFormType == IFF.TYPE_PBM) { - // TODO: Arraycopy might not be necessary, if it's okay with row larger than width - System.arraycopy(planeData, 0, row, 0, mHeader.mBitplanes * planeWidth); - raster.setDataElements(0, 0, width, 1, row); + else if (formType == IFF.TYPE_PBM) { + raster.setDataElements(0, 0, width, 1, planeData); } else { - throw new AssertionError(String.format("Unsupported FORM type: %s", mFormType)); + throw new AssertionError(String.format("Unsupported FORM type: %s", formType)); } int dstY = (srcY - aoi.y) / sourceYSubsampling; // Handle non-converting raster as special case for performance if (cm.isCompatibleRaster(destination)) { - // Rasters are compatible, just write to destinaiton + // Rasters are compatible, just write to destination if (sourceXSubsampling == 1) { destination.setRect(offset.x, dstY, sourceRow); // dataElements = raster.getDataElements(aoi.x, 0, aoi.width, 1, dataElements); @@ -470,7 +532,7 @@ public class IFFImageReader extends ImageReaderBase { for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { dataElements = sourceRow.getDataElements(srcX, 0, dataElements); int rgb = icm.getRGB(dataElements); - outDataElements = mImage.getColorModel().getDataElements(rgb, outDataElements); + outDataElements = image.getColorModel().getDataElements(rgb, outDataElements); int dstX = srcX / sourceXSubsampling; destination.setDataElements(dstX, dstY, outDataElements); } @@ -479,7 +541,7 @@ public class IFFImageReader extends ImageReaderBase { // TODO: This branch is never tested, and is probably "dead" // ColorConvertOp if (converter == null) { - converter = new ColorConvertOp(cm.getColorSpace(), mImage.getColorModel().getColorSpace(), null); + converter = new ColorConvertOp(cm.getColorSpace(), image.getColorModel().getColorSpace(), null); } converter.filter( raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, null), @@ -488,7 +550,7 @@ public class IFFImageReader extends ImageReaderBase { } } - processImageProgress(srcY * 100f / mHeader.mWidth); + processImageProgress(srcY * 100f / header.width); if (abortRequested()) { processReadAborted(); break; @@ -502,8 +564,8 @@ public class IFFImageReader extends ImageReaderBase { // bit of the red value for each pixel, and the last holds the most // significant bit of the blue value. private void readTrueColor(ImageReadParam pParam, final ImageInputStream pInput) throws IOException { - final int width = mHeader.mWidth; - final int height = mHeader.mHeight; + final int width = header.width; + final int height = header.height; final Rectangle aoi = getSourceRegion(pParam, width, height); final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset(); @@ -524,29 +586,28 @@ public class IFFImageReader extends ImageReaderBase { } // Ensure band settings from param are compatible with images - checkReadParamBandSettings(pParam, mHeader.mBitplanes / 8, mImage.getSampleModel().getNumBands()); + checkReadParamBandSettings(pParam, header.bitplanes / 8, image.getSampleModel().getNumBands()); // NOTE: Each row of the image is stored in an integral number of 16 bit words. // The number of words per row is words=((w+15)/16) int planeWidth = 2 * ((width + 15) / 16); final byte[] planeData = new byte[8 * planeWidth]; - WritableRaster destination = mImage.getRaster(); + WritableRaster destination = image.getRaster(); if (destinationBands != null || offset.x != 0 || offset.y != 0) { destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); } -// WritableRaster raster = mImage.getRaster().createCompatibleWritableRaster(width, 1); - WritableRaster raster = mImage.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1); +// WritableRaster raster = image.getRaster().createCompatibleWritableRaster(width, 1); + WritableRaster raster = image.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1); Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands); final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - final int channels = (mHeader.mBitplanes + 7) / 8; + final int channels = (header.bitplanes + 7) / 8; final int planesPerChannel = 8; Object dataElements = null; for (int srcY = 0; srcY < height; srcY++) { for (int c = 0; c < channels; c++) { - for (int p = 0; p < planesPerChannel; p++) { readPlaneData(pInput, planeData, p * planeWidth, planeWidth); } @@ -559,7 +620,7 @@ public class IFFImageReader extends ImageReaderBase { return; } - if (mFormType == IFF.TYPE_ILBM) { + if (formType == IFF.TYPE_ILBM) { // NOTE: Using (channels - c - 1) instead of just c, // effectively reverses the channel order from RGBA to ABGR int off = (channels - c - 1); @@ -570,11 +631,11 @@ public class IFFImageReader extends ImageReaderBase { pixelPos += 8; } } - else if (mFormType == IFF.TYPE_PBM) { + else if (formType == IFF.TYPE_PBM) { System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth); } else { - throw new AssertionError(String.format("Unsupported FORM type: %s", mFormType)); + throw new AssertionError(String.format("Unsupported FORM type: %s", formType)); } } @@ -594,7 +655,7 @@ public class IFFImageReader extends ImageReaderBase { } } - processImageProgress(srcY * 100f / mHeader.mWidth); + processImageProgress(srcY * 100f / header.width); if (abortRequested()) { processReadAborted(); break; @@ -605,13 +666,15 @@ public class IFFImageReader extends ImageReaderBase { private void readPlaneData(final ImageInputStream pInput, final byte[] pData, final int pOffset, final int pPlaneWidth) throws IOException { - switch (mHeader.mCompressionType) { + switch (header.compressionType) { case BMHDChunk.COMPRESSION_NONE: pInput.readFully(pData, pOffset, pPlaneWidth); + // Uncompressed rows must have even number of bytes - if ((mHeader.mBitplanes * pPlaneWidth) % 2 != 0) { + if ((header.bitplanes * pPlaneWidth) % 2 != 0) { pInput.readByte(); } + break; case BMHDChunk.COMPRESSION_BYTE_RUN: @@ -620,46 +683,61 @@ public class IFFImageReader extends ImageReaderBase { // However, we don't know how long each compressed row is, without decoding it... // The workaround below, is to use a decode buffer size of pPlaneWidth, // to make sure we don't decode anything we don't have to (shouldn't). - if (mByteRunStream == null) { - mByteRunStream = new DataInputStream( + if (byteRunStream == null) { + byteRunStream = new DataInputStream( new DecoderStream( - IIOUtil.createStreamAdapter(pInput, mBody.mChunkLength), + IIOUtil.createStreamAdapter(pInput, body.chunkLength), new PackBitsDecoder(true), - pPlaneWidth * mHeader.mBitplanes + pPlaneWidth * header.bitplanes ) ); } - mByteRunStream.readFully(pData, pOffset, pPlaneWidth); + + byteRunStream.readFully(pData, pOffset, pPlaneWidth); break; default: - throw new IIOException(String.format("Unknown compression type: %d", mHeader.mCompressionType)); + throw new IIOException(String.format("Unknown compression type: %d", header.compressionType)); } } - private void hamToRGB(final byte[] pIndexed, final IndexColorModel pModel, - final byte[] pDest, final int pDestOffset) { - final int bits = mHeader.mBitplanes; - final int width = mHeader.mWidth; + private void multiPaletteToRGB(final int row, final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, final int destOffset) { + final int width = header.width; + + ColorModel palette = paletteChange.getColorModel(colorModel, row, isLaced()); + + for (int x = 0; x < width; x++) { + int pixel = indexed[x] & 0xff; + + int rgb = palette.getRGB(pixel); + + int offset = (x * 3) + destOffset; + dest[2 + offset] = (byte) ((rgb >> 16) & 0xff); + dest[1 + offset] = (byte) ((rgb >> 8) & 0xff); + dest[ offset] = (byte) ( rgb & 0xff); + } + } + + private void hamToRGB(final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, final int destOffset) { + final int bits = header.bitplanes; + final int width = header.width; int lastRed = 0; int lastGreen = 0; int lastBlue = 0; for (int x = 0; x < width; x++) { - int pixel = pIndexed[x] & 0xff; + int pixel = indexed[x] & 0xff; - //System.out.println("--> ham" + bits); int paletteIndex = bits == 6 ? pixel & 0x0f : pixel & 0x3f; int indexShift = bits == 6 ? 4 : 2; int colorMask = bits == 6 ? 0x0f : 0x03; - //System.out.println("palette index=" + paletteIndex); // Get Hold and Modify bits switch ((pixel >> (8 - indexShift)) & 0x03) { case 0x00:// HOLD - lastRed = pModel.getRed(paletteIndex); - lastGreen = pModel.getGreen(paletteIndex); - lastBlue = pModel.getBlue(paletteIndex); + lastRed = colorModel.getRed(paletteIndex); + lastGreen = colorModel.getGreen(paletteIndex); + lastBlue = colorModel.getBlue(paletteIndex); break; case 0x01:// MODIFY BLUE lastBlue = (lastBlue & colorMask) | (paletteIndex << indexShift); @@ -671,40 +749,79 @@ public class IFFImageReader extends ImageReaderBase { lastGreen = (lastGreen & colorMask) | (paletteIndex << indexShift); break; } - int offset = (x * 3) + pDestOffset; - pDest[2 + offset] = (byte) lastRed; - pDest[1 + offset] = (byte) lastGreen; - pDest[offset] = (byte) lastBlue; + + int offset = (x * 3) + destOffset; + dest[2 + offset] = (byte) lastRed; + dest[1 + offset] = (byte) lastGreen; + dest[ offset] = (byte) lastBlue; } } + private boolean isSHAM() { + // TODO: + return false; + } + + private boolean isPCHG() { + return paletteChange != null; + } + + private boolean isEHB() { + return viewPort != null && viewPort.isEHB(); + } + private boolean isHAM() { - return mViewPort != null && mViewPort.isHAM(); + return viewPort != null && viewPort.isHAM(); + } + + public boolean isLaced() { + return viewPort != null && viewPort.isLaced(); } public static void main(String[] pArgs) throws IOException { ImageReader reader = new IFFImageReader(); -// ImageInputStream input = ImageIO.createImageInputStream(new File(pArgs[0])); - ImageInputStream input = new BufferedImageInputStream(ImageIO.createImageInputStream(new File(pArgs[0]))); - boolean canRead = reader.getOriginatingProvider().canDecodeInput(input); + boolean scale = false; + for (String arg : pArgs) { + if (arg.startsWith("-")) { + scale = true; + continue; + } - System.out.println("Can read: " + canRead); + File file = new File(arg); + if (!file.isFile()) { + continue; + } - if (canRead) { - reader.setInput(input); - ImageReadParam param = reader.getDefaultReadParam(); -// param.setSourceRegion(new Rectangle(0, 0, 160, 200)); -// param.setSourceRegion(new Rectangle(160, 200, 160, 200)); -// param.setSourceRegion(new Rectangle(80, 100, 160, 200)); -// param.setDestinationOffset(new Point(80, 100)); -// param.setSourceSubsampling(3, 3, 0, 0); -// param.setSourceBands(new int[]{0, 1, 2}); -// param.setDestinationBands(new int[]{1, 0, 2}); - BufferedImage image = reader.read(0, param); - System.out.println("image = " + image); + try { + ImageInputStream input = new BufferedImageInputStream(ImageIO.createImageInputStream(file)); + boolean canRead = reader.getOriginatingProvider().canDecodeInput(input); - showIt(image, pArgs[0]); + if (canRead) { + reader.setInput(input); + ImageReadParam param = reader.getDefaultReadParam(); + // param.setSourceRegion(new Rectangle(0, 0, 160, 200)); + // param.setSourceRegion(new Rectangle(160, 200, 160, 200)); + // param.setSourceRegion(new Rectangle(80, 100, 160, 200)); + // param.setDestinationOffset(new Point(80, 100)); + // param.setSourceSubsampling(3, 3, 0, 0); + // param.setSourceBands(new int[]{0, 1, 2}); + // param.setDestinationBands(new int[]{1, 0, 2}); + BufferedImage image = reader.read(0, param); + System.out.println("image = " + image); + + if (scale) { + image = new ResampleOp(image.getWidth() / 2, image.getHeight(), ResampleOp.FILTER_LANCZOS).filter(image, null); + // image = ImageUtil.createResampled(image, image.getWidth(), image.getHeight() * 2, Image.SCALE_FAST); + } + + showIt(image, arg); + } + } + catch (IOException e) { + System.err.println("Error reading file: " + file); + e.printStackTrace(); + } } } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java index 62e9bb08..bd3350d0 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriter.java @@ -99,22 +99,24 @@ public class IFFImageWriter extends ImageWriterBase { } private void writeBody(ByteArrayOutputStream pImageData) throws IOException { - mImageOutput.writeInt(IFF.CHUNK_BODY); - mImageOutput.writeInt(pImageData.size()); + imageOutput.writeInt(IFF.CHUNK_BODY); + imageOutput.writeInt(pImageData.size()); - // NOTE: This is much faster than mOutput.write(pImageData.toByteArray()) + // NOTE: This is much faster than imageOutput.write(pImageData.toByteArray()) // as the data array is not duplicated - pImageData.writeTo(IIOUtil.createStreamAdapter(mImageOutput)); - - if (pImageData.size() % 2 == 0) { - mImageOutput.writeByte(0); // PAD + OutputStream adapter = IIOUtil.createStreamAdapter(imageOutput); + try { + pImageData.writeTo(adapter); + } + finally { + adapter.close(); } - // NOTE: Most progress is done in packImageData, however, as we need to - // buffer, to write correct size, we defer the last 10 percent until now. - processImageProgress(100f); + if (pImageData.size() % 2 == 0) { + imageOutput.writeByte(0); // PAD + } - mImageOutput.flush(); + imageOutput.flush(); } private void packImageData(OutputStream pOutput, RenderedImage pImage, ImageWriteParam pParam) throws IOException { @@ -167,7 +169,9 @@ public class IFFImageWriter extends ImageWriterBase { } } - processImageProgress(y * 90f / height); + output.flush(); + + processImageProgress(y * 100f / height); } output.flush(); @@ -208,21 +212,22 @@ public class IFFImageWriter extends ImageWriterBase { } // ILBM(4) + anno(8+len) + header(8+20) + cmap(8+len)? + body(8+len); - int size = 4 + 8 + anno.mChunkLength + 28 + 8 + pBodyLength; + int size = 4 + 8 + anno.chunkLength + 28 + 8 + pBodyLength; if (cmap != null) { - size += 8 + cmap.mChunkLength; + size += 8 + cmap.chunkLength; } - mImageOutput.writeInt(IFF.CHUNK_FORM); - mImageOutput.writeInt(size); + imageOutput.writeInt(IFF.CHUNK_FORM); + imageOutput.writeInt(size); - mImageOutput.writeInt(IFF.TYPE_ILBM); + imageOutput.writeInt(IFF.TYPE_ILBM); + + anno.writeChunk(imageOutput); + header.writeChunk(imageOutput); - anno.writeChunk(mImageOutput); - header.writeChunk(mImageOutput); if (cmap != null) { //System.out.println("CMAP written"); - cmap.writeChunk(mImageOutput); + cmap.writeChunk(imageOutput); } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java index da5c734b..4704236d 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterSpi.java @@ -51,7 +51,6 @@ public class IFFImageWriterSpi extends ImageWriterSpi { */ public IFFImageWriterSpi() { this(IIOUtil.getProviderInfo(IFFImageWriterSpi.class)); - } private IFFImageWriterSpi(final ProviderInfo pProviderInfo) { diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/Multipalette.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/Multipalette.java new file mode 100644 index 00000000..6130b25c --- /dev/null +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/Multipalette.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.iff; + +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; + +/** + * MultiPalette + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: MultiPalette.java,v 1.0 30.03.12 15:22 haraldk Exp$ + */ +interface MultiPalette { + ColorModel getColorModel(IndexColorModel colorModel, int rowIndex, boolean laced); +} diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/MutableIndexColorModel.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/MutableIndexColorModel.java new file mode 100644 index 00000000..4535b580 --- /dev/null +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/MutableIndexColorModel.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ +/* + * Parts of this code is based on ilbmtoppm.c + * + * Copyright (C) 1989 by Jef Poskanzer. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, provided + * that the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. This software is provided "as is" without express or + * implied warranty. + * + * Multipalette-support by Ingo Wilken (Ingo.Wilken@informatik.uni-oldenburg.de) + */ + +package com.twelvemonkeys.imageio.plugins.iff; + +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; + +/** + * A mutable indexed color model. + * For use with images that exploits Amiga hardware to change the color + * lookup table between scan lines. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: MutableIndexColorModel.java,v 1.0 29.03.12 15:00 haraldk Exp$ + */ +final class MutableIndexColorModel extends ColorModel { + final static int MP_REG_IGNORE = -1; + + final int[] rgbs; + + public MutableIndexColorModel(final IndexColorModel base) { + super(base.getPixelSize(), base.getComponentSize(), base.getColorSpace(), base.hasAlpha(), base.isAlphaPremultiplied(), base.getTransparency(), base.getTransferType()); + + this.rgbs = getRGBs(base); + } + + private static int[] getRGBs(final IndexColorModel colorModel) { + int[] rgbs = new int[colorModel.getMapSize()]; + colorModel.getRGBs(rgbs); + return rgbs; + } + + public void adjustColorMap(final PaletteChange[] changes) { + for (int i = 0; i < changes.length; i++) { + int index = changes[i].index; + + // TODO: Move validation to chunk (when reading) + if (index >= rgbs.length) { + // TODO: Issue IIO warning + System.err.printf("warning - palette change register out of range\n"); + System.err.printf(" change structure %d index=%d (max %d)\n", i, index, getMapSize() - 1); + System.err.printf(" ignoring it... colors might get messed up from here\n"); + } + else if (index != MP_REG_IGNORE) { + updateRGB(index, ((changes[i].r & 0xff) << 16) | ((changes[i].g & 0xff) << 8) | (changes[i].b & 0xff)); + } + } + } + + @Override + public int getRGB(int pixel) { + return rgbs[pixel]; + } + + @Override + public int getRed(int pixel) { + return (rgbs[pixel] >> 16) & 0xff; + } + + @Override + public int getGreen(int pixel) { + return (rgbs[pixel] >> 8) & 0xff; + } + + @Override + public int getBlue(int pixel) { + return rgbs[pixel] & 0xff; + } + + @Override + public int getAlpha(int pixel) { + return (rgbs[pixel] >> 24) & 0xff; + } + + private void updateRGB(int index, int rgb) { + rgbs[index] = rgb; + } + + public int getMapSize() { + return rgbs.length; + } + + static class PaletteChange { + /* palette index to change */ + public int index; + /* new colors for index */ + public byte r; + public byte g; + public byte b; + } +} diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/PCHGChunk.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/PCHGChunk.java new file mode 100644 index 00000000..499e5690 --- /dev/null +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/PCHGChunk.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ +/* + * Parts of this code is based on ilbmtoppm.c + * + * Copyright (C) 1989 by Jef Poskanzer. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, provided + * that the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. This software is provided "as is" without express or + * implied warranty. + * + * Multipalette-support by Ingo Wilken (Ingo.Wilken@informatik.uni-oldenburg.de) + */ +package com.twelvemonkeys.imageio.plugins.iff; + +import javax.imageio.IIOException; +import java.io.DataInput; +import java.io.IOException; + +/** + * PCHGChunk + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PCHGChunk.java,v 1.0 27.03.12 13:02 haraldk Exp$ + */ +final class PCHGChunk extends AbstractMultiPaletteChunk { + // NOTE: Values from ilbm2ppm. + final static int PCHG_COMP_NONE = 0; + final static int PCHG_COMP_HUFFMAN = 1; + + /** Use SmallLineChanges */ + final static int PCHGF_12BIT = 1; // NOTE: The beta spec refers to this as PHCGF_4BIT + /** Use BigLineChanges */ + final static int PCHGF_32BIT = 2; + /** meaningful only if PCHG_32BIT is on: use the Alpha channel info */ + final static int PCHGF_USE_ALPHA = 4; + + private int startLine; + private int changedLines; + private int lineCount; + private int totalChanges; + private int minReg; + + public PCHGChunk(int pChunkLength) { + super(IFF.CHUNK_PCHG, pChunkLength); + } + + @Override + void readChunk(final DataInput pInput) throws IOException { + int compression = pInput.readUnsignedShort(); + int flags = pInput.readUnsignedShort(); + startLine = pInput.readShort(); + lineCount = pInput.readUnsignedShort(); + changedLines = pInput.readUnsignedShort(); + minReg = pInput.readUnsignedShort(); + int maxReg = pInput.readUnsignedShort(); + /*int maxChangesPerLine = */pInput.readUnsignedShort(); // We don't really care, as we're not limited by the Amiga display hardware + totalChanges = pInput.readInt(); + + byte[] data; + + switch (compression) { + case PCHG_COMP_NONE: + data = new byte[chunkLength - 20]; + pInput.readFully(data); + + break; + case PCHG_COMP_HUFFMAN: + // NOTE: Huffman decompression is completely untested, due to lack of source data (read: Probably broken). + int compInfoSize = pInput.readInt(); + int originalDataSize = pInput.readInt(); + + short[] compTree = new short[compInfoSize / 2]; + for (int i = 0; i < compTree.length; i++) { + compTree[i] = pInput.readShort(); + } + + byte[] compData = new byte[chunkLength - 20 - 8 - compInfoSize]; + pInput.readFully(compData); + + data = new byte[originalDataSize]; + + // decompress the change structure data + decompressHuffman(compData, data, compTree, data.length); + + default: + throw new IIOException("Unknown PCHG compression: " + compression); + } + + changes = new MutableIndexColorModel.PaletteChange[startLine + lineCount][]; + + if (startLine < 0) { + int numChanges = maxReg - minReg + 1; + + initialChanges = new MutableIndexColorModel.PaletteChange[numChanges]; + } + + // TODO: Postpone conversion to when the data is actually needed + parseChanges(data, flags); + } + + static void decompressHuffman(byte[] src, byte[] dest, short[] tree, int origSize) { + int i = 0; + int bits = 0; + int thisbyte = 0; + + int treeIdx = tree.length - 1; + int srcIdx = 0; + int destIdx = 0; + + while (i < origSize) { + if (bits == 0) { + thisbyte = src[srcIdx++]; + bits = 8; + } + + if ((thisbyte & (1 << 7)) != 0) { + if (tree[treeIdx] >= 0) { + dest[destIdx++] = (byte) tree[treeIdx]; + i++; + treeIdx = tree.length - 1; + } + else { + treeIdx += tree[treeIdx] / 2; + } + } + else { + treeIdx--; + + if (tree[treeIdx] > 0 && (tree[treeIdx] & 0x100) != 0) { + dest[destIdx++] = (byte) tree[treeIdx]; + i++; + treeIdx = tree.length - 1; + } + } + + thisbyte <<= 1; + bits--; + } + } + + private void parseChanges(final byte[] data, int flags) throws IIOException { + boolean small; + + if ((flags & PCHGF_12BIT) != 0) { + small = true; + } + else if ((flags & PCHGF_32BIT) != 0) { + if ((flags & PCHGF_USE_ALPHA) != 0) { + // TODO: Warning, or actually implement + new IIOException("Alpha currently not supported.").printStackTrace(); + } + + small = false; + } + else { + throw new IIOException("Missing PCHG 12/32 bit flag."); + } + + int thismask = 0; + int changeCount; + int totalchanges = 0; + int changedlines = changedLines; + + int maskBytesLeft = 4 * ((lineCount + 31) / 32); + + int maskIdx = 0; + int dataIdx = maskBytesLeft; + int dataBytesLeft = data.length - maskBytesLeft; + + int bits = 0; + for (int row = startLine; changedlines != 0 && row < 0; row++) { + if (bits == 0) { + if (maskBytesLeft == 0) { + throw new IIOException("Insufficient data in line mask"); + } + + thismask = data[maskIdx++]; + --maskBytesLeft; + bits = 8; + } + + if ((thismask & (1 << 7)) != 0) { + if (dataBytesLeft < 2) { + throw new IIOException("Insufficient data in SmallLineChanges structures: " + dataBytesLeft); + } + + int changeCount16 = 0; + if (small) { + changeCount16 = data[dataIdx++] & 0xff; + changeCount = changeCount16 + (data[dataIdx++] & 0xff); + } + else { + changeCount = toShort(data, dataIdx); + dataIdx += 2; + } + dataBytesLeft -= 2; + + for (int i = 0; i < changeCount; i++) { + if (totalchanges >= this.totalChanges) { + throw new IIOException("Insufficient data in SmallLineChanges structures (changeCount): " + totalchanges); + } + if (dataBytesLeft < 2) { + throw new IIOException("Insufficient data in SmallLineChanges structures: " + dataBytesLeft); + } + + // TODO: Make PaletteChange immutable with constructor params, assign outside test? + if (small) { + int smallChange = toShort(data, dataIdx); + dataIdx += 2; + dataBytesLeft -= 2; + int reg = ((smallChange & 0xf000) >> 12) + (i >= changeCount16 ? 16 : 0); + initialChanges[reg - minReg] = new MutableIndexColorModel.PaletteChange(); + initialChanges[reg - minReg].index = reg; + initialChanges[reg - minReg].r = (byte) (((smallChange & 0x0f00) >> 8) * FACTOR_4BIT); + initialChanges[reg - minReg].g = (byte) (((smallChange & 0x00f0) >> 4) * FACTOR_4BIT); + initialChanges[reg - minReg].b = (byte) (((smallChange & 0x000f) ) * FACTOR_4BIT); + } + else { + int reg = toShort(data, dataIdx); + dataIdx += 2; + initialChanges[reg - minReg] = new MutableIndexColorModel.PaletteChange(); + initialChanges[reg - minReg].index = reg; + dataIdx++; /* skip alpha */ + initialChanges[reg - minReg].r = data[dataIdx++]; + initialChanges[reg - minReg].b = data[dataIdx++]; /* yes, RBG */ + initialChanges[reg - minReg].g = data[dataIdx++]; + dataBytesLeft -= 6; + + } + + ++totalchanges; + } + + --changedlines; + } + + thismask <<= 1; + bits--; + } + + for (int row = startLine; changedlines != 0 && row < changes.length; row++) { + if (bits == 0) { + if (maskBytesLeft == 0) { + throw new IIOException("Insufficient data in line mask"); + } + + thismask = data[maskIdx++]; + --maskBytesLeft; + bits = 8; + } + + if ((thismask & (1 << 7)) != 0) { + if (dataBytesLeft < 2) { + throw new IIOException("Insufficient data in SmallLineChanges structures: " + dataBytesLeft); + } + + int changeCount16 = 0; + if (small) { + changeCount16 = data[dataIdx++] & 0xff; + changeCount = changeCount16 + (data[dataIdx++] & 0xff); + } + else { + changeCount = toShort(data, dataIdx); + dataIdx += 2; + } + dataBytesLeft -= 2; + + changes[row] = new MutableIndexColorModel.PaletteChange[changeCount]; + + for (int i = 0; i < changeCount; i++) { + if (totalchanges >= this.totalChanges) { + throw new IIOException("Insufficient data in SmallLineChanges structures (changeCount): " + totalchanges); + } + + if (dataBytesLeft < 2) { + throw new IIOException("Insufficient data in SmallLineChanges structures: " + dataBytesLeft); + } + + if (small) { + int smallChange = toShort(data, dataIdx); + dataIdx += 2; + dataBytesLeft -= 2; + int reg = ((smallChange & 0xf000) >> 12) + (i >= changeCount16 ? 16 : 0); + + MutableIndexColorModel.PaletteChange paletteChange = new MutableIndexColorModel.PaletteChange(); + paletteChange.index = reg; + paletteChange.r = (byte) (((smallChange & 0x0f00) >> 8) * FACTOR_4BIT); + paletteChange.g = (byte) (((smallChange & 0x00f0) >> 4) * FACTOR_4BIT); + paletteChange.b = (byte) (((smallChange & 0x000f) ) * FACTOR_4BIT); + + changes[row][i] = paletteChange; + } + else { + int reg = toShort(data, dataIdx); + dataIdx += 2; + + MutableIndexColorModel.PaletteChange paletteChange = new MutableIndexColorModel.PaletteChange(); + paletteChange.index = reg; + dataIdx++; /* skip alpha */ + paletteChange.r = data[dataIdx++]; + paletteChange.b = data[dataIdx++]; /* yes, RBG */ + paletteChange.g = data[dataIdx++]; + changes[row][i] = paletteChange; + + dataBytesLeft -= 6; + } + + ++totalchanges; + } + + --changedlines; + } + + thismask <<= 1; + bits--; + } + + if (totalchanges != this.totalChanges) { + // TODO: Issue IIO warning + new IIOException(String.format("Got %d change structures, chunk header reports %d", totalchanges, this.totalChanges)).printStackTrace(); + } + } + + // TODO: Util method + private static short toShort(byte[] bytes, int idx) { + return (short) ((bytes[idx] & 0xff) << 8 | (bytes[idx + 1] & 0xff)); + } +} diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/LZWEncoder.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/SHAMChunk.java similarity index 75% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/LZWEncoder.java rename to imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/SHAMChunk.java index 31b917fa..74b8805a 100644 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/LZWEncoder.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/SHAMChunk.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, Harald Kuhr + * Copyright (c) 2012, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,21 +26,22 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.io.enc; - -import java.io.OutputStream; -import java.io.IOException; +package com.twelvemonkeys.imageio.plugins.iff; /** - * LZWEncoder. - *

    + * SHAMChunk * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWEncoder.java#2 $ + * @author last modified by $Author: haraldk$ + * @version $Id: SHAMChunk.java,v 1.0 30.03.12 14:53 haraldk Exp$ */ -final class LZWEncoder implements Encoder { - public void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { - // TODO: Implement - // TODO: We probably need a GIF specific subclass +final class SHAMChunk extends AbstractMultiPaletteChunk { + protected SHAMChunk(int pChunkLength) { + super(IFF.CHUNK_SHAM, pChunkLength); } -} \ No newline at end of file + + @Override + protected boolean skipLaced() { + return true; + } +} diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java new file mode 100755 index 00000000..11d88b74 --- /dev/null +++ b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.iff; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * IFFImageReaderTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IFFImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ + */ +public class IFFImageReaderTest extends ImageReaderAbstractTestCase { + protected List getTestData() { + return Arrays.asList( + // 32 bit - Ok + new TestData(getClassLoaderResource("/iff/test.iff"), new Dimension(300, 200)), + // 24 bit - Ok + new TestData(getClassLoaderResource("/iff/survivor.iff"), new Dimension(800, 600)), + // HAM6 - Ok (a lot of visual "fringe", would be interesting to see on a real HAM display) + new TestData(getClassLoaderResource("/iff/A4000T_HAM6.IFF"), new Dimension(320, 512)), + // HAM8 - Ok + new TestData(getClassLoaderResource("/iff/A4000T_HAM8.IFF"), new Dimension(628, 512)), + // 2 color indexed - Ok + new TestData(getClassLoaderResource("/iff/owl.iff"), new Dimension(160, 174)), + // 8 color indexed - Ok + new TestData(getClassLoaderResource("/iff/AmigaAmiga.iff"), new Dimension(200, 150)), + // 16 color indexed - Ok + new TestData(getClassLoaderResource("/iff/Lion.iff"), new Dimension(704, 480)), + // 32 color indexed - Ok + new TestData(getClassLoaderResource("/iff/GoldPorsche.iff"), new Dimension(320, 200)), + // 64 color indexed EHB - Ok + new TestData(getClassLoaderResource("/iff/Bryce.iff"), new Dimension(320, 200)), + // 256 color indexed - Ok + new TestData(getClassLoaderResource("/iff/IKKEGOD.iff"), new Dimension(640, 256)), + // PBM, indexed - Ok + new TestData(getClassLoaderResource("/iff/ASH.PBM"), new Dimension(320, 240)), + // 16 color indexed, multi palette (PCHG) - Ok + new TestData(getClassLoaderResource("/iff/Manhattan.PCHG"), new Dimension(704, 440)), + // 16 color indexed, multi palette (PCHG + SHAM) - Ok + new TestData(getClassLoaderResource("/iff/Somnambulist-2.SHAM"), new Dimension(704, 440)) + ); + } + + protected ImageReaderSpi createProvider() { + return new IFFImageReaderSpi(); + } + + protected Class getReaderClass() { + return IFFImageReader.class; + } + + protected List getFormatNames() { + return Arrays.asList("iff"); + } + + protected List getSuffixes() { + return Arrays.asList("iff", "ilbm", "ham", "ham8", "lbm"); + } + + protected List getMIMETypes() { + return Arrays.asList("image/iff", "image/x-iff"); + } + + // Regression tests + + @Test + public void testEHBColors() throws IOException { + IFFImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/iff/Bryce.iff"))); + + BufferedImage image = reader.read(0); + assertEquals(BufferedImage.TYPE_BYTE_INDEXED, image.getType()); + + ColorModel colorModel = image.getColorModel(); + assertNotNull(colorModel); + assertTrue(colorModel instanceof IndexColorModel); + + IndexColorModel indexColorModel = (IndexColorModel) colorModel; + + assertEquals(64, indexColorModel.getMapSize()); + + byte[] reds = new byte[indexColorModel.getMapSize()]; + indexColorModel.getReds(reds); + byte[] blues = new byte[indexColorModel.getMapSize()]; + indexColorModel.getBlues(blues); + byte[] greens = new byte[indexColorModel.getMapSize()]; + indexColorModel.getGreens(greens); + + for (int i = 0; i < 32; i++) { + // Make sure the color model is really EHB + assertEquals("red", (reds[i] & 0xff) / 2, reds[i + 32] & 0xff); + assertEquals("blue", (blues[i] & 0xff) / 2, blues[i + 32] & 0xff); + assertEquals("green", (greens[i] & 0xff) / 2, greens[i + 32] & 0xff); + } + } +} diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterTest.java b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterTest.java new file mode 100644 index 00000000..1cff8e2e --- /dev/null +++ b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageWriterTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.iff; + +import com.twelvemonkeys.image.MonochromeColorModel; +import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * JPEG2000ImageWriterTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEG2000ImageWriterTest.java,v 1.0 20.01.12 12:19 haraldk Exp$ + */ +public class IFFImageWriterTest extends ImageWriterAbstractTestCase { + private final IFFImageWriterSpi provider = new IFFImageWriterSpi(); + + @Override + protected ImageWriter createImageWriter() { + return new IFFImageWriter(provider); + } + + @Override + protected List getTestData() { + return Arrays.asList( + new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(33, 20, BufferedImage.TYPE_BYTE_GRAY), + new BufferedImage(31, 23, BufferedImage.TYPE_BYTE_INDEXED), + new BufferedImage(30, 27, BufferedImage.TYPE_BYTE_BINARY), + new BufferedImage(29, 29, BufferedImage.TYPE_BYTE_INDEXED, MonochromeColorModel.getInstance()), + new BufferedImage(28, 31, BufferedImage.TYPE_INT_BGR), + new BufferedImage(27, 33, BufferedImage.TYPE_INT_RGB), + new BufferedImage(24, 37, BufferedImage.TYPE_3BYTE_BGR), + new BufferedImage(23, 41, BufferedImage.TYPE_4BYTE_ABGR) + ); + } + + @Test + public void testWriteReadCompare() throws IOException { + ImageWriter writer = createImageWriter(); + + List testData = getTestData(); + + for (int i = 0; i < testData.size(); i++) { + try { + RenderedImage image = testData.get(i); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); + writer.setOutput(stream); + + BufferedImage original = drawSomething((BufferedImage) image); + + try { + writer.write(original); + } + catch (IOException e) { + fail(e.getMessage()); + } + finally { + stream.close(); // Force data to be written + } + + assertTrue("No image data written", buffer.size() > 0); + + ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray())); + BufferedImage written = ImageIO.read(input); + + assertNotNull(written); + assertEquals(original.getWidth(), written.getWidth()); + assertEquals(original.getHeight(), written.getHeight()); + assertSameType(original, written); + assertSameData(original, written); + } + catch (IOException e) { + AssertionError fail = new AssertionError("Failure writing test data " + i + " " + e); + fail.initCause(e); + throw fail; + } + } + } + + private static void assertSameData(BufferedImage expected, BufferedImage actual) { + for (int y = 0; y < expected.getHeight(); y++) { + for (int x = 0; x < expected.getWidth(); x++) { + int expectedRGB = expected.getRGB(x, y); + int actualRGB = actual.getRGB(x, y); + + if (expected.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) { + // NOTE: For some reason, gray data seems to be one step off... + assertEquals("R(" + x + "," + y + ")", expectedRGB & 0xff0000, actualRGB & 0xff0000, 0x10000); + assertEquals("G(" + x + "," + y + ")", expectedRGB & 0x00ff00, actualRGB & 0x00ff00, 0x100); + assertEquals("B(" + x + "," + y + ")", expectedRGB & 0x0000ff, actualRGB & 0x0000ff, 0x1); + } + else { + assertEquals("R(" + x + "," + y + ")", expectedRGB & 0xff0000, actualRGB & 0xff0000); + assertEquals("G(" + x + "," + y + ")", expectedRGB & 0x00ff00, actualRGB & 0x00ff00); + assertEquals("B(" + x + "," + y + ")", expectedRGB & 0x0000ff, actualRGB & 0x0000ff); + } + } + } + } + + private static void assertSameType(BufferedImage expected, BufferedImage actual) { + if (expected.getType() != actual.getType()) { + if (expected.getType() == BufferedImage.TYPE_INT_RGB || expected.getType() == BufferedImage.TYPE_INT_BGR) { + assertEquals(BufferedImage.TYPE_3BYTE_BGR, actual.getType()); + } + else if (expected.getType() == BufferedImage.TYPE_INT_ARGB || expected.getType() == BufferedImage.TYPE_INT_ARGB_PRE) { + assertEquals(BufferedImage.TYPE_4BYTE_ABGR, actual.getType()); + } + else if (expected.getType() == BufferedImage.TYPE_BYTE_INDEXED && expected.getColorModel().getPixelSize() <= 16) { + assertEquals(BufferedImage.TYPE_BYTE_BINARY, actual.getType()); + } + + // NOTE: Actually, TYPE_GRAY may be converted to TYPE_BYTE_INDEXED with linear gray color-map, + // without being a problem (just a waste of time and space). + } + } +} diff --git a/imageio/imageio-iff/src/test/resources/iff/ASH.PBM b/imageio/imageio-iff/src/test/resources/iff/ASH.PBM new file mode 100644 index 00000000..d52f0ebb Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/ASH.PBM differ diff --git a/imageio/imageio-iff/src/test/resources/iff/Abyss.iff b/imageio/imageio-iff/src/test/resources/iff/Abyss.iff deleted file mode 100755 index 4540de22..00000000 Binary files a/imageio/imageio-iff/src/test/resources/iff/Abyss.iff and /dev/null differ diff --git a/imageio/imageio-iff/src/test/resources/iff/AmigaBig.iff b/imageio/imageio-iff/src/test/resources/iff/AmigaBig.iff deleted file mode 100755 index 0ee086e5..00000000 Binary files a/imageio/imageio-iff/src/test/resources/iff/AmigaBig.iff and /dev/null differ diff --git a/imageio/imageio-iff/src/test/resources/iff/Bryce.iff b/imageio/imageio-iff/src/test/resources/iff/Bryce.iff new file mode 100644 index 00000000..0be90771 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/Bryce.iff differ diff --git a/imageio/imageio-iff/src/test/resources/iff/GoldPorsche.iff b/imageio/imageio-iff/src/test/resources/iff/GoldPorsche.iff new file mode 100644 index 00000000..01c77b18 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/GoldPorsche.iff differ diff --git a/imageio/imageio-iff/src/test/resources/iff/IKKEGOD.iff b/imageio/imageio-iff/src/test/resources/iff/IKKEGOD.iff new file mode 100644 index 00000000..fa4c2e46 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/IKKEGOD.iff differ diff --git a/imageio/imageio-iff/src/test/resources/iff/Lion.iff b/imageio/imageio-iff/src/test/resources/iff/Lion.iff new file mode 100644 index 00000000..2a0bfef2 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/Lion.iff differ diff --git a/imageio/imageio-iff/src/test/resources/iff/Manhattan.PCHG b/imageio/imageio-iff/src/test/resources/iff/Manhattan.PCHG new file mode 100644 index 00000000..f5c03150 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/Manhattan.PCHG differ diff --git a/imageio/imageio-iff/src/test/resources/iff/Somnambulist-2.SHAM b/imageio/imageio-iff/src/test/resources/iff/Somnambulist-2.SHAM new file mode 100644 index 00000000..1779180f Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/Somnambulist-2.SHAM differ diff --git a/imageio/imageio-iff/src/test/resources/iff/fair-use.txt b/imageio/imageio-iff/src/test/resources/iff/fair-use.txt new file mode 100644 index 00000000..194b8df5 --- /dev/null +++ b/imageio/imageio-iff/src/test/resources/iff/fair-use.txt @@ -0,0 +1,5 @@ +The image files in this folder may contain copyrighted artwork. However, I believe that using them for test purposes +(without actually displaying the artwork) must be considered fair use. +If you disagree for any reason, please send me a note, and I will remove your image from the distribution. + + -- harald.kuhr@gmail.com diff --git a/imageio/imageio-iff/src/test/resources/iff/owl.iff b/imageio/imageio-iff/src/test/resources/iff/owl.iff new file mode 100644 index 00000000..65b12e02 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/owl.iff differ diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java index 20c96a58..7d51b6c1 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickReader.java @@ -286,11 +286,11 @@ abstract class JMagickReader extends ImageReaderBase { // TODO: If ImageInputStream is already file-backed, maybe we can peek into that file? // At the moment, the cache/file is not accessible, but we could create our own // FileImageInputStream provider that gives us this access. - if (!mUseTempFile && mImageInput.length() >= 0 && mImageInput.length() <= Integer.MAX_VALUE) { + if (!mUseTempFile && imageInput.length() >= 0 && imageInput.length() <= Integer.MAX_VALUE) { // This works for most file formats, as long as ImageMagick // uses the file magic to decide file format - byte[] bytes = new byte[(int) mImageInput.length()]; - mImageInput.readFully(bytes); + byte[] bytes = new byte[(int) imageInput.length()]; + imageInput.readFully(bytes); // Unfortunately, this is a waste of space & time... ImageInfo info = new ImageInfo(); @@ -309,7 +309,7 @@ abstract class JMagickReader extends ImageReaderBase { byte[] buffer = new byte[FileUtil.BUF_SIZE]; int count; - while ((count = mImageInput.read(buffer)) != -1) { + while ((count = imageInput.read(buffer)) != -1) { out.write(buffer, 0, count); } diff --git a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickWriter.java b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickWriter.java index 8106bbee..9a7dfc4b 100755 --- a/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickWriter.java +++ b/imageio/imageio-jmagick/src/main/java/com/twelvemonkeys/imageio/plugins/jmagick/JMagickWriter.java @@ -113,8 +113,8 @@ abstract class JMagickWriter extends ImageWriterBase { processImageProgress(67); // Write blob to output - mImageOutput.write(bytes); - mImageOutput.flush(); + imageOutput.write(bytes); + imageOutput.flush(); processImageProgress(100); } catch (MagickException e) { diff --git a/imageio/imageio-jmagick/src/test/java/com/twelvemonkeys/imageio/plugins/jmagick/BMPImageReaderTestCase.java b/imageio/imageio-jmagick/src/test/java/com/twelvemonkeys/imageio/plugins/jmagick/BMPImageReaderTestCase.java index 50994a44..c38268ec 100755 --- a/imageio/imageio-jmagick/src/test/java/com/twelvemonkeys/imageio/plugins/jmagick/BMPImageReaderTestCase.java +++ b/imageio/imageio-jmagick/src/test/java/com/twelvemonkeys/imageio/plugins/jmagick/BMPImageReaderTestCase.java @@ -34,7 +34,7 @@ import java.util.Arrays; import java.util.List; public class BMPImageReaderTestCase extends JMagickImageReaderAbstractTestCase { - private BMPImageReaderSpi mProvider = new BMPImageReaderSpi(); + private BMPImageReaderSpi provider = new BMPImageReaderSpi(); protected List getTestData() { return Arrays.asList( @@ -63,7 +63,7 @@ public class BMPImageReaderTestCase extends JMagickImageReaderAbstractTestCase extends ImageReaderAbstractTestCase { - - @Override - protected void runTest() throws Throwable { - if (JMagickImageReaderSpiSupport.AVAILABLE) { - super.runTest(); + @Rule + public MethodRule rule = new MethodRule() { + public Statement apply(final Statement base, final FrameworkMethod method, final Object target) { + if (JMagickImageReaderSpiSupport.AVAILABLE) { + return base; + } + + return new Statement() { + @Override + public void evaluate() throws Throwable { + // TODO: Make this increase "skipped" count, rather than run/success + System.err.println("WARNING: JMagick not installed. Skipping test " + method.getName()); + } + }; } - else { - System.err.println("WARNING: JMagick not installed. Skipping test " + getName()); - } - } + }; } diff --git a/imageio/imageio-jmagick/todo.txt b/imageio/imageio-jmagick/todo.txt index 1d35e265..bd5338cc 100755 --- a/imageio/imageio-jmagick/todo.txt +++ b/imageio/imageio-jmagick/todo.txt @@ -1,5 +1,5 @@ - These readers/writers could probably benefit alot from using JNI with nio bytechannel buffers... RFE to JMagick guys? -- Create stream providers and allow creatig a ImageInputStream from a +- Create stream providers and allow creating an ImageInputStream from a MagickImage? Does that even make sense? Or maybe create File based streams that exposes the file name for JMagick to write directly. \ No newline at end of file diff --git a/imageio/imageio-jpeg/pom.xml b/imageio/imageio-jpeg/pom.xml new file mode 100644 index 00000000..7e6a16e3 --- /dev/null +++ b/imageio/imageio-jpeg/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.0-SNAPSHOT + + imageio-jpeg + TwelveMonkeys :: ImageIO :: JPEG plugin + + ImageIO plugin for Joint Photographer Expert Group images (JPEG/JFIF). + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + tests + + + com.twelvemonkeys.imageio + imageio-metadata + + + diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java new file mode 100644 index 00000000..c95a8355 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +/** + * AdobeDCT + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$ + */ +class AdobeDCT { + public static final int Unknown = 0; + public static final int YCC = 1; + public static final int YCCK = 2; + + final int version; + final int flags0; + final int flags1; + final int transform; + + public AdobeDCT(int version, int flags0, int flags1, int transform) { + this.version = version; // 100 or 101 + this.flags0 = flags0; + this.flags1 = flags1; + this.transform = transform; + } + + public int getVersion() { + return version; + } + + public int getFlags0() { + return flags0; + } + + public int getFlags1() { + return flags1; + } + + public int getTransform() { + return transform; + } + + @Override + public String toString() { + return String.format( + "AdobeDCT[ver: %d.%02d, flags: %s %s, transform: %d]", + getVersion() / 100, getVersion() % 100, Integer.toBinaryString(getFlags0()), Integer.toBinaryString(getFlags1()), getTransform() + ); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java new file mode 100644 index 00000000..d18dbdf6 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.util.IIOUtil; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.lang.ref.SoftReference; +import java.util.Arrays; + +/** + * EXIFThumbnail + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$ + */ +final class EXIFThumbnailReader extends ThumbnailReader { + private final Directory ifd; + private final ImageInputStream stream; + private final int compression; + + private transient SoftReference cachedThumbnail; + + public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) { + super(progressListener, imageIndex, thumbnailIndex); + this.ifd = ifd; + this.stream = stream; + + Entry compression = ifd.getEntryById(TIFF.TAG_COMPRESSION); + + this.compression = compression != null ? ((Number) compression.getValue()).intValue() : 6; + } + + @Override + public BufferedImage read() throws IOException { + if (compression == 1) { // 1 = no compression + processThumbnailStarted(); + BufferedImage thumbnail = readUncompressed(); + processThumbnailProgress(100f); + processThumbnailComplete(); + + return thumbnail; + } + else if (compression == 6) { // 6 = JPEG compression + processThumbnailStarted(); + BufferedImage thumbnail = readJPEGCached(true); + processThumbnailProgress(100f); + processThumbnailComplete(); + + return thumbnail; + } + else { + throw new IIOException("Unsupported EXIF thumbnail compression: " + compression); + } + } + + private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException { + BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null; + + if (thumbnail == null) { + thumbnail = readJPEG(); + } + + cachedThumbnail = pixelsExposed ? null : new SoftReference(thumbnail); + + return thumbnail; + } + + private BufferedImage readJPEG() throws IOException { + // IFD1 should contain JPEG offset for JPEG thumbnail + Entry jpegOffset = ifd.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT); + + if (jpegOffset != null) { + stream.seek(((Number) jpegOffset.getValue()).longValue()); + InputStream input = IIOUtil.createStreamAdapter(stream); + + // For certain EXIF files (encoded with TIFF.TAG_YCBCR_POSITIONING = 2?), we need + // EXIF information to read the thumbnail correctly (otherwise the colors are messed up). + // Probably related to: http://bugs.sun.com/view_bug.do?bug_id=4881314 + + // HACK: Splice empty EXIF information into the thumbnail stream + byte[] fakeEmptyExif = { + // SOI (from original data) + (byte) input.read(), (byte) input.read(), + // APP1 + len (016) + 'Exif' + 0-term + pad + (byte) 0xFF, (byte) 0xE1, 0, 16, 'E', 'x', 'i', 'f', 0, 0, + // Big-endian BOM (MM), TIFF magic (042), offset (0000) + 'M', 'M', 0, 42, 0, 0, 0, 0, + }; + + input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input); + try { + return readJPEGThumbnail(input); + } + finally { + input.close(); + } + } + + throw new IIOException("Missing JPEGInterchangeFormat tag for JPEG compressed EXIF thumbnail"); + } + + private BufferedImage readUncompressed() throws IOException { + // Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always) + // PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always), + Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH); + Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT); + + if (width == null || height == null) { + throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail"); + } + + Entry bitsPerSample = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE); + Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS); + Entry photometricInterpretation = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION); + + // Required + int w = ((Number) width.getValue()).intValue(); + int h = ((Number) height.getValue()).intValue(); + + if (bitsPerSample != null) { + int[] bpp = (int[]) bitsPerSample.getValue(); + if (!Arrays.equals(bpp, new int[] {8, 8, 8})) { + throw new IIOException("Unknown BitsPerSample value for uncompressed EXIF thumbnail (expected [8, 8, 8]): " + bitsPerSample.getValueAsString()); + } + } + + if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) { + throw new IIOException("Unknown SamplesPerPixel value for uncompressed EXIF thumbnail (expected 3): " + samplesPerPixel.getValueAsString()); + } + + int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2; + + // IFD1 should contain strip offsets for uncompressed images + Entry offset = ifd.getEntryById(TIFF.TAG_STRIP_OFFSETS); + if (offset != null) { + stream.seek(((Number) offset.getValue()).longValue()); + + // Read raw image data, either RGB or YCbCr + int thumbSize = w * h * 3; + byte[] thumbData = JPEGImageReader.readFully(stream, thumbSize); + + switch (interpretation) { + case 2: + // RGB + break; + case 6: + // YCbCr + for (int i = 0, thumbDataLength = thumbData.length; i < thumbDataLength; i += 3) { + JPEGImageReader.YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i); + } + break; + default: + throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation); + } + + return ThumbnailReader.readRawThumbnail(thumbData, thumbData.length, 0, w, h); + } + + throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail"); + } + + @Override + public int getWidth() throws IOException { + if (compression == 1) { // 1 = no compression + Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH); + + if (width == null) { + throw new IIOException("Missing dimensions for unknown EXIF thumbnail"); + } + + return ((Number) width.getValue()).intValue(); + } + else if (compression == 6) { // 6 = JPEG compression + return readJPEGCached(false).getWidth(); + } + else { + throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression); + } + } + + @Override + public int getHeight() throws IOException { + if (compression == 1) { // 1 = no compression + Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT); + + if (height == null) { + throw new IIOException("Missing dimensions for unknown EXIF thumbnail"); + } + + return ((Number) height.getValue()).intValue(); + } + else if (compression == 6) { // 6 = JPEG compression + return readJPEGCached(false).getHeight(); + } + else { + throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression); + } + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java new file mode 100644 index 00000000..5897a7aa --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.lang.Validate; + +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; + +/** + * This class performs a pixel by pixel conversion of the source image, from CMYK to RGB. + *

    + * The conversion is fast, but performed without any color space conversion. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FastCMYKToRGB.java,v 1.0 21.02.11 13.22 haraldk Exp$ + * @see ColorConvertOp + */ +class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp { + // TODO: Force dest alpha to match source alpha? + + public FastCMYKToRGB() { + } + + /** + * Converts the CMYK source raster to the destination RGB raster. + * + * @param src assumed to be 4 byte CMYK + * @param dest raster, in either 3 byte BGR/BGR, 4 byte ABGR or int RGB/ARGB format, or {@code null} + * @return {@code dest}, or a new {@link WritableRaster} if {@code dest} is {@code null}. + * @throws IllegalArgumentException if {@code src} and {@code dest} refer to the same object + */ + public WritableRaster filter(Raster src, WritableRaster dest) { + Validate.notNull(src, "src may not be null"); + // TODO: Why not allow same raster, if converting to 4 byte ABGR? + Validate.isTrue(src != dest, "src and dest raster may not be same"); + Validate.isTrue(src.getTransferType() == DataBuffer.TYPE_BYTE, src, "only TYPE_BYTE rasters supported as src: %s"); + Validate.isTrue(src.getNumDataElements() >= 4, src.getNumDataElements(), "CMYK raster must have at least 4 data elements: %s"); + + if (dest == null) { + dest = createCompatibleDestRaster(src); + } + else { + Validate.isTrue( + dest.getTransferType() == DataBuffer.TYPE_BYTE && dest.getNumDataElements() >= 3 || + dest.getTransferType() == DataBuffer.TYPE_INT && dest.getNumDataElements() == 1, + src, + "only 3 or 4 byte TYPE_BYTE or 1 int TYPE_INT rasters supported as dest: %s" + ); + } + + final int height = src.getHeight(); + final int width = src.getWidth(); + + final byte[] in = new byte[src.getNumDataElements()]; // CMYK + + if (dest.getTransferType() == DataBuffer.TYPE_BYTE) { + final byte[] out = new byte[dest.getNumDataElements()]; + + if (out.length > 3) { + out[3] = (byte) 0xFF; + } + + for (int y = dest.getMinY(); y < height; y++) { + for (int x = dest.getMinX(); x < width; x++) { + src.getDataElements(x, y, in); + convertCMYKToRGB(in, out); + dest.setDataElements(x, y, out); + } + } + } + else if (dest.getTransferType() == DataBuffer.TYPE_INT) { + final int[] out = new int[dest.getNumDataElements()]; + final byte[] temp = new byte[3]; // RGB + + // Special case for INT_BGR types, as bit offsets are not handled in setDataElements like for the byte raster case + int[] bitOffsets; + SampleModel sm = dest.getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + bitOffsets = ((SinglePixelPackedSampleModel) sm).getBitOffsets(); + } + else { + bitOffsets = new int[]{0, 8, 16}; + } + + final int alpha = bitOffsets.length > 3 ? 0xFF : 0x00; + + for (int y = dest.getMinY(); y < height; y++) { + for (int x = dest.getMinX(); x < width; x++) { + src.getDataElements(x, y, in); + convertCMYKToRGB(in, temp); + out[0] = alpha << 24 | (temp[0] & 0xFF) << bitOffsets[0] | (temp[1] & 0xFF) << bitOffsets[1] | (temp[2] & 0xFF) << bitOffsets[2]; + dest.setDataElements(x, y, out); + } + } + } + else { + // This is already tested for + throw new AssertionError(); + } + + return dest; + } + + @SuppressWarnings({"PointlessArithmeticExpression"}) + private void convertCMYKToRGB(byte[] cmyk, byte[] rgb) { + rgb[0] = (byte) (((255 - cmyk[0] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255); + rgb[1] = (byte) (((255 - cmyk[1] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255); + rgb[2] = (byte) (((255 - cmyk[2] & 0xFF) * (255 - cmyk[3] & 0xFF)) / 255); + } + + public Rectangle2D getBounds2D(Raster src) { + return src.getBounds(); + } + + public WritableRaster createCompatibleDestRaster(final Raster src) { + return src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[]{0, 1, 2}).createCompatibleWritableRaster(); + } + + /* + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + Validate.notNull(src, "src may not be null"); +// Validate.isTrue(src != dest, "src and dest image may not be same"); + + if (dest == null) { + dest = createCompatibleDestImage(src, ColorModel.getRGBdefault()); + } + + filter(src.getRaster(), dest.getRaster()); + + return dest; + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return getBounds2D(src.getRaster()); + } + + public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { + // TODO: dest color model depends on bands... + return destCM == null ? + new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_3BYTE_BGR) : + new BufferedImage(destCM, destCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), destCM.isAlphaPremultiplied(), null); + } + */ + + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY()); + } + else { + dstPt.setLocation(srcPt); + } + + return dstPt; + } + + public RenderingHints getRenderingHints() { + return null; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java new file mode 100644 index 00000000..373e1476 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * JFIFSegment + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JFIFSegment.java,v 1.0 23.04.12 16:52 haraldk Exp$ + */ +class JFIFSegment { + final int majorVersion; + final int minorVersion; + final int units; + final int xDensity; + final int yDensity; + final int xThumbnail; + final int yThumbnail; + final byte[] thumbnail; + + private JFIFSegment(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail) { + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + this.units = units; + this.xDensity = xDensity; + this.yDensity = yDensity; + this.xThumbnail = xThumbnail; + this.yThumbnail = yThumbnail; + this.thumbnail = thumbnail; + } + + @Override + public String toString() { + return String.format("JFIF v%d.%02d %dx%d %s (%s)", majorVersion, minorVersion, xDensity, yDensity, unitsAsString(), thumbnailToString()); + } + + private String unitsAsString() { + switch (units) { + case 0: + return "(aspect only)"; + case 1: + return "dpi"; + case 2: + return "dpcm"; + default: + return "(unknown unit)"; + } + } + + private String thumbnailToString() { + if (xThumbnail == 0 || yThumbnail == 0) { + return "no thumbnail"; + } + + return String.format("thumbnail: %dx%d", xThumbnail, yThumbnail); + } + + public static JFIFSegment read(final InputStream data) throws IOException { + DataInputStream stream = new DataInputStream(data); + + int x, y; + + return new JFIFSegment( + stream.readUnsignedByte(), + stream.readUnsignedByte(), + stream.readUnsignedByte(), + stream.readUnsignedShort(), + stream.readUnsignedShort(), + x = stream.readUnsignedByte(), + y = stream.readUnsignedByte(), + JPEGImageReader.readFully(stream, x * y * 3) + ); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java new file mode 100644 index 00000000..71e25bb4 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * JFIFThumbnailReader + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JFIFThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$ + */ +final class JFIFThumbnailReader extends ThumbnailReader { + private final JFIFSegment segment; + + public JFIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, JFIFSegment segment) { + super(progressListener, imageIndex, thumbnailIndex); + this.segment = segment; + } + + @Override + public BufferedImage read() { + processThumbnailStarted(); + BufferedImage thumbnail = readRawThumbnail(segment.thumbnail, segment.thumbnail.length, 0, segment.xThumbnail, segment.yThumbnail); + processThumbnailProgress(100f); + processThumbnailComplete(); + + return thumbnail; + } + + @Override + public int getWidth() throws IOException { + return segment.xThumbnail; + } + + @Override + public int getHeight() throws IOException { + return segment.yThumbnail; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java new file mode 100644 index 00000000..00d33534 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * JFXXSegment + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JFXXSegment.java,v 1.0 23.04.12 16:54 haraldk Exp$ + */ +class JFXXSegment { + public static final int JPEG = 0x10; + public static final int INDEXED = 0x11; + public static final int RGB = 0x13; + + final int extensionCode; + final byte[] thumbnail; + + private JFXXSegment(int extensionCode, byte[] thumbnail) { + this.extensionCode = extensionCode; + this.thumbnail = thumbnail; + } + + @Override + public String toString() { + return String.format("JFXX extension (%s thumb size: %d)", extensionAsString(), thumbnail.length); + } + + private String extensionAsString() { + switch (extensionCode) { + case JPEG: + return "JPEG"; + case INDEXED: + return "Indexed"; + case RGB: + return "RGB"; + default: + return String.valueOf(extensionCode); + } + } + + public static JFXXSegment read(InputStream data, int length) throws IOException { + DataInputStream stream = new DataInputStream(data); + + return new JFXXSegment( + stream.readUnsignedByte(), + JPEGImageReader.readFully(stream, length - 1) + ); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java new file mode 100644 index 00000000..cce8b3bb --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.image.InverseColorMapIndexColorModel; + +import javax.imageio.IIOException; +import java.awt.image.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.ref.SoftReference; + +/** + * JFXXThumbnailReader + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JFXXThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$ + */ +final class JFXXThumbnailReader extends ThumbnailReader { + + private final JFXXSegment segment; + + private transient SoftReference cachedThumbnail; + + protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) { + super(progressListener, imageIndex, thumbnailIndex); + this.segment = segment; + } + + @Override + public BufferedImage read() throws IOException { + processThumbnailStarted(); + + BufferedImage thumbnail; + switch (segment.extensionCode) { + case JFXXSegment.JPEG: + thumbnail = readJPEGCached(true); + break; + case JFXXSegment.INDEXED: + thumbnail = readIndexed(); + break; + case JFXXSegment.RGB: + thumbnail = readRGB(); + break; + default: + throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode)); + } + + processThumbnailProgress(100f); + processThumbnailComplete(); + + return thumbnail; + } + + private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException { + BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null; + + if (thumbnail == null) { + thumbnail = readJPEGThumbnail(new ByteArrayInputStream(segment.thumbnail)); + } + + cachedThumbnail = pixelsExposed ? null : new SoftReference(thumbnail); + + return thumbnail; + } + + @Override + public int getWidth() throws IOException { + switch (segment.extensionCode) { + case JFXXSegment.RGB: + case JFXXSegment.INDEXED: + return segment.thumbnail[0] & 0xff; + case JFXXSegment.JPEG: + return readJPEGCached(false).getWidth(); + default: + throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode)); + } + } + + @Override + public int getHeight() throws IOException { + switch (segment.extensionCode) { + case JFXXSegment.RGB: + case JFXXSegment.INDEXED: + return segment.thumbnail[1] & 0xff; + case JFXXSegment.JPEG: + return readJPEGCached(false).getHeight(); + default: + throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode)); + } + } + + private BufferedImage readIndexed() { + // 1 byte: xThumb + // 1 byte: yThumb + // 768 bytes: palette + // x * y bytes: 8 bit indexed pixels + int w = segment.thumbnail[0] & 0xff; + int h = segment.thumbnail[1] & 0xff; + + int[] rgbs = new int[256]; + for (int i = 0; i < rgbs.length; i++) { + rgbs[i] = (segment.thumbnail[3 * i + 2] & 0xff) << 16 + | (segment.thumbnail[3 * i + 3] & 0xff) << 8 + | (segment.thumbnail[3 * i + 4] & 0xff); + } + + IndexColorModel icm = new InverseColorMapIndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE); + DataBufferByte buffer = new DataBufferByte(segment.thumbnail, segment.thumbnail.length - 770, 770); + WritableRaster raster = Raster.createPackedRaster(buffer, w, h, 8, null); + + return new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null); + } + + private BufferedImage readRGB() { + // 1 byte: xThumb + // 1 byte: yThumb + // 3 * x * y bytes: 24 bit RGB pixels + int w = segment.thumbnail[0] & 0xff; + int h = segment.thumbnail[1] & 0xff; + + return ThumbnailReader.readRawThumbnail(segment.thumbnail, segment.thumbnail.length - 2, 2, w, h); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGColorSpace.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGColorSpace.java new file mode 100644 index 00000000..157648fc --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGColorSpace.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +/** + * JPEGColorSpace + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGColorSpace.java,v 1.0 26.04.12 15:05 haraldk Exp$ + */ +enum JPEGColorSpace { + Gray, + GrayA, + RGB, + RGBA, + YCbCr, + YCbCrA, + PhotoYCC, + PhotoYCCA, + CMYK, + YCCK +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java new file mode 100644 index 00000000..6a674242 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -0,0 +1,1323 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.color.ColorSpaces; +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import com.twelvemonkeys.imageio.util.ProgressListenerBase; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.*; +import javax.imageio.event.IIOReadUpdateListener; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.*; +import java.io.*; +import java.util.*; +import java.util.List; + +/** + * A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader}, + * with support for CMYK/YCCK JPEGs, non-standard color spaces,broken ICC profiles + * and more. + * + * @author Harald Kuhr + * @author LUT-based YCbCR conversion by Werner Randelshofer + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$ + */ +public class JPEGImageReader extends ImageReaderBase { + // TODO: Fix the (stream) metadata inconsistency issues. + // - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/ + + private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); + + /** Segment identifiers for the JPEG segments we care about reading. */ + private static final Map> SEGMENT_IDENTIFIERS = createSegmentIds(); + + private static Map> createSegmentIds() { + Map> map = new HashMap>(); + + // JFIF/JFXX APP0 markers + map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS); + + // Exif metadata + map.put(JPEG.APP1, Collections.singletonList("Exif")); + + // ICC Color Profile + map.put(JPEG.APP2, Collections.singletonList("ICC_PROFILE")); + + // Adobe APP14 marker + map.put(JPEG.APP14, Collections.singletonList("Adobe")); + + // SOFn markers + map.put(JPEG.SOF0, null); + map.put(JPEG.SOF1, null); + map.put(JPEG.SOF2, null); + map.put(JPEG.SOF3, null); + map.put(JPEG.SOF5, null); + map.put(JPEG.SOF6, null); + map.put(JPEG.SOF7, null); + map.put(JPEG.SOF9, null); + map.put(JPEG.SOF10, null); + map.put(JPEG.SOF11, null); + map.put(JPEG.SOF13, null); + map.put(JPEG.SOF14, null); + map.put(JPEG.SOF15, null); + + return Collections.unmodifiableMap(map); + } + + /** Our JPEG reading delegate */ + private final ImageReader delegate; + + /** Listens to progress updates in the delegate, and delegates back to this instance */ + private final ProgressDelegator progressDelegator; + + /** Cached JPEG app segments */ + private List segments; + + private List thumbnails; + + JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { + super(provider); + this.delegate = Validate.notNull(delegate); + + progressDelegator = new ProgressDelegator(); + } + + private void installListeners() { + delegate.addIIOReadProgressListener(progressDelegator); + delegate.addIIOReadUpdateListener(progressDelegator); + delegate.addIIOReadWarningListener(progressDelegator); + } + + @Override + protected void resetMembers() { + delegate.reset(); + segments = null; + thumbnails = null; + + installListeners(); + } + + @Override + public void dispose() { + super.dispose(); + + delegate.dispose(); + } + + @Override + public String getFormatName() throws IOException { + return delegate.getFormatName(); + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return delegate.getNumImages(allowSearch); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + return delegate.getWidth(imageIndex); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + return delegate.getHeight(imageIndex); + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + Iterator types = delegate.getImageTypes(imageIndex); + JPEGColorSpace csType = getSourceCSType(getAdobeDCT(), getSOF()); + + if (types == null || !types.hasNext() || csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) { + ArrayList typeList = new ArrayList(); + // Add the standard types, we can always convert to these + typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); + typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); + typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)); + + // We also read and return CMYK if the source image is CMYK/YCCK + original color profile if present + ICC_Profile profile = getEmbeddedICCProfile(); + + if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) { + if (profile != null) { + typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); + } + + typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); + } + else if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.RGB) { + if (profile != null) { + typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2}, DataBuffer.TYPE_BYTE, false, false)); + } + } + else if (csType == JPEGColorSpace.YCbCrA || csType == JPEGColorSpace.RGBA) { + // Prepend ARGB types + typeList.addAll(0, Arrays.asList( + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE), + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE) + )); + + if (profile != null) { + typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2, 3}, DataBuffer.TYPE_BYTE, false, false)); + } + } + + return typeList.iterator(); + } + + return types; + } + + @Override + public + ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException { + // If delegate can determine the spec, we'll just go with that + ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex); + + if (rawType != null) { + return rawType; + } + + // Otherwise, consult the image metadata + JPEGColorSpace csType = getSourceCSType(getAdobeDCT(), getSOF()); + + switch (csType) { + case CMYK: + // Create based on embedded profile if exists, or create from "Generic CMYK" + ICC_Profile profile = getEmbeddedICCProfile(); + + if (profile != null) { + return ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); + } + + return ImageTypeSpecifier.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); + default: + // For other types, we probably can't give a proper type, return null + return null; + } + } + + @Override + public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(input, seekForwardOnly, ignoreMetadata); + + // JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments + delegate.setInput(imageInput != null ? new JPEGSegmentImageInputStream(imageInput) : null, seekForwardOnly, ignoreMetadata); + } + + @Override + public boolean isRandomAccessEasy(int imageIndex) throws IOException { + return delegate.isRandomAccessEasy(imageIndex); + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + assertInput(); + checkBounds(imageIndex); + + // TODO: This test is not good enough for JDK7, which seems to have fixed some of the issues. + // NOTE: We rely on the fact that unsupported images has no valid types. This is kind of hacky. + // Might want to look into the metadata, to see if there's a better way to identify these. + boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext(); + + ICC_Profile profile = getEmbeddedICCProfile(); + AdobeDCT adobeDCT = getAdobeDCT(); + + // TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more... + // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) + // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream. + if (delegate.canReadRaster() && ( + unsupported || + adobeDCT != null && adobeDCT.getTransform() == AdobeDCT.YCCK || + profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) { + if (DEBUG) { + System.out.println("Reading using raster and extra conversion"); + System.out.println("ICC color profile: " + profile); + } + + return readImageAsRasterAndReplaceColorProfile(imageIndex, param, ensureDisplayProfile(profile)); + } + + if (DEBUG) { + System.out.println("Reading using delegate"); + } + + return delegate.read(imageIndex, param); + } + + private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, ICC_Profile profile) throws IOException { + int origWidth = getWidth(imageIndex); + int origHeight = getHeight(imageIndex); + + AdobeDCT adobeDCT = getAdobeDCT(); + SOF startOfFrame = getSOF(); + JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame); + + Iterator imageTypes = getImageTypes(imageIndex); + BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight); + WritableRaster destination = image.getRaster(); + + // TODO: checkReadParamBandSettings(param, ); + + RasterOp convert = null; + ICC_ColorSpace intendedCS = profile != null ? ColorSpaces.createColorSpace(profile) : null; + + if (profile != null && (csType == JPEGColorSpace.Gray || csType == JPEGColorSpace.GrayA)) { + // com.sun. reader does not do ColorConvertOp for CS_GRAY, even if embedded ICC profile, + // probably because IJG native part does it already...? If applied, color looks wrong (too dark)... +// convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null); + } + else if (intendedCS != null) { + // Handle inconsistencies + if (startOfFrame.componentsInFrame != intendedCS.getNumComponents()) { + if (startOfFrame.componentsInFrame < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) { + processWarningOccurred(String.format( + "Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " + + "Ignoring Adobe App14 marker, assuming YCbCr/RGB data.", + startOfFrame.marker & 0xf, startOfFrame.componentsInFrame + )); + + csType = JPEGColorSpace.YCbCr; + } + else { + // If ICC profile number of components and startOfFrame does not match, ignore ICC profile + processWarningOccurred(String.format( + "Embedded ICC color profile is incompatible with image data. " + + "Profile indicates %d components, but SOF%d has %d color components. " + + "Ignoring ICC profile, assuming source color space %s.", + intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame, csType + )); + } + } + // NOTE: Avoid using CCOp if same color space, as it's more compatible that way + else if (intendedCS != image.getColorModel().getColorSpace()) { + convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null); + } + // Else, pass through with no conversion + } + else if (csType == JPEGColorSpace.YCCK || csType == JPEGColorSpace.CMYK) { + ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); + + if (cmykCS instanceof ICC_ColorSpace) { + convert = new ColorConvertOp(cmykCS, image.getColorModel().getColorSpace(), null); + } + else { + // ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead + convert = new FastCMYKToRGB(); + } + } + else if (profile != null) { + processWarningOccurred("Embedded ICC color profile is incompatible with Java 2D, color profile will be ignored."); + } + + // We'll need a read param + Rectangle origSourceRegion; + if (param == null) { + param = delegate.getDefaultReadParam(); + origSourceRegion = null; + } + else { + origSourceRegion = param.getSourceRegion(); + } + + Rectangle srcRegion = new Rectangle(); + Rectangle dstRegion = new Rectangle(); + computeRegions(param, origWidth, origHeight, image, srcRegion, dstRegion); + + // We're ready to go + processImageStarted(imageIndex); + + // Unfortunately looping is slower than reading all at once, but + // that requires 2 x memory or more, so a few steps is an ok compromise I guess + try { + final int step = Math.max(1024, srcRegion.height / 10); // * param.getSourceYSubsampling(); // TODO: Using a multiple of 8 is probably a good idea for JPEG + final int srcMaxY = srcRegion.y + srcRegion.height; + int destY = dstRegion.y; + + for (int y = srcRegion.y; y < srcMaxY; y += step) { + int scan = Math.min(step, srcMaxY - y); + + // Let the progress delegator handle progress, using corrected range + progressDelegator.updateProgressRange(100f * (y + scan) / srcRegion.height); + + Rectangle subRegion = new Rectangle(srcRegion.x, y, srcRegion.width, scan); + param.setSourceRegion(subRegion); + Raster raster = delegate.readRaster(imageIndex, param); // non-converted + + // Apply source color conversion from implicit color space + if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) { + YCbCrConverter.convertYCbCr2RGB(raster); + } + else if (csType == JPEGColorSpace.YCCK) { + YCbCrConverter.convertYCCK2CMYK(raster); + } + else if (csType == JPEGColorSpace.CMYK) { + invertCMYK(raster); + } + // ...else assume the raster is already converted + + int destHeight = Math.min(raster.getHeight(), dstRegion.height - destY); // Avoid off-by-one + Raster src = raster.createChild(0, 0, raster.getWidth(), destHeight, 0, 0, param.getSourceBands()); + WritableRaster dest = destination.createWritableChild(dstRegion.x, destY, raster.getWidth(), destHeight, 0, 0, param.getDestinationBands()); + + // Apply further color conversion for explicit color space, or just copy the pixels into place + if (convert != null) { + convert.filter(src, dest); + } + else { + dest.setRect(0, 0, src); + } + + destY += raster.getHeight(); + + if (abortRequested()) { + processReadAborted(); + break; + } + } + } + finally { + // Restore normal read progress processing + progressDelegator.resetProgressRange(); + + // NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily... + param.setSourceRegion(origSourceRegion); + } + + processImageComplete(); + + return image; + } + + static JPEGColorSpace getSourceCSType(AdobeDCT adobeDCT, final SOF startOfFrame) throws IIOException { + /* + ADAPTED from http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html: + + When reading, the contents of the stream are interpreted by the usual JPEG conventions, as follows: + + • If a JFIF APP0 marker segment is present, the colorspace is known to be either grayscale, YCbCr or CMYK. + If an APP2 marker segment containing an embedded ICC profile is also present, then YCbCr is converted to RGB according + to the formulas given in the JFIF spec, and the ICC profile is assumed to refer to the resulting RGB space. + CMYK data is read as is, and the ICC profile is assumed to refer to the resulting CMYK space. + + • If an Adobe APP14 marker segment is present, the colorspace is determined by consulting the transform flag. + The transform flag takes one of three values: + o 2 - The image is encoded as YCCK (implicitly converted from CMYK on encoding). + o 1 - The image is encoded as YCbCr (implicitly converted from RGB on encoding). + o 0 - Unknown. 3-channel images are assumed to be RGB, 4-channel images are assumed to be CMYK. + + • If neither marker segment is present, the following procedure is followed: Single-channel images are assumed + to be grayscale, and 2-channel images are assumed to be grayscale with an alpha channel. For 3- and 4-channel + images, the component ids are consulted. If these values are 1-3 for a 3-channel image, then the image is + assumed to be YCbCr. If these values are 1-4 for a 4-channel image, then the image is assumed to be YCbCrA. If + these values are > 4, they are checked against the ASCII codes for 'R', 'G', 'B', 'A', 'C', 'c', 'M', 'Y', 'K'. + These can encode the following colorspaces: + + RGB + RGBA + YCC (as 'Y','C','c'), assumed to be PhotoYCC + YCCA (as 'Y','C','c','A'), assumed to be PhotoYCCA + CMYK (as 'C', 'M', 'Y', 'K'). + + Otherwise, 3-channel subsampled images are assumed to be YCbCr, 3-channel non-subsampled images are assumed to + be RGB, 4-channel subsampled images are assumed to be YCCK, and 4-channel, non-subsampled images are assumed to + be CMYK. + + • All other images are declared uninterpretable and an exception is thrown if an attempt is made to read one as + a BufferedImage. Such an image may be read only as a Raster. If an image is interpretable but there is no Java + ColorSpace available corresponding to the encoded colorspace (e.g. YCbCr/YCCK), then ImageReader.getRawImageType + will return null. + */ + + if (adobeDCT != null) { + switch (adobeDCT.getTransform()) { + case AdobeDCT.YCC: + return JPEGColorSpace.YCbCr; + case AdobeDCT.YCCK: + return JPEGColorSpace.YCCK; + case AdobeDCT.Unknown: + if (startOfFrame.components.length == 1) { + return JPEGColorSpace.Gray; + } + else if (startOfFrame.components.length == 3) { + return JPEGColorSpace.RGB; + } + else if (startOfFrame.components.length == 4) { + return JPEGColorSpace.CMYK; + } + // Else fall through + default: + } + } + + switch (startOfFrame.components.length) { + case 1: + return JPEGColorSpace.Gray; + case 2: + return JPEGColorSpace.GrayA; + case 3: + if (startOfFrame.components[0].id == 1 && startOfFrame.components[1].id == 2 && startOfFrame.components[2].id == 3) { + return JPEGColorSpace.YCbCr; + } + else if (startOfFrame.components[0].id == 'R' && startOfFrame.components[1].id == 'G' && startOfFrame.components[2].id == 'B') { + return JPEGColorSpace.RGB; + } + else if (startOfFrame.components[0].id == 'Y' && startOfFrame.components[1].id == 'C' && startOfFrame.components[2].id == 'c') { + return JPEGColorSpace.PhotoYCC; + } + else { + // if subsampled, YCbCr else RGB + for (SOFComponent component : startOfFrame.components) { + if (component.hSub != 1 || component.vSub != 1) { + return JPEGColorSpace.YCbCr; + } + } + + return JPEGColorSpace.RGB; + } + case 4: + if (startOfFrame.components[0].id == 1 && startOfFrame.components[1].id == 2 && startOfFrame.components[2].id == 3 && startOfFrame.components[3].id == 4) { + return JPEGColorSpace.YCbCrA; + } + else if (startOfFrame.components[0].id == 'R' && startOfFrame.components[1].id == 'G' && startOfFrame.components[2].id == 'B' && startOfFrame.components[3].id == 'A') { + return JPEGColorSpace.RGBA; + } + else if (startOfFrame.components[0].id == 'Y' && startOfFrame.components[1].id == 'C' && startOfFrame.components[2].id == 'c' && startOfFrame.components[3].id == 'A') { + return JPEGColorSpace.PhotoYCCA; + } + else if (startOfFrame.components[0].id == 'C' && startOfFrame.components[1].id == 'M' && startOfFrame.components[2].id == 'Y' && startOfFrame.components[3].id == 'K') { + return JPEGColorSpace.CMYK; + } + else if (startOfFrame.components[0].id == 'Y' && startOfFrame.components[1].id == 'C' && startOfFrame.components[2].id == 'c' && startOfFrame.components[3].id == 'K') { + return JPEGColorSpace.YCCK; + } + else { + // if subsampled, YCCK else CMYK + for (SOFComponent component : startOfFrame.components) { + if (component.hSub != 1 || component.vSub != 1) { + return JPEGColorSpace.YCCK; + } + } + + return JPEGColorSpace.CMYK; + } + default: + throw new IIOException("Cannot determine source color space"); + } + } + + private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) { + // NOTE: This is probably not the right way to do it... :-P + // TODO: Consider moving method to ColorSpaces class or new class in imageio.color package + + // NOTE: Workaround for the ColorConvertOp treating the input as relative colorimetric, + // if the FIRST profile has class OUTPUT, regardless of the actual rendering intent in that profile... + // See ColorConvertOp#filter(Raster, WritableRaster) + + if (profile != null && profile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) { + byte[] profileData = profile.getData(); // Need to clone entire profile, due to a JDK 7 bug + + if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) { + intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first + + return ICC_Profile.getInstance(profileData); + } + } + + return profile; + } + + static void intToBigEndian(int value, byte[] array, int index) { + array[index] = (byte) (value >> 24); + array[index+1] = (byte) (value >> 16); + array[index+2] = (byte) (value >> 8); + array[index+3] = (byte) (value); + } + + private void initHeader() throws IOException { + if (segments == null) { + long start = DEBUG ? System.currentTimeMillis() : 0; + + readSegments(); + + if (DEBUG) { + System.out.println("Read metadata in " + (System.currentTimeMillis() - start) + " ms"); + } + } + } + + private void readSegments() throws IOException { + imageInput.mark(); + + try { + imageInput.seek(0); // TODO: Seek to wanted image, skip images on the way + + segments = JPEGSegmentUtil.readSegments(imageInput, SEGMENT_IDENTIFIERS); + } + catch (IOException ignore) { + } + catch (IllegalArgumentException foo) { + foo.printStackTrace(); + } + finally { + imageInput.reset(); + } + } + + private List getAppSegments(final int marker, final String identifier) throws IOException { + initHeader(); + + List appSegments = Collections.emptyList(); + + for (JPEGSegment segment : segments) { + if (segment.marker() == marker && (identifier == null || identifier.equals(segment.identifier()))) { + if (appSegments == Collections.EMPTY_LIST) { + appSegments = new ArrayList(segments.size()); + } + + appSegments.add(segment); + } + } + + return appSegments; + } + + private SOF getSOF() throws IOException { + for (JPEGSegment segment : segments) { + if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 || + JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 || + JPEG.SOF9 >= segment.marker() && segment.marker() <= JPEG.SOF11 || + JPEG.SOF13 >= segment.marker() && segment.marker() <= JPEG.SOF15) { + + DataInputStream data = new DataInputStream(segment.data()); + + try { + int samplePrecision = data.readUnsignedByte(); + int lines = data.readUnsignedShort(); + int samplesPerLine = data.readUnsignedShort(); + int componentsInFrame = data.readUnsignedByte(); + + SOFComponent[] components = new SOFComponent[componentsInFrame]; + + for (int i = 0; i < componentsInFrame; i++) { + int id = data.readUnsignedByte(); + int sub = data.readUnsignedByte(); + int qtSel = data.readUnsignedByte(); + + components[i] = new SOFComponent(id, ((sub & 0xF0) >> 4), (sub & 0xF), qtSel); + } + + return new SOF(segment.marker(), samplePrecision, lines, samplesPerLine, componentsInFrame, components); + } + finally { + data.close(); + } + } + } + + return null; + } + + private AdobeDCT getAdobeDCT() throws IOException { + // TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers + List adobe = getAppSegments(JPEG.APP14, "Adobe"); + + if (!adobe.isEmpty()) { + // version (byte), flags (4bytes), color transform (byte: 0=unknown, 1=YCC, 2=YCCK) + DataInputStream stream = new DataInputStream(adobe.get(0).data()); + + return new AdobeDCT( + stream.readUnsignedByte(), + stream.readUnsignedShort(), + stream.readUnsignedShort(), + stream.readUnsignedByte() + ); + } + + return null; + } + + private JFIFSegment getJFIF() throws IOException{ + List jfif = getAppSegments(JPEG.APP0, "JFIF"); + + if (!jfif.isEmpty()) { + JPEGSegment segment = jfif.get(0); + return JFIFSegment.read(segment.data()); + } + + return null; + } + + private JFXXSegment getJFXX() throws IOException { + List jfxx = getAppSegments(JPEG.APP0, "JFXX"); + + if (!jfxx.isEmpty()) { + JPEGSegment segment = jfxx.get(0); + return JFXXSegment.read(segment.data(), segment.length()); + } + + return null; + } + + // TODO: Util method? + static byte[] readFully(DataInput stream, int len) throws IOException { + if (len == 0) { + return null; + } + + byte[] data = new byte[len]; + stream.readFully(data); + return data; + } + + private ICC_Profile getEmbeddedICCProfile() throws IOException { + // ICC v 1.42 (2006) annex B: + // APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination) + // + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments) + List segments = getAppSegments(JPEG.APP2, "ICC_PROFILE"); + + if (segments.size() == 1) { + // Faster code for the common case + JPEGSegment segment = segments.get(0); + DataInputStream stream = new DataInputStream(segment.data()); + int chunkNumber = stream.readUnsignedByte(); + int chunkCount = stream.readUnsignedByte(); + + if (chunkNumber != 1 && chunkCount != 1) { + processWarningOccurred(String.format("Bad number of 'ICC_PROFILE' chunks: %d of %d. Assuming single chunk.", chunkNumber, chunkCount)); + } + + return readICCProfileSafe(stream); + } + else if (!segments.isEmpty()) { + // NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order... + DataInputStream stream = new DataInputStream(segments.get(0).data()); + int chunkNumber = stream.readUnsignedByte(); + int chunkCount = stream.readUnsignedByte(); + + boolean badICC = false; + if (chunkCount != segments.size()) { + // Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0. + // Others use count == 1, and all numbers == 1. + // Handle these by issuing warning + badICC = true; + processWarningOccurred(String.format("Unexpected 'ICC_PROFILE' chunk count: %d. Ignoring count, assuming %d chunks in sequence.", chunkCount, segments.size())); + } + + if (!badICC && chunkNumber < 1) { + // Anything else is just ignored + processWarningOccurred(String.format("Invalid 'ICC_PROFILE' chunk index: %d. Ignoring ICC profile.", chunkNumber)); + return null; + } + + int count = badICC ? segments.size() : chunkCount; + InputStream[] streams = new InputStream[count]; + streams[badICC ? 0 : chunkNumber - 1] = stream; + + for (int i = 1; i < count; i++) { + stream = new DataInputStream(segments.get(i).data()); + + chunkNumber = stream.readUnsignedByte(); + + if (!badICC && stream.readUnsignedByte() != chunkCount) { + throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks: %d of %d.", chunkNumber, chunkCount)); + } + + streams[badICC ? i : chunkNumber - 1] = stream; + } + + return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams)))); + } + + return null; + } + + private ICC_Profile readICCProfileSafe(final InputStream stream) throws IOException { + try { + return ICC_Profile.getInstance(stream); + } + catch (RuntimeException e) { + // NOTE: Throws either IllegalArgumentException or CMMException, depending on platform. + // Usual reason: Broken tools store truncated ICC profiles in a single ICC_PROFILE chunk... + processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk(s): %s. Ignoring ICC profile.", e.getMessage())); + return null; + } + } + + @Override + public boolean canReadRaster() { + return delegate.canReadRaster(); + } + + @Override + public Raster readRaster(int imageIndex, ImageReadParam param) throws IOException { + return delegate.readRaster(imageIndex, param); + } + + @Override + public RenderedImage readAsRenderedImage(int imageIndex, ImageReadParam param) throws IOException { + return read(imageIndex, param); + } + + @Override + public void abort() { + super.abort(); + + delegate.abort(); + } + + @Override + public boolean readerSupportsThumbnails() { + return true; // We support EXIF, JFIF and JFXX style thumbnails + } + + private void readThumbnailMetadata(int imageIndex) throws IOException { + checkBounds(imageIndex); + + if (thumbnails == null) { + thumbnails = new ArrayList(); + ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate(); + + // Read JFIF thumbnails if present + JFIFSegment jfif = getJFIF(); + if (jfif != null && jfif.thumbnail != null) { + thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif)); + } + + // Read JFXX thumbnails if present + JFXXSegment jfxx = getJFXX(); + if (jfxx != null && jfxx.thumbnail != null) { + switch (jfxx.extensionCode) { + case JFXXSegment.JPEG: + case JFXXSegment.INDEXED: + case JFXXSegment.RGB: + thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfxx)); + break; + default: + processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode); + } + } + + // Read Exif thumbnails if present + List exifSegments = getAppSegments(JPEG.APP1, "Exif"); + if (!exifSegments.isEmpty()) { + JPEGSegment exif = exifSegments.get(0); + InputStream data = exif.data(); + + if (data.read() == -1) { + // Pad + processWarningOccurred("Exif chunk has no data."); + } + else { + ImageInputStream stream = ImageIO.createImageInputStream(data); + CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream); + + if (exifMetadata.directoryCount() == 2) { + Directory ifd1 = exifMetadata.getDirectory(1); + + Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION); + + // 1 = no compression, 6 = JPEG compression (default) + if (compression == null || compression.getValue().equals(1) || compression.getValue().equals(6)) { + thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, 0, thumbnails.size(), ifd1, stream)); + } + else { + processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression.getValue()); + } + } + } + } + } + } + + @Override + public int getNumThumbnails(final int imageIndex) throws IOException { + readThumbnailMetadata(imageIndex); + + return thumbnails.size(); + } + + private void checkThumbnailBounds(int imageIndex, int thumbnailIndex) throws IOException { + Validate.isTrue(thumbnailIndex >= 0, thumbnailIndex, "thumbnailIndex < 0; %d"); + Validate.isTrue(getNumThumbnails(imageIndex) > thumbnailIndex, thumbnailIndex, "thumbnailIndex >= numThumbnails; %d"); + } + + @Override + public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException { + checkThumbnailBounds(imageIndex, thumbnailIndex); + + return thumbnails.get(thumbnailIndex).getWidth(); + } + + @Override + public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException { + checkThumbnailBounds(imageIndex, thumbnailIndex); + + return thumbnails.get(thumbnailIndex).getHeight(); + } + + @Override + public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException { + checkThumbnailBounds(imageIndex, thumbnailIndex); + + return thumbnails.get(thumbnailIndex).read(); + } + + private static void invertCMYK(final Raster raster) { + byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); + + for (int i = 0, dataLength = data.length; i < dataLength; i++) { + data[i] = (byte) (255 - data[i] & 0xff); + } + } + + /** + * Static inner class for lazy-loading of conversion tables. + */ + static final class YCbCrConverter { + /** Define tables for YCC->RGB color space conversion. */ + private final static int SCALEBITS = 16; + private final static int MAXJSAMPLE = 255; + private final static int CENTERJSAMPLE = 128; + private final static int ONE_HALF = 1 << (SCALEBITS - 1); + + private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1]; + private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1]; + private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1]; + private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1]; + + /** + * Initializes tables for YCC->RGB color space conversion. + */ + private static void buildYCCtoRGBtable() { + if (DEBUG) { + System.err.println("Building YCC conversion table"); + } + + for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) { + // i is the actual input pixel value, in the range 0..MAXJSAMPLE + // The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE + // Cr=>R value is nearest int to 1.40200 * x + Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; + // Cb=>B value is nearest int to 1.77200 * x + Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS; + // Cr=>G value is scaled-up -0.71414 * x + Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x; + // Cb=>G value is scaled-up -0.34414 * x + // We also add in ONE_HALF so that need not do it in inner loop + Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF; + } + } + + static { + buildYCCtoRGBtable(); + } + + static void convertYCbCr2RGB(final Raster raster) { + final int height = raster.getHeight(); + final int width = raster.getWidth(); + final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + convertYCbCr2RGB(data, data, (x + y * width) * 3); + } + } + } + + static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) { + int y = yCbCr[offset ] & 0xff; + int cr = yCbCr[offset + 2] & 0xff; + int cb = yCbCr[offset + 1] & 0xff; + + rgb[offset ] = clamp(y + Cr_R_LUT[cr]); + rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); + rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]); + } + + static void convertYCCK2CMYK(final Raster raster) { + final int height = raster.getHeight(); + final int width = raster.getWidth(); + final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + convertYCCK2CMYK(data, data, (x + y * width) * 4); + } + } + } + + private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) { + // Inverted + int y = 255 - ycck[offset ] & 0xff; + int cb = 255 - ycck[offset + 1] & 0xff; + int cr = 255 - ycck[offset + 2] & 0xff; + int k = 255 - ycck[offset + 3] & 0xff; + + int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]); + int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS)); + int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]); + + cmyk[offset ] = clamp(cmykC); + cmyk[offset + 1] = clamp(cmykM); + cmyk[offset + 2] = clamp(cmykY); + cmyk[offset + 3] = (byte) k; // K passes through unchanged + } + + private static byte clamp(int val) { + return (byte) Math.max(0, Math.min(255, val)); + } + } + + private class ProgressDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener { + float readProgressStart = -1; + float readProgressStop = -1; + + void resetProgressRange() { + readProgressStart = -1; + readProgressStop = -1; + } + + private boolean isProgressRangeCorrected() { + return readProgressStart == -1 && readProgressStop == -1; + } + + void updateProgressRange(float limit) { + Validate.isTrue(limit >= 0, limit, "Negative range limit"); + + readProgressStart = readProgressStop != -1 ? readProgressStop : 0; + readProgressStop = limit; + } + + @Override + public void imageComplete(ImageReader source) { + if (isProgressRangeCorrected()) { + processImageComplete(); + } + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + if (isProgressRangeCorrected()) { + processImageProgress(percentageDone); + } + else { + processImageProgress(readProgressStart + (percentageDone * (readProgressStop - readProgressStart) / 100f)); + } + } + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + if (isProgressRangeCorrected()) { + processImageStarted(imageIndex); + } + } + + @Override + public void readAborted(ImageReader source) { + if (isProgressRangeCorrected()) { + processReadAborted(); + } + } + + @Override + public void sequenceComplete(ImageReader source) { + processSequenceComplete(); + } + + @Override + public void sequenceStarted(ImageReader source, int minIndex) { + processSequenceStarted(minIndex); + } + + @Override + public void thumbnailComplete(ImageReader source) { + processThumbnailComplete(); + } + + @Override + public void thumbnailProgress(ImageReader source, float percentageDone) { + processThumbnailProgress(percentageDone); + } + + @Override + public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { + processThumbnailStarted(imageIndex, thumbnailIndex); + } + + public void passStarted(ImageReader source, BufferedImage theImage, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) { + processPassStarted(theImage, pass, minPass, maxPass, minX, minY, periodX, periodY, bands); + } + + public void imageUpdate(ImageReader source, BufferedImage theImage, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) { + processImageUpdate(theImage, minX, minY, width, height, periodX, periodY, bands); + } + + public void passComplete(ImageReader source, BufferedImage theImage) { + processPassComplete(theImage); + } + + public void thumbnailPassStarted(ImageReader source, BufferedImage theThumbnail, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) { + processThumbnailPassStarted(theThumbnail, pass, minPass, maxPass, minX, minY, periodX, periodY, bands); + } + + public void thumbnailUpdate(ImageReader source, BufferedImage theThumbnail, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) { + processThumbnailUpdate(theThumbnail, minX, minY, width, height, periodX, periodY, bands); + } + + public void thumbnailPassComplete(ImageReader source, BufferedImage theThumbnail) { + processThumbnailPassComplete(theThumbnail); + } + + public void warningOccurred(ImageReader source, String warning) { + processWarningOccurred(warning); + } + } + + private class ThumbnailProgressDelegate implements ThumbnailReadProgressListener { + public void processThumbnailStarted(int imageIndex, int thumbnailIndex) { + JPEGImageReader.this.processThumbnailStarted(imageIndex, thumbnailIndex); + } + + public void processThumbnailProgress(float percentageDone) { + JPEGImageReader.this.processThumbnailProgress(percentageDone); + } + + public void processThumbnailComplete() { + JPEGImageReader.this.processThumbnailComplete(); + } + } + + private static class SOF { + private final int marker; + private final int samplePrecision; + private final int lines; // height + private final int samplesPerLine; // width + private final int componentsInFrame; + final SOFComponent[] components; + + public SOF(int marker, int samplePrecision, int lines, int samplesPerLine, int componentsInFrame, SOFComponent[] components) { + this.marker = marker; + this.samplePrecision = samplePrecision; + this.lines = lines; + this.samplesPerLine = samplesPerLine; + this.componentsInFrame = componentsInFrame; + this.components = components; + } + + public int getMarker() { + return marker; + } + + public int getSamplePrecision() { + return samplePrecision; + } + + public int getLines() { + return lines; + } + + public int getSamplesPerLine() { + return samplesPerLine; + } + + public int getComponentsInFrame() { + return componentsInFrame; + } + + @Override + public String toString() { + return String.format( + "SOF%d[%04x, precision: %d, lines: %d, samples/line: %d, components: %s]", + marker & 0xf, marker, samplePrecision, lines, samplesPerLine, Arrays.toString(components) + ); + } + } + + private static class SOFComponent { + final int id; + final int hSub; + final int vSub; + final int qtSel; + + public SOFComponent(int id, int hSub, int vSub, int qtSel) { + this.id = id; + this.hSub = hSub; + this.vSub = vSub; + this.qtSel = qtSel; + } + + @Override + public String toString() { + // Use id either as component number or component name, based on value + Serializable idStr = (id >= 'a' && id <= 'z' || id >= 'A' && id <= 'Z') ? "'" + (char) id + "'" : id; + return String.format("id: %s, sub: %d/%d, sel: %d", idStr, hSub, vSub, qtSel); + } + } + + protected static void showIt(final BufferedImage pImage, final String pTitle) { + ImageReaderBase.showIt(pImage, pTitle); + } + + public static void main(final String[] args) throws IOException { + for (final String arg : args) { + File file = new File(arg); + + ImageInputStream input = ImageIO.createImageInputStream(file); + if (input == null) { + System.err.println("Could not read file: " + file); + continue; + } + + Iterator readers = ImageIO.getImageReaders(input); + + if (!readers.hasNext()) { + System.err.println("No reader for: " + file); + continue; + } + + ImageReader reader = readers.next(); +// System.err.println("Reading using: " + reader); + + reader.addIIOReadWarningListener(new IIOReadWarningListener() { + public void warningOccurred(ImageReader source, String warning) { + System.err.println("Warning: " + arg + ": " + warning); + } + }); + reader.addIIOReadProgressListener(new ProgressListenerBase() { + private static final int MAX_W = 78; + int lastProgress = 0; + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + System.out.print("["); + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + int steps = ((int) (percentageDone * MAX_W) / 100); + + for (int i = lastProgress; i < steps; i++) { + System.out.print("."); + } + + System.out.flush(); + lastProgress = steps; + } + + @Override + public void imageComplete(ImageReader source) { + for (int i = lastProgress; i < MAX_W; i++) { + System.out.print("."); + } + + System.out.println("]"); + } + }); + + reader.setInput(input); + + try { + ImageReadParam param = reader.getDefaultReadParam(); +// if (args.length > 1) { +// int sub = Integer.parseInt(args[1]); +// int sub = 4; +// param.setSourceSubsampling(sub, sub, 0, 0); +// } + + long start = System.currentTimeMillis(); + BufferedImage image = reader.read(0, param); +// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); +// System.err.println("image: " + image); + + +// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null); + +// int maxW = 1280; +// int maxH = 800; + int maxW = 400; + int maxH = 400; + if (image.getWidth() > maxW || image.getHeight() > maxH) { + start = System.currentTimeMillis(); + float aspect = reader.getAspectRatio(0); + if (aspect >= 1f) { + image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT); + } + else { + image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_DEFAULT); + } +// System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms"); + } + + showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(0), reader.getHeight(0))); + + try { + int numThumbnails = reader.getNumThumbnails(0); + for (int i = 0; i < numThumbnails; i++) { + BufferedImage thumbnail = reader.readThumbnail(0, i); +// System.err.println("thumbnail: " + thumbnail); + showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); + } + } + catch (IIOException e) { + System.err.println("Could not read thumbnails: " + e.getMessage()); + e.printStackTrace(); + } + } + catch (Throwable t) { + System.err.println(file); + t.printStackTrace(); + } + finally { + input.close(); + } + } + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java new file mode 100644 index 00000000..d3814c0c --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderSpi.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.spi.ProviderInfo; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadataFormat; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ServiceRegistry; +import java.io.IOException; +import java.util.Locale; + +/** + * JPEGImageReaderSpi + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImageReaderSpi.java,v 1.0 24.01.11 22.12 haraldk Exp$ + */ +public class JPEGImageReaderSpi extends ImageReaderSpi { + private ImageReaderSpi delegateProvider; + + /** + * Constructor for use by {@link javax.imageio.spi.IIORegistry} only. + * The instance created will not work without being properly registered. + */ + public JPEGImageReaderSpi() { + this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class)); + } + + private JPEGImageReaderSpi(final ProviderInfo providerInfo) { + super( + providerInfo.getVendorName(), + providerInfo.getVersion(), + new String[]{"JPEG", "jpeg", "JPG", "jpg"}, + new String[]{"jpg", "jpeg"}, + new String[]{"image/jpeg"}, + "com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReader", + STANDARD_INPUT_TYPE, + new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi"}, + true, null, null, null, null, + true, null, null, null, null + ); + } + + /** + * Creates a {@code JPEGImageReaderSpi} with the given delegate. + * + * @param delegateProvider a {@code ImageReaderSpi} that can read JPEG. + */ + protected JPEGImageReaderSpi(final ImageReaderSpi delegateProvider) { + this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class)); + + this.delegateProvider = Validate.notNull(delegateProvider); + } + + static ImageReaderSpi lookupDelegateProvider(final ServiceRegistry registry) { + // Should be safe to lookup now, as the bundled providers are hardcoded usually + try { + return (ImageReaderSpi) registry.getServiceProviderByClass(Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi")); + } + catch (ClassNotFoundException ignore) { + } + catch (SecurityException e) { + e.printStackTrace(); + } + + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public void onRegistration(final ServiceRegistry registry, final Class category) { + if (delegateProvider == null) { + // Install delegate now + delegateProvider = lookupDelegateProvider(registry); + } + + if (delegateProvider != null) { + // Order before com.sun provider, to aid ImageIO in selecting our reader + registry.setOrdering((Class) category, this, delegateProvider); + } + else { + // Or, if no delegate is found, silently deregister from the registry + IIOUtil.deregisterProvider(registry, this, category); + } + } + + @Override + public String getVendorName() { + return String.format("%s/%s", super.getVendorName(), delegateProvider.getVendorName()); + } + + @Override + public String getVersion() { + return String.format("%s/%s", super.getVersion(), delegateProvider.getVersion()); + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new JPEGImageReader(this, delegateProvider.createReaderInstance(extension)); + } + + @Override + public boolean canDecodeInput(Object source) throws IOException { + return delegateProvider.canDecodeInput(source); + } + + @Override + public String[] getFormatNames() { + return delegateProvider.getFormatNames(); + } + + @Override + public String[] getFileSuffixes() { + return delegateProvider.getFileSuffixes(); + } + + @Override + public String[] getMIMETypes() { + return delegateProvider.getMIMETypes(); + } + + @Override + public boolean isStandardStreamMetadataFormatSupported() { + return delegateProvider.isStandardStreamMetadataFormatSupported(); + } + + @Override + public String getNativeStreamMetadataFormatName() { + return delegateProvider.getNativeStreamMetadataFormatName(); + } + + @Override + public String[] getExtraStreamMetadataFormatNames() { + return delegateProvider.getExtraStreamMetadataFormatNames(); + } + + @Override + public boolean isStandardImageMetadataFormatSupported() { + return delegateProvider.isStandardImageMetadataFormatSupported(); + } + + @Override + public String getNativeImageMetadataFormatName() { + return delegateProvider.getNativeImageMetadataFormatName(); + } + + @Override + public String[] getExtraImageMetadataFormatNames() { + return delegateProvider.getExtraImageMetadataFormatNames(); + } + + @Override + public IIOMetadataFormat getStreamMetadataFormat(String formatName) { + return delegateProvider.getStreamMetadataFormat(formatName); + } + + @Override + public IIOMetadataFormat getImageMetadataFormat(String formatName) { + return delegateProvider.getImageMetadataFormat(formatName); + } + + @Override + public String getDescription(Locale locale) { + return delegateProvider.getDescription(locale); + } + + @Override + public Class[] getInputTypes() { + return delegateProvider.getInputTypes(); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriter.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriter.java new file mode 100644 index 00000000..65363996 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriter.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.ImageWriterBase; +import com.twelvemonkeys.imageio.util.ProgressListenerBase; + +import javax.imageio.IIOImage; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.event.IIOWriteWarningListener; +import javax.imageio.metadata.IIOMetadata; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +/** + * JPEGImageWriter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImageWriter.java,v 1.0 06.02.12 16:39 haraldk Exp$ + */ +public class JPEGImageWriter extends ImageWriterBase { + // TODO: Extend with functionality to be able to write CMYK JPEGs as well? + + /** Our JPEG writing delegate */ + private final ImageWriter delegate; + + /** Listens to progress updates in the delegate, and delegates back to this instance */ + private final ProgressDelegator progressDelegator; + + public JPEGImageWriter(final JPEGImageWriterSpi provider, final ImageWriter delegate) { + super(provider); + this.delegate = delegate; + + progressDelegator = new ProgressDelegator(); + } + + private void installListeners() { + delegate.addIIOWriteProgressListener(progressDelegator); + delegate.addIIOWriteWarningListener(progressDelegator); + } + + @Override + protected void resetMembers() { + installListeners(); + } + + @Override + public void setOutput(Object output) { + super.setOutput(output); + + delegate.setOutput(output); + } + + @Override + public Object getOutput() { + return delegate.getOutput(); + } + + @Override + public Locale[] getAvailableLocales() { + return delegate.getAvailableLocales(); + } + + @Override + public void setLocale(Locale locale) { + delegate.setLocale(locale); + } + + @Override + public Locale getLocale() { + return delegate.getLocale(); + } + + @Override + public ImageWriteParam getDefaultWriteParam() { + return delegate.getDefaultWriteParam(); + } + + @Override + public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { + return delegate.getDefaultStreamMetadata(param); + } + + @Override + public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { + return delegate.getDefaultImageMetadata(imageType, param); + } + + @Override + public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { + return delegate.convertStreamMetadata(inData, param); + } + + @Override + public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { + return delegate.convertImageMetadata(inData, imageType, param); + } + + @Override + public int getNumThumbnailsSupported(ImageTypeSpecifier imageType, ImageWriteParam param, IIOMetadata streamMetadata, IIOMetadata imageMetadata) { + return delegate.getNumThumbnailsSupported(imageType, param, streamMetadata, imageMetadata); + } + + @Override + public Dimension[] getPreferredThumbnailSizes(ImageTypeSpecifier imageType, ImageWriteParam param, IIOMetadata streamMetadata, IIOMetadata imageMetadata) { + return delegate.getPreferredThumbnailSizes(imageType, param, streamMetadata, imageMetadata); + } + + @Override + public boolean canWriteRasters() { + return delegate.canWriteRasters(); + } + + @Override + public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { + delegate.write(streamMetadata, image, param); + } + + @Override + public void write(IIOImage image) throws IOException { + delegate.write(image); + } + + @Override + public void write(RenderedImage image) throws IOException { + delegate.write(image); + } + + @Override + public boolean canWriteSequence() { + return delegate.canWriteSequence(); + } + + @Override + public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException { + delegate.prepareWriteSequence(streamMetadata); + } + + @Override + public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException { + delegate.writeToSequence(image, param); + } + + @Override + public void endWriteSequence() throws IOException { + delegate.endWriteSequence(); + } + + @Override + public boolean canReplaceStreamMetadata() throws IOException { + return delegate.canReplaceStreamMetadata(); + } + + @Override + public void replaceStreamMetadata(IIOMetadata streamMetadata) throws IOException { + delegate.replaceStreamMetadata(streamMetadata); + } + + @Override + public boolean canReplaceImageMetadata(int imageIndex) throws IOException { + return delegate.canReplaceImageMetadata(imageIndex); + } + + @Override + public void replaceImageMetadata(int imageIndex, IIOMetadata imageMetadata) throws IOException { + delegate.replaceImageMetadata(imageIndex, imageMetadata); + } + + @Override + public boolean canInsertImage(int imageIndex) throws IOException { + return delegate.canInsertImage(imageIndex); + } + + @Override + public void writeInsert(int imageIndex, IIOImage image, ImageWriteParam param) throws IOException { + delegate.writeInsert(imageIndex, image, param); + } + + @Override + public boolean canRemoveImage(int imageIndex) throws IOException { + return delegate.canRemoveImage(imageIndex); + } + + @Override + public void removeImage(int imageIndex) throws IOException { + delegate.removeImage(imageIndex); + } + + @Override + public boolean canWriteEmpty() throws IOException { + return delegate.canWriteEmpty(); + } + + @Override + public void prepareWriteEmpty(IIOMetadata streamMetadata, ImageTypeSpecifier imageType, int width, int height, IIOMetadata imageMetadata, List thumbnails, ImageWriteParam param) throws IOException { + delegate.prepareWriteEmpty(streamMetadata, imageType, width, height, imageMetadata, thumbnails, param); + } + + @Override + public void endWriteEmpty() throws IOException { + delegate.endWriteEmpty(); + } + + @Override + public boolean canInsertEmpty(int imageIndex) throws IOException { + return delegate.canInsertEmpty(imageIndex); + } + + @Override + public void prepareInsertEmpty(int imageIndex, ImageTypeSpecifier imageType, int width, int height, IIOMetadata imageMetadata, List thumbnails, ImageWriteParam param) throws IOException { + delegate.prepareInsertEmpty(imageIndex, imageType, width, height, imageMetadata, thumbnails, param); + } + + @Override + public void endInsertEmpty() throws IOException { + delegate.endInsertEmpty(); + } + + @Override + public boolean canReplacePixels(int imageIndex) throws IOException { + return delegate.canReplacePixels(imageIndex); + } + + @Override + public void prepareReplacePixels(int imageIndex, Rectangle region) throws IOException { + delegate.prepareReplacePixels(imageIndex, region); + } + + @Override + public void replacePixels(RenderedImage image, ImageWriteParam param) throws IOException { + delegate.replacePixels(image, param); + } + + @Override + public void replacePixels(Raster raster, ImageWriteParam param) throws IOException { + delegate.replacePixels(raster, param); + } + + @Override + public void endReplacePixels() throws IOException { + delegate.endReplacePixels(); + } + + @Override + public void abort() { + super.abort(); + delegate.abort(); + } + + @Override + public void reset() { + super.reset(); + delegate.reset(); + } + + @Override + public void dispose() { + super.dispose(); + delegate.dispose(); + } + + private class ProgressDelegator extends ProgressListenerBase implements IIOWriteWarningListener { + @Override + public void imageComplete(ImageWriter source) { + processImageComplete(); + } + + @Override + public void imageProgress(ImageWriter source, float percentageDone) { + processImageProgress(percentageDone); + } + + @Override + public void imageStarted(ImageWriter source, int imageIndex) { + processImageStarted(imageIndex); + } + + @Override + public void thumbnailComplete(ImageWriter source) { + processThumbnailComplete(); + } + + @Override + public void thumbnailProgress(ImageWriter source, float percentageDone) { + processThumbnailProgress(percentageDone); + } + + @Override + public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) { + processThumbnailStarted(imageIndex, thumbnailIndex); + } + + @Override + public void writeAborted(ImageWriter source) { + processWriteAborted(); + } + + public void warningOccurred(ImageWriter source, int imageIndex, String warning) { + processWarningOccurred(imageIndex, warning); + } + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java new file mode 100644 index 00000000..4201be18 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterSpi.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.spi.ProviderInfo; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOMetadataFormat; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.spi.ServiceRegistry; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.util.Locale; + +/** + * JPEGImageWriterSpi + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImageWriterSpi.java,v 1.0 06.02.12 16:09 haraldk Exp$ + */ +public class JPEGImageWriterSpi extends ImageWriterSpi { + private ImageWriterSpi delegateProvider; + + /** + * Constructor for use by {@link javax.imageio.spi.IIORegistry} only. + * The instance created will not work without being properly registered. + */ + public JPEGImageWriterSpi() { + this(IIOUtil.getProviderInfo(JPEGImageWriterSpi.class)); + } + + private JPEGImageWriterSpi(final ProviderInfo providerInfo) { + super( + providerInfo.getVendorName(), + providerInfo.getVersion(), + new String[]{"JPEG", "jpeg", "JPG", "jpg"}, + new String[]{"jpg", "jpeg"}, + new String[]{"image/jpeg"}, + "twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter", + STANDARD_OUTPUT_TYPE, + new String[] {"twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"}, + true, null, null, null, null, + true, null, null, null, null + ); + } + + /** + * Creates a {@code JPEGImageWriterSpi} with the given delegate. + * + * @param delegateProvider a {@code ImageWriterSpi} that can write JPEG. + */ + protected JPEGImageWriterSpi(final ImageWriterSpi delegateProvider) { + this(IIOUtil.getProviderInfo(JPEGImageReaderSpi.class)); + + this.delegateProvider = Validate.notNull(delegateProvider); + } + + static ImageWriterSpi lookupDelegateProvider(final ServiceRegistry registry) { + // Should be safe to lookup now, as the bundled providers are hardcoded usually + try { + return (ImageWriterSpi) registry.getServiceProviderByClass(Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi")); + } + catch (ClassNotFoundException ignore) { + } + catch (SecurityException e) { + e.printStackTrace(); + } + + return null; + } + + @SuppressWarnings({"unchecked"}) + @Override + public void onRegistration(final ServiceRegistry registry, final Class category) { + if (delegateProvider == null) { + // Install delegate now + delegateProvider = lookupDelegateProvider(registry); + } + + if (delegateProvider != null) { + // Order before com.sun provider, to aid ImageIO in selecting our reader + registry.setOrdering((Class) category, this, delegateProvider); + } + else { + // Or, if no delegate is found, silently deregister from the registry + IIOUtil.deregisterProvider(registry, this, category); + } + } + + @Override + public String getVendorName() { + return String.format("%s/%s", super.getVendorName(), delegateProvider.getVendorName()); + } + + @Override + public String getVersion() { + return String.format("%s/%s", super.getVersion(), delegateProvider.getVersion()); + } + + @Override + public ImageWriter createWriterInstance(Object extension) throws IOException { + return new JPEGImageWriter(this, delegateProvider.createWriterInstance(extension)); + } + + @Override + public String[] getFormatNames() { + return delegateProvider.getFormatNames(); + } + + @Override + public String[] getFileSuffixes() { + return delegateProvider.getFileSuffixes(); + } + + @Override + public String[] getMIMETypes() { + return delegateProvider.getMIMETypes(); + } + + @Override + public boolean isStandardStreamMetadataFormatSupported() { + return delegateProvider.isStandardStreamMetadataFormatSupported(); + } + + @Override + public String getNativeStreamMetadataFormatName() { + return delegateProvider.getNativeStreamMetadataFormatName(); + } + + @Override + public String[] getExtraStreamMetadataFormatNames() { + return delegateProvider.getExtraStreamMetadataFormatNames(); + } + + @Override + public boolean isStandardImageMetadataFormatSupported() { + return delegateProvider.isStandardImageMetadataFormatSupported(); + } + + @Override + public String getNativeImageMetadataFormatName() { + return delegateProvider.getNativeImageMetadataFormatName(); + } + + @Override + public String[] getExtraImageMetadataFormatNames() { + return delegateProvider.getExtraImageMetadataFormatNames(); + } + + @Override + public IIOMetadataFormat getStreamMetadataFormat(String formatName) { + return delegateProvider.getStreamMetadataFormat(formatName); + } + + @Override + public IIOMetadataFormat getImageMetadataFormat(String formatName) { + return delegateProvider.getImageMetadataFormat(formatName); + } + + @Override + public boolean canEncodeImage(ImageTypeSpecifier type) { + return delegateProvider.canEncodeImage(type); + } + + @Override + public boolean canEncodeImage(RenderedImage im) { + return delegateProvider.canEncodeImage(im); + } + + @Override + public String getDescription(Locale locale) { + return delegateProvider.getDescription(locale); + } + + @Override + public boolean isFormatLossless() { + return delegateProvider.isFormatLossless(); + } + + @Override + public Class[] getOutputTypes() { + return delegateProvider.getOutputTypes(); + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java new file mode 100644 index 00000000..87dd6004 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageInputStreamImpl; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * JPEGSegmentImageInputStream. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGSegmentImageInputStream.java,v 1.0 30.01.12 16:15 haraldk Exp$ + */ +final class JPEGSegmentImageInputStream extends ImageInputStreamImpl { + // TODO: Rewrite JPEGSegment (from metadata) to store stream pos/length, and be able to replay data, and use instead of Segment? + // TODO: Change order of segments, to make sure APP0/JFIF is always before APP14/Adobe? + // TODO: Insert fake APP0/JFIF if needed by the reader? + + final private ImageInputStream stream; + + private final List segments = new ArrayList(64); + private int currentSegment = -1; + private Segment segment; + + JPEGSegmentImageInputStream(final ImageInputStream stream) { + this.stream = notNull(stream, "stream"); + } + + private Segment fetchSegment() throws IOException { + // Stream init + if (currentSegment == -1) { + streamInit(); + } + else { + segment = segments.get(currentSegment); + } + + if (streamPos >= segment.end()) { + // Go forward in cache + while (++currentSegment < segments.size()) { + segment = segments.get(currentSegment); + + if (streamPos >= segment.start && streamPos < segment.end()) { + stream.seek(segment.realStart + streamPos - segment.start); + + return segment; + } + } + + stream.seek(segment.realEnd()); + + // Scan forward + while (true) { + long realPosition = stream.getStreamPosition(); + int marker = stream.readUnsignedShort(); + + // TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment, 'Exif' APP1 and very few others + if (isAppSegmentMarker(marker) && marker != JPEG.APP0 && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) { + int length = stream.readUnsignedShort(); // Length including length field itself + stream.seek(realPosition + 2 + length); // Skip marker (2) + length + } + else { + if (marker == JPEG.EOI) { + segment = new Segment(marker, realPosition, segment.end(), 2); + segments.add(segment); + } + else { + long length; + + if (marker == JPEG.SOS) { + // Treat rest of stream as a single segment (scanning for EOI is too much work) + length = Long.MAX_VALUE - realPosition; + } + else { + // Length including length field itself + length = stream.readUnsignedShort() + 2; + } + + segment = new Segment(marker, realPosition, segment.end(), length); + segments.add(segment); + } + + currentSegment = segments.size() - 1; + + if (streamPos >= segment.start && streamPos < segment.end()) { + stream.seek(segment.realStart + streamPos - segment.start); + + break; + } + else { + stream.seek(segment.realEnd()); + // Else continue forward scan + } + } + } + } + else if (streamPos < segment.start) { + // Go back in cache + while (--currentSegment >= 0) { + segment = segments.get(currentSegment); + + if (streamPos >= segment.start && streamPos < segment.end()) { + stream.seek(segment.realStart + streamPos - segment.start); + + break; + } + } + } + else { + stream.seek(segment.realStart + streamPos - segment.start); + } + + return segment; + } + + private static boolean isAppSegmentWithId(String segmentId, ImageInputStream stream) throws IOException { + notNull(segmentId, "segmentId"); + + stream.mark(); + + try { + int length = stream.readUnsignedShort(); // Length including length field itself + + byte[] data = new byte[Math.max(20, length - 2)]; + stream.readFully(data); + + return segmentId.equals(asNullTerminatedAsciiString(data, 0)); + } + finally { + stream.reset(); + } + } + + static String asNullTerminatedAsciiString(final byte[] data, final int offset) { + for (int i = 0; i < data.length - offset; i++) { + if (data[offset + i] == 0 || i > 255) { + return asAsciiString(data, offset, offset + i); + } + } + + return null; + } + + static String asAsciiString(final byte[] data, final int offset, final int length) { + return new String(data, offset, length, Charset.forName("ascii")); + } + + private void streamInit() throws IOException { + stream.seek(0); + + int soi = stream.readUnsignedShort(); + if (soi != JPEG.SOI) { + throw new IIOException(String.format("Not a JPEG stream (starts with: 0x%04x, expected SOI: 0x%04x)", soi, JPEG.SOI)); + } + else { + segment = new Segment(soi, 0, 0, 2); + + segments.add(segment); + currentSegment = segments.size() - 1; // 0 + } + } + + static boolean isAppSegmentMarker(final int marker) { + return marker >= JPEG.APP0 && marker <= JPEG.APP15; + } + + private void repositionAsNecessary() throws IOException { + if (segment == null || streamPos < segment.start || streamPos >= segment.end()) { + fetchSegment(); + } + } + + @Override + public int read() throws IOException { + bitOffset = 0; + + repositionAsNecessary(); + + int read = stream.read(); + + if (read != -1) { + streamPos++; + } + + return read; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + bitOffset = 0; + + // NOTE: There is a bug in the JPEGMetadata constructor (JPEGBuffer.loadBuf() method) that expects read to + // always read len bytes. Therefore, this is more complicated than it needs to... :-/ + int total = 0; + + while (total < len) { + repositionAsNecessary(); + + int count = stream.read(b, off + total, (int) Math.min(len - total, segment.end() - streamPos)); + + if (count == -1) { + // EOF + if (total == 0) { + return -1; + } + + break; + } + else { + streamPos += count; + total += count; + } + } + + return total; + } + + @SuppressWarnings({"FinalizeDoesntCallSuperFinalize"}) + @Override + protected void finalize() throws Throwable { + // Empty finalizer (for improved performance; no need to call super.finalize() in this case) + } + + static class Segment { + private final int marker; + + final long realStart; + final long start; + final long length; + + Segment(int marker, long realStart, long start, long length) { + this.marker = marker; + this.realStart = realStart; + this.start = start; + this.length = length; + } + + long realEnd() { + return realStart + length; + } + + long end() { + return start + length; + } + + @Override + public String toString() { + return String.format("0x%04x[%d-%d]", marker, realStart, realEnd()); + } + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java new file mode 100644 index 00000000..b6b201ed --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +/** + * ThumbnailReadProgressListener + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ThumbnailReadProgressListener.java,v 1.0 07.05.12 10:15 haraldk Exp$ + */ +interface ThumbnailReadProgressListener { + void processThumbnailStarted(int imageIndex, int thumbnailIndex); + + void processThumbnailProgress(float percentageDone); + + void processThumbnailComplete(); +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java new file mode 100644 index 00000000..6354ef93 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.io.IOException; +import java.io.InputStream; + +/** + * ThumbnailReader + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ThumbnailReader.java,v 1.0 18.04.12 12:22 haraldk Exp$ + */ +abstract class ThumbnailReader { + + private final ThumbnailReadProgressListener progressListener; + protected final int imageIndex; + protected final int thumbnailIndex; + + protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) { + this.progressListener = progressListener; + this.imageIndex = imageIndex; + this.thumbnailIndex = thumbnailIndex; + } + protected final void processThumbnailStarted() { + progressListener.processThumbnailStarted(imageIndex, thumbnailIndex); + } + + protected final void processThumbnailProgress(float percentageDone) { + progressListener.processThumbnailProgress(percentageDone); + } + + protected final void processThumbnailComplete() { + progressListener.processThumbnailComplete(); + } + + static protected BufferedImage readJPEGThumbnail(InputStream stream) throws IOException { + return ImageIO.read(stream); + } + + static protected BufferedImage readRawThumbnail(final byte[] thumbnail, final int size, final int offset, int w, int h) { + DataBufferByte buffer = new DataBufferByte(thumbnail, size, offset); + WritableRaster raster = Raster.createInterleavedRaster(buffer, w, h, w * 3, 3, new int[] {0, 1, 2}, null); + ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + + return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + } + + public abstract BufferedImage read() throws IOException; + + public abstract int getWidth() throws IOException; + + public abstract int getHeight() throws IOException; +} diff --git a/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100644 index 00000000..7ae9b24a --- /dev/null +++ b/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi diff --git a/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi new file mode 100644 index 00000000..b15b4f60 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java new file mode 100644 index 00000000..9cd65353 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertNotNull; + +/** + * AbstractThumbnailReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractThumbnailReaderTest.java,v 1.0 04.05.12 15:55 haraldk Exp$ + */ +public abstract class AbstractThumbnailReaderTest { + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + } + + protected abstract ThumbnailReader createReader( + ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream + ) throws IOException; + + protected final ImageInputStream createStream(final String name) throws IOException { + URL resource = getClass().getResource(name); + ImageInputStream stream = ImageIO.createImageInputStream(resource); + assertNotNull("Could not create stream for resource " + resource, stream); + return stream; + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java new file mode 100644 index 00000000..f1b92e27 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import org.junit.Test; +import org.mockito.InOrder; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * EXIFThumbnailReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: EXIFThumbnailReaderTest.java,v 1.0 04.05.12 15:55 haraldk Exp$ + */ +public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { + + @Override + protected EXIFThumbnailReader createReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final ImageInputStream stream) throws IOException { + List segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif"); + stream.close(); + + assertNotNull(segments); + assertFalse(segments.isEmpty()); + + EXIFReader reader = new EXIFReader(); + InputStream data = segments.get(0).data(); + if (data.read() < 0) { + throw new AssertionError("EOF!"); + } + + ImageInputStream exifStream = ImageIO.createImageInputStream(data); + CompoundDirectory ifds = (CompoundDirectory) reader.read(exifStream); + + assertEquals(2, ifds.directoryCount()); + + return new EXIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream); + } + + @Test + public void testReadJPEG() throws IOException { + ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg")); + + assertEquals(114, reader.getWidth()); + assertEquals(160, reader.getHeight()); + + BufferedImage thumbnail = reader.read(); + assertNotNull(thumbnail); + assertEquals(114, thumbnail.getWidth()); + assertEquals(160, thumbnail.getHeight()); + } + + @Test + public void testReadRaw() throws IOException { + ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg")); + + assertEquals(80, reader.getWidth()); + assertEquals(60, reader.getHeight()); + + BufferedImage thumbnail = reader.read(); + assertNotNull(thumbnail); + assertEquals(80, thumbnail.getWidth()); + assertEquals(60, thumbnail.getHeight()); + } + + @Test + public void testProgressListenerJPEG() throws IOException { + ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class); + + createReader(listener, 42, 43, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg")).read(); + + InOrder order = inOrder(listener); + order.verify(listener).processThumbnailStarted(42, 43); + order.verify(listener, atLeastOnce()).processThumbnailProgress(100f); + order.verify(listener).processThumbnailComplete(); + } + + @Test + public void testProgressListenerRaw() throws IOException { + ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class); + + createReader(listener, 0, 99, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg")).read(); + + InOrder order = inOrder(listener); + order.verify(listener).processThumbnailStarted(0, 99); + order.verify(listener, atLeastOnce()).processThumbnailProgress(100f); + order.verify(listener).processThumbnailComplete(); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java new file mode 100644 index 00000000..9f8bb46d --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import org.junit.Test; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * FastCMYKToRGBTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FastCMYKToRGBTest.java,v 1.0 22.02.11 16.22 haraldk Exp$ + */ +public class FastCMYKToRGBTest { + @Test + public void testCreate() { + new FastCMYKToRGB(); + } + + @Test + public void testConvertByteRGBWhite() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = convert.filter(input, null); + byte[] pixel = (byte[]) result.getDataElements(0, 0, null); + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) 255, (byte) 255, (byte) 255}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + + @Test + public void testConvertIntRGBWhite() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = convert.filter(input, new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getRaster()); + int[] pixel = (int[]) result.getDataElements(0, 0, null); + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = 0xFFFFFF; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + + @Test + public void testConvertByteRGBBlack() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i), (byte) 255}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) 0, (byte) 0, (byte) 0}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertIntRGBBlack() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i), (byte) 255}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = 0x0; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + } + + @Test + public void testConvertByteRGBColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertByteBGRColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(3, pixel.length); + byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertByteABGRColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR).getRaster(); + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(4, pixel.length); + byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i), (byte) 0xff}; + assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + } + } + + @Test + public void testConvertIntRGBColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = (((byte) (255 - i)) & 0xFF) << 16 | (((byte) i) & 0xFF) << 8 | ((byte) (127 - i)) & 0xFF; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + } + + @Test + public void testConvertIntBGRColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_BGR).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = (((byte) (127 - i)) & 0xFF) << 16 | (((byte) i) & 0xFF) << 8 | ((byte) (255 - i)) & 0xFF; + int rgb = pixel[0] & 0xFFFFFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", rgb, expected), expected, rgb); + } + } + + @Test + public void testConvertIntARGBColors() { + FastCMYKToRGB convert = new FastCMYKToRGB(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getRaster(); + int[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[]{(byte) i, (byte) (255 - i), (byte) (128 + i), 0}); + result = convert.filter(input, result); + pixel = (int[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + int expected = 0xFF << 24 | (((byte) (255 - i)) & 0xFF) << 16 | (((byte) i) & 0xFF) << 8 | ((byte) (127 - i)) & 0xFF; + assertEquals(String.format("Was: 0x%08x, expected: 0x%08x", pixel[0], expected), expected, pixel[0]); + } + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java new file mode 100644 index 00000000..77b69653 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import org.junit.Test; +import org.mockito.InOrder; + +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * JFIFThumbnailReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JFIFThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$ + */ +public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest { + @Override + protected JFIFThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException { + List segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFIF"); + stream.close(); + + assertNotNull(segments); + assertFalse(segments.isEmpty()); + + return new JFIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFIFSegment.read(segments.get(0).data())); + } + + @Test + public void testReadRaw() throws IOException { + ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg")); + + assertEquals(131, reader.getWidth()); + assertEquals(122, reader.getHeight()); + + BufferedImage thumbnail = reader.read(); + assertNotNull(thumbnail); + assertEquals(131, thumbnail.getWidth()); + assertEquals(122, thumbnail.getHeight()); + } + + @Test + public void testProgressListenerRaw() throws IOException { + ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class); + + createReader(listener, 0, 99, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg")).read(); + + InOrder order = inOrder(listener); + order.verify(listener).processThumbnailStarted(0, 99); + order.verify(listener, atLeastOnce()).processThumbnailProgress(100f); + order.verify(listener).processThumbnailComplete(); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java new file mode 100644 index 00000000..4f8db3ff --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import org.junit.Test; +import org.mockito.InOrder; + +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * JFXXThumbnailReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JFXXThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$ + */ +public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest { + @Override + protected JFXXThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException { + List segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX"); + stream.close(); + + assertNotNull(segments); + assertFalse(segments.isEmpty()); + + JPEGSegment jfxx = segments.get(0); + return new JFXXThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length())); + } + + @Test + public void testReadJPEG() throws IOException { + ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")); + + assertEquals(80, reader.getWidth()); + assertEquals(60, reader.getHeight()); + + BufferedImage thumbnail = reader.read(); + assertNotNull(thumbnail); + assertEquals(80, thumbnail.getWidth()); + assertEquals(60, thumbnail.getHeight()); + } + + // TODO: Test JFXX indexed thumbnail + // TODO: Test JFXX RGB thumbnail + + @Test + public void testProgressListenerRaw() throws IOException { + ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class); + + createReader(listener, 0, 99, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")).read(); + + InOrder order = inOrder(listener); + order.verify(listener).processThumbnailStarted(0, 99); + order.verify(listener, atLeastOnce()).processThumbnailProgress(100f); + order.verify(listener).processThumbnailComplete(); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java new file mode 100644 index 00000000..6fa34246 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -0,0 +1,602 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * JPEGImageReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImageReaderTest.java,v 1.0 24.01.11 22.04 haraldk Exp$ + */ +public class JPEGImageReaderTest extends ImageReaderAbstractTestCase { + + private static final JPEGImageReaderSpi SPI = new JPEGImageReaderSpi(lookupDelegateProvider()); + + private static ImageReaderSpi lookupDelegateProvider() { + return JPEGImageReaderSpi.lookupDelegateProvider(IIORegistry.getDefaultInstance()); + } + + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/jpeg/cmm-exception-adobe-rgb.jpg"), new Dimension(626, 76)), + new TestData(getClassLoaderResource("/jpeg/cmm-exception-srgb.jpg"), new Dimension(1800, 1200)), + new TestData(getClassLoaderResource("/jpeg/gray-sample.jpg"), new Dimension(386, 396)), + new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(160, 227)), + new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804)), + new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480)) + ); + + // More test data in specific tests below + } + + @Override + protected ImageReaderSpi createProvider() { + return SPI; + } + + @Override + protected JPEGImageReader createReader() { + try { + return (JPEGImageReader) SPI.createReaderInstance(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings({"unchecked"}) + @Override + protected Class getReaderClass() { + return JPEGImageReader.class; + } + + @Override + protected boolean allowsNullRawImageType() { + return true; + } + + @Override + protected List getFormatNames() { + return Arrays.asList("JPEG", "jpeg", "JPG", "jpg"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("jpeg", "jpg"); + } + + @Override + protected List getMIMETypes() { + return Arrays.asList("image/jpeg"); + } + + // TODO: Test that subsampling is actually reading something + + // Special cases found in the wild below + + @Test + public void testICCProfileCMYKClassOutputColors() throws IOException { + // Make sure ICC profile with class output isn't converted to too bright values + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmyk-sample-custom-icc-bright.jpg"))); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(800, 800, 64, 8)); + param.setSourceSubsampling(8, 8, 1, 1); + + BufferedImage image = reader.read(0, param); + assertNotNull(image); + + byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + byte[] expectedData = {34, 37, 34, 47, 47, 44, 22, 26, 28, 23, 26, 28, 20, 23, 26, 20, 22, 25, 22, 25, 27, 18, 21, 24}; + + assertEquals(expectedData.length, data.length); + + assertJPEGPixelsEqual(expectedData, data, 0); + + reader.dispose(); + } + + private static void assertJPEGPixelsEqual(byte[] expected, byte[] actual, int actualOffset) { + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actual[i + actualOffset], 5); + } + } + + @Test + public void testICCDuplicateSequence() throws IOException { + // Variation of the above, file contains multiple ICC chunks, with all counts and sequence numbers == 1 + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg"))); + + assertEquals(345, reader.getWidth(0)); + assertEquals(540, reader.getHeight(0)); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertEquals(345, image.getWidth()); + assertEquals(540, image.getHeight()); + + reader.dispose(); + } + + @Test + public void testICCDuplicateSequenceZeroBased() throws IOException { + // File contains multiple ICC chunks, with all counts and sequence numbers == 0 + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg"))); + + assertEquals(3874, reader.getWidth(0)); + assertEquals(5480, reader.getHeight(0)); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(0, 0, 3874, 16)); // Save some memory + BufferedImage image = reader.read(0, param); + + assertNotNull(image); + assertEquals(3874, image.getWidth()); + assertEquals(16, image.getHeight()); + + reader.dispose(); + } + + @Test + public void testTruncatedICCProfile() throws IOException { + // File contains single 'ICC_PROFILE' chunk, with a truncated (32 000 bytes) "Europe ISO Coated FOGRA27" ICC profile (by Adobe). + // Profile should have been about 550 000 bytes, split into multiple chunks. Written by GIMP 2.6.11 + // See: https://bugzilla.redhat.com/show_bug.cgi?id=695246 + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-invalid-icc-profile-data.jpg"))); + + assertEquals(1993, reader.getWidth(0)); + assertEquals(1038, reader.getHeight(0)); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(reader.getWidth(0), 8)); + BufferedImage image = reader.read(0, param); + + assertNotNull(image); + assertEquals(1993, image.getWidth()); + assertEquals(8, image.getHeight()); + + reader.dispose(); + } + + @Test + public void testCCOIllegalArgument() throws IOException { + // File contains CMYK ICC profile ("Coated FOGRA27 (ISO 12647-2:2004)"), but image data is 3 channel YCC/RGB + // JFIF 1.1 with unknown origin. + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cco-illegalargument-rgb-coated-fogra27.jpg"))); + + assertEquals(281, reader.getWidth(0)); + assertEquals(449, reader.getHeight(0)); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertEquals(281, image.getWidth()); + assertEquals(449, image.getHeight()); + + // TODO: Need to test colors! + reader.dispose(); + } + + @Test + public void testNoImageTypesRGBWithCMYKProfile() throws IOException { + // File contains CMYK ICC profile ("U.S. Web Coated (SWOP) v2") AND Adobe App14 specifying YCCK conversion (!), + // but image data is plain 3 channel YCC/RGB. + // EXIF/TIFF metadata says Software: "Microsoft Windows Photo Gallery 6.0.6001.18000"... + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg"))); + + assertEquals(1743, reader.getWidth(0)); + assertEquals(2551, reader.getHeight(0)); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(0, 0, 1743, 16)); // Save some memory + BufferedImage image = reader.read(0, param); + + assertNotNull(image); + assertEquals(1743, image.getWidth()); + assertEquals(16, image.getHeight()); + + // TODO: Need to test colors! + + assertTrue(reader.hasThumbnails(0)); // Should not blow up! + } + + @Test + public void testWarningEmbeddedColorProfileInvalidIgnored() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/warning-embedded-color-profile-invalid-ignored-cmyk.jpg"))); + + assertEquals(183, reader.getWidth(0)); + assertEquals(283, reader.getHeight(0)); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertEquals(183, image.getWidth()); + assertEquals(283, image.getHeight()); + + // TODO: Need to test colors! + } + + @Test + public void testEOFSOSSegment() throws IOException { + // Regression... + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/eof-sos-segment-bug.jpg"))); + + assertEquals(266, reader.getWidth(0)); + assertEquals(400, reader.getHeight(0)); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertEquals(266, image.getWidth()); + assertEquals(400, image.getHeight()); + } + + @Test + public void testInvalidICCSingleChunkBadSequence() throws IOException { + // Regression + // Single segment ICC profile, with chunk index/count == 0 + + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-single-chunk-bad-sequence-number.jpg"))); + + assertEquals(1772, reader.getWidth(0)); + assertEquals(2126, reader.getHeight(0)); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(reader.getWidth(0), 8)); + + IIOReadWarningListener warningListener = mock(IIOReadWarningListener.class); + reader.addIIOReadWarningListener(warningListener); + + BufferedImage image = reader.read(0, param); + + assertNotNull(image); + assertEquals(1772, image.getWidth()); + assertEquals(8, image.getHeight()); + + verify(warningListener).warningOccurred(eq(reader), anyString()); + } + + @Test + public void testHasThumbnailNoIFD1() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/srgb-exif-no-ifd1.jpg"))); + + assertEquals(150, reader.getWidth(0)); + assertEquals(207, reader.getHeight(0)); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertEquals(150, image.getWidth()); + assertEquals(207, image.getHeight()); + + assertFalse(reader.hasThumbnails(0)); // Should just not blow up, even if the EXIF IFD1 is missing + } + + @Test + public void testJFIFRawRGBThumbnail() throws IOException { + // JFIF with raw RGB thumbnail (+ EXIF thumbnail) + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg"))); + + assertTrue(reader.hasThumbnails(0)); + assertEquals(2, reader.getNumThumbnails(0)); + + // RAW JFIF + assertEquals(131, reader.getThumbnailWidth(0, 0)); + assertEquals(122, reader.getThumbnailHeight(0, 0)); + + BufferedImage rawJFIFThumb = reader.readThumbnail(0, 0); + assertNotNull(rawJFIFThumb); + assertEquals(131, rawJFIFThumb.getWidth()); + assertEquals(122, rawJFIFThumb.getHeight()); + + // Exif (old thumbnail, from original image, should probably been removed by the software...) + assertEquals(160, reader.getThumbnailWidth(0, 1)); + assertEquals(120, reader.getThumbnailHeight(0, 1)); + + BufferedImage exifThumb = reader.readThumbnail(0, 1); + assertNotNull(exifThumb); + assertEquals(160, exifThumb.getWidth()); + assertEquals(120, exifThumb.getHeight()); + } + + // TODO: Test JFXX indexed thumbnail + // TODO: Test JFXX RGB thumbnail + + @Test + public void testJFXXJPEGThumbnail() throws IOException { + // JFIF with JFXX JPEG encoded thumbnail + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"))); + + assertTrue(reader.hasThumbnails(0)); + assertEquals(1, reader.getNumThumbnails(0)); + assertEquals(80, reader.getThumbnailWidth(0, 0)); + assertEquals(60, reader.getThumbnailHeight(0, 0)); + + BufferedImage thumbnail = reader.readThumbnail(0, 0); + assertNotNull(thumbnail); + assertEquals(80, thumbnail.getWidth()); + assertEquals(60, thumbnail.getHeight()); + } + + @Test + public void testEXIFJPEGThumbnail() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"))); + + assertTrue(reader.hasThumbnails(0)); + assertEquals(1, reader.getNumThumbnails(0)); + assertEquals(114, reader.getThumbnailWidth(0, 0)); + assertEquals(160, reader.getThumbnailHeight(0, 0)); + + BufferedImage thumbnail = reader.readThumbnail(0, 0); + assertNotNull(thumbnail); + assertEquals(114, thumbnail.getWidth()); + assertEquals(160, thumbnail.getHeight()); + } + + @Test + public void testEXIFRawThumbnail() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-rgb-thumbnail-sony-d700.jpg"))); + + assertTrue(reader.hasThumbnails(0)); + assertEquals(1, reader.getNumThumbnails(0)); + assertEquals(80, reader.getThumbnailWidth(0, 0)); + assertEquals(60, reader.getThumbnailHeight(0, 0)); + + BufferedImage thumbnail = reader.readThumbnail(0, 0); + assertNotNull(thumbnail); + assertEquals(80, thumbnail.getWidth()); + assertEquals(60, thumbnail.getHeight()); + } + + @Test + public void testBadEXIFRawThumbnail() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg"))); + + assertTrue(reader.hasThumbnails(0)); + assertEquals(1, reader.getNumThumbnails(0)); + assertEquals(96, reader.getThumbnailWidth(0, 0)); + assertEquals(72, reader.getThumbnailHeight(0, 0)); + + BufferedImage thumbnail = reader.readThumbnail(0, 0); + assertNotNull(thumbnail); + assertEquals(96, thumbnail.getWidth()); + assertEquals(72, thumbnail.getHeight()); + } + + @Test + public void testInvertedColors() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg"))); + + assertEquals(2437, reader.getWidth(0)); + assertEquals(1662, reader.getHeight(0)); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(0, 0, reader.getWidth(0), 8)); + BufferedImage strip = reader.read(0, param); + + assertNotNull(strip); + assertEquals(2437, strip.getWidth()); + assertEquals(8, strip.getHeight()); + + int[] expectedRGB = new int[] { + 0xffe9d0bc, 0xfff3decd, 0xfff5e6d3, 0xfff8ecdc, 0xfff8f0e5, 0xffe3ceb9, 0xff6d3923, 0xff5a2d18, + 0xff00170b, 0xff131311, 0xff52402c, 0xff624a30, 0xff6a4f34, 0xfffbf8f1, 0xfff4efeb, 0xffefeae6, + 0xffebe6e2, 0xffe3e0d9, 0xffe1d6d0, 0xff10100e + }; + + // Validate strip colors + for (int i = 0; i < strip.getWidth() / 128; i++) { + int actualRGB = strip.getRGB(i * 128, 4); + assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5); + assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5); + assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5); + } + } + + @Test + public void testThumbnailInvertedColors() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg"))); + + assertTrue(reader.hasThumbnails(0)); + assertEquals(1, reader.getNumThumbnails(0)); + assertEquals(160, reader.getThumbnailWidth(0, 0)); + assertEquals(109, reader.getThumbnailHeight(0, 0)); + + BufferedImage thumbnail = reader.readThumbnail(0, 0); + assertNotNull(thumbnail); + assertEquals(160, thumbnail.getWidth()); + assertEquals(109, thumbnail.getHeight()); + + int[] expectedRGB = new int[] { + 0xffefd5c4, 0xffead3b1, 0xff55392d, 0xff55403b, 0xff6d635a, 0xff7b726b, 0xff68341f, 0xff5c2f1c, + 0xff250f12, 0xff6d7c77, 0xff414247, 0xff6a4f3a, 0xff6a4e39, 0xff564438, 0xfffcf7f1, 0xffefece7, + 0xfff0ebe7, 0xff464040, 0xffe3deda, 0xffd4cfc9, + }; + + // Validate strip colors + for (int i = 0; i < thumbnail.getWidth() / 8; i++) { + int actualRGB = thumbnail.getRGB(i * 8, 4); + assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5); + assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5); + assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5); + } + } + + private List getCMYKData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(100, 100)), + new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(100, 100)), + new TestData(getClassLoaderResource("/jpeg/cmyk-sample-custom-icc-bright.jpg"), new Dimension(100, 100)), + new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100)) + ); + } + + @Test + public void testGetImageTypesCMYK() throws IOException { + // Make sure CMYK images will report their embedded color profile among image types + JPEGImageReader reader = createReader(); + + List cmykData = getCMYKData(); + + for (TestData data : cmykData) { + reader.setInput(data.getInputStream()); + Iterator types = reader.getImageTypes(0); + + assertTrue(data + " has no image types", types.hasNext()); + + boolean hasRGBType = false; + boolean hasCMYKType = false; + + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + + int csType = type.getColorModel().getColorSpace().getType(); + if (csType == ColorSpace.TYPE_RGB) { + hasRGBType = true; + } + else if (csType == ColorSpace.TYPE_CMYK) { + assertTrue("CMYK types should be delivered after RGB types (violates \"contract\" of more \"natural\" type first) for " + data, hasRGBType); + + hasCMYKType = true; + break; + } + } + + assertTrue("No RGB types for " + data, hasRGBType); + assertTrue("No CMYK types for " + data, hasCMYKType); + } + + reader.dispose(); + } + + @Test + public void testGetRawImageTypeCMYK() throws IOException { + // Make sure images that are encoded as CMYK (not YCCK) actually return non-null for getRawImageType + JPEGImageReader reader = createReader(); + + List cmykData = Arrays.asList( + new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(100, 100)), + new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100)) + ); + + + for (TestData data : cmykData) { + reader.setInput(data.getInputStream()); + + ImageTypeSpecifier rawType = reader.getRawImageType(0); + assertNotNull("No raw type for " + data, rawType); + } + } + + @Test + public void testReadCMYKAsCMYK() throws IOException { + // Make sure CMYK images can be read and still contain their original (embedded) color profile + JPEGImageReader reader = createReader(); + + List cmykData = getCMYKData(); + + for (TestData data : cmykData) { + reader.setInput(data.getInputStream()); + Iterator types = reader.getImageTypes(0); + + assertTrue(data + " has no image types", types.hasNext()); + + ImageTypeSpecifier cmykType = null; + + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + + int csType = type.getColorModel().getColorSpace().getType(); + if (csType == ColorSpace.TYPE_CMYK) { + cmykType = type; + break; + } + } + + assertNotNull("No CMYK types for " + data, cmykType); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setDestinationType(cmykType); + param.setSourceRegion(new Rectangle(reader.getWidth(0), 8)); // We don't really need to read it all + + BufferedImage image = reader.read(0, param); + + assertNotNull(image); + assertEquals(ColorSpace.TYPE_CMYK, image.getColorModel().getColorSpace().getType()); + } + + reader.dispose(); + } + + // TODO: Test RGBA/YCbCrA handling +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterTest.java new file mode 100644 index 00000000..0fbe8bc0 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageWriterTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; + +import javax.imageio.ImageWriter; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageWriterSpi; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * JPEGImageWriterTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImageWriterTest.java,v 1.0 06.02.12 17:05 haraldk Exp$ + */ +public class JPEGImageWriterTest extends ImageWriterAbstractTestCase { + + private static final JPEGImageWriterSpi SPI = new JPEGImageWriterSpi(lookupDelegateProvider()); + + private static ImageWriterSpi lookupDelegateProvider() { + return JPEGImageWriterSpi.lookupDelegateProvider(IIORegistry.getDefaultInstance()); + } + + @Override + protected ImageWriter createImageWriter() { + try { + return SPI.createWriterInstance(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected List getTestData() { + return Arrays.asList( + new BufferedImage(320, 200, BufferedImage.TYPE_3BYTE_BGR), + new BufferedImage(32, 20, BufferedImage.TYPE_INT_RGB), + new BufferedImage(32, 20, BufferedImage.TYPE_INT_BGR), + new BufferedImage(32, 20, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(32, 20, BufferedImage.TYPE_4BYTE_ABGR), + new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_GRAY) + ); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java new file mode 100644 index 00000000..762e7484 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; +import org.junit.Test; +import org.mockito.internal.matchers.LessOrEqual; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * JPEGSegmentImageInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGSegmentImageInputStreamTest.java,v 1.0 30.01.12 22:14 haraldk Exp$ + */ +public class JPEGSegmentImageInputStreamTest { + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + } + + protected URL getClassLoaderResource(final String pName) { + return getClass().getResource(pName); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNull() { + new JPEGSegmentImageInputStream(null); + } + + @Test(expected = IIOException.class) + public void testStreamNonJPEG() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[] {42, 42, 0, 0, 77, 99}))); + stream.read(); + } + + @Test + public void testStreamRealData() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg"))); + assertEquals(JPEG.SOI, stream.readUnsignedShort()); + assertEquals(JPEG.APP0, stream.readUnsignedShort()); + } + + @Test + public void testStreamRealDataArray() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg"))); + byte[] bytes = new byte[20]; + + // NOTE: read(byte[], int, int) must always read len bytes (or until EOF), due to known bug in Sun code + assertEquals(20, stream.read(bytes, 0, 20)); + + assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, 0x0, 0x10, 'J', 'F', 'I', 'F', 0x0, 0x1, 0x1, 0x1, 0x1, (byte) 0xCC, 0x1, (byte) 0xCC, 0, 0}, bytes); + } + + @Test + public void testStreamRealDataLength() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-adobe-rgb.jpg"))); + + long length = 0; + while (stream.read() != -1) { + length++; + } + + assertThat(length, new LessOrEqual(10203l)); // In no case should length increase + + assertEquals(9625l, length); // May change, if more chunks are passed to reader... + } + + @Test + public void testAppSegmentsFiltering() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg"))); + List appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS); + + assertEquals(3, appSegments.size()); + + assertEquals(JPEG.APP0, appSegments.get(0).marker()); + assertEquals("JFIF", appSegments.get(0).identifier()); + + assertEquals(JPEG.APP1, appSegments.get(1).marker()); + assertEquals("Exif", appSegments.get(1).identifier()); + + assertEquals(JPEG.APP14, appSegments.get(2).marker()); + assertEquals("Adobe", appSegments.get(2).identifier()); + + // And thus, no XMP, no ICC_PROFILE or other segments + } + + @Test + public void testEOFSOSSegmentBug() throws IOException { + ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/eof-sos-segment-bug.jpg"))); + + long length = 0; + while (stream.read() != -1) { + length++; + } + + assertEquals(9299l, length); // Sanity check: same as file size + } +} diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cco-illegalargument-rgb-coated-fogra27.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cco-illegalargument-rgb-coated-fogra27.jpg new file mode 100644 index 00000000..6bf57a00 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cco-illegalargument-rgb-coated-fogra27.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-adobe-rgb.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-adobe-rgb.jpg new file mode 100644 index 00000000..72dcdc69 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-adobe-rgb.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-invalid-icc-profile-data.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-invalid-icc-profile-data.jpg new file mode 100644 index 00000000..a34fd4ee Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-invalid-icc-profile-data.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-srgb.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-srgb.jpg new file mode 100644 index 00000000..0ea63d8b Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-srgb.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-custom-icc-bright.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-custom-icc-bright.jpg new file mode 100644 index 00000000..aac34aa4 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-custom-icc-bright.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-multiple-chunk-icc.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-multiple-chunk-icc.jpg new file mode 100644 index 00000000..1e35e3b8 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-multiple-chunk-icc.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-no-icc.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-no-icc.jpg new file mode 100644 index 00000000..3e4e7587 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-no-icc.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample.jpg new file mode 100644 index 00000000..295234be Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/eof-sos-segment-bug.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/eof-sos-segment-bug.jpg new file mode 100644 index 00000000..c1d2d973 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/eof-sos-segment-bug.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg new file mode 100644 index 00000000..90e18f08 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg new file mode 100644 index 00000000..6ad31099 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/exif-rgb-thumbnail-sony-d700.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-rgb-thumbnail-sony-d700.jpg new file mode 100644 index 00000000..907bdb08 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-rgb-thumbnail-sony-d700.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/fair-use.txt b/imageio/imageio-jpeg/src/test/resources/jpeg/fair-use.txt new file mode 100644 index 00000000..0321e685 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/resources/jpeg/fair-use.txt @@ -0,0 +1,5 @@ +The test files in this folder may contain copyrighted artwork. However, I believe that using them for test purposes +(without actually displaying the artwork) must be considered fair use. +If you disagree for any reason, please send me a note, and I will remove your image from the distribution. + + -- harald.kuhr@gmail.com diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/gray-sample.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/gray-sample.jpg new file mode 100644 index 00000000..a20a5899 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/gray-sample.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg new file mode 100644 index 00000000..7cd11dbf Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg new file mode 100644 index 00000000..f63b983c Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-single-chunk-bad-sequence-number.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-single-chunk-bad-sequence-number.jpg new file mode 100644 index 00000000..b76f3a2a Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/invalid-icc-single-chunk-bad-sequence-number.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg new file mode 100644 index 00000000..b3cb336b Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg new file mode 100644 index 00000000..58171a29 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg new file mode 100644 index 00000000..47c09e61 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/srgb-exif-no-ifd1.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/srgb-exif-no-ifd1.jpg new file mode 100644 index 00000000..82241293 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/srgb-exif-no-ifd1.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/warning-embedded-color-profile-invalid-ignored-cmyk.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/warning-embedded-color-profile-invalid-ignored-cmyk.jpg new file mode 100644 index 00000000..5c46086c Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/warning-embedded-color-profile-invalid-ignored-cmyk.jpg differ diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractCompoundDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractCompoundDirectory.java new file mode 100644 index 00000000..6cd35e0b --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractCompoundDirectory.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import static com.twelvemonkeys.lang.Validate.noNullElements; + +/** + * AbstractCompoundDirectory + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractCompoundDirectory.java,v 1.0 02.01.12 12:43 haraldk Exp$ + */ +public abstract class AbstractCompoundDirectory extends AbstractDirectory implements CompoundDirectory { + private final List directories = new ArrayList(); + + protected AbstractCompoundDirectory(final Collection directories) { + super(null); + + if (directories != null) { + this.directories.addAll(noNullElements(directories)); + } + } + + public Directory getDirectory(int index) { + return directories.get(index); + } + + public int directoryCount() { + return directories.size(); + } + + @Override + public Entry getEntryById(final Object identifier) { + for (Directory directory : directories) { + Entry entry = directory.getEntryById(identifier); + + if (entry != null) { + return entry; + } + } + + return null; + } + + @Override + public Entry getEntryByFieldName(final String fieldName) { + for (Directory directory : directories) { + Entry entry = directory.getEntryByFieldName(fieldName); + + if (entry != null) { + return entry; + } + } + + return null; + } + + @Override + public Iterator iterator() { + return new Iterator() { + Iterator directoryIterator = directories.iterator(); + Iterator current; + + public boolean hasNext() { + return current != null && current.hasNext() || directoryIterator.hasNext() && (current = directoryIterator.next().iterator()).hasNext(); + } + + public Entry next() { + hasNext(); + + return current.next(); + } + + public void remove() { + current.remove(); + } + }; + } + + // TODO: Define semantics, or leave to subclasses? + // Add to first/last directory? + // Introduce a "current" directory? And a way to advance/go back + // Remove form the first directory that contains entry? + @Override + public boolean add(final Entry entry) { + throw new UnsupportedOperationException("Directory is read-only"); + } + + @Override + public boolean remove(final Object entry) { + throw new UnsupportedOperationException("Directory is read-only"); + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public int size() { + int size = 0; + + for (Directory directory : directories) { + size += directory.size(); + } + + return size; + } + + @Override + public String toString() { + return String.format("%s%s", getClass().getSimpleName(), directories.toString()); + } + + @Override + public int hashCode() { + int hash = 0; + + for (Directory ifd : directories) { + hash ^= ifd.hashCode(); + } + + return hash; + } + + @Override + public boolean equals(Object pOther) { + if (pOther == this) { + return true; + } + if (pOther == null) { + return false; + } + if (pOther.getClass() != getClass()) { + return false; + } + + CompoundDirectory other = (CompoundDirectory) pOther; + + if (directoryCount() != other.directoryCount()) { + return false; + } + + for (int i = 0; i < directoryCount(); i++) { + if (!getDirectory(i).equals(other.getDirectory(i))) { + return false; + } + } + + return true; + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java index 69748af5..93cf0c0f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java @@ -28,10 +28,9 @@ package com.twelvemonkeys.imageio.metadata; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; +import java.util.*; + +import static com.twelvemonkeys.lang.Validate.noNullElements; /** * AbstractDirectory @@ -41,17 +40,18 @@ import java.util.List; * @version $Id: AbstractDirectory.java,v 1.0 Nov 11, 2009 5:31:04 PM haraldk Exp$ */ public abstract class AbstractDirectory implements Directory { - private final List mEntries = new ArrayList(); + private final List entries = new ArrayList(); + private final List unmodifiable = Collections.unmodifiableList(entries); - protected AbstractDirectory(final Collection pEntries) { - if (pEntries != null) { - mEntries.addAll(pEntries); + protected AbstractDirectory(final Collection entries) { + if (entries != null) { + this.entries.addAll(noNullElements(entries)); } } - public Entry getEntryById(final Object pIdentifier) { + public Entry getEntryById(final Object identifier) { for (Entry entry : this) { - if (entry.getIdentifier().equals(pIdentifier)) { + if (entry.getIdentifier().equals(identifier)) { return entry; } } @@ -59,9 +59,9 @@ public abstract class AbstractDirectory implements Directory { return null; } - public Entry getEntryByFieldName(final String pFieldName) { + public Entry getEntryByFieldName(final String fieldName) { for (Entry entry : this) { - if (entry.getFieldName() != null && entry.getFieldName().equals(pFieldName)) { + if (entry.getFieldName() != null && entry.getFieldName().equals(fieldName)) { return entry; } } @@ -70,7 +70,7 @@ public abstract class AbstractDirectory implements Directory { } public Iterator iterator() { - return mEntries.iterator(); + return isReadOnly() ? unmodifiable.iterator() : entries.iterator(); } /** @@ -85,23 +85,23 @@ public abstract class AbstractDirectory implements Directory { } } - public boolean add(final Entry pEntry) { + public boolean add(final Entry entry) { assertMutable(); // TODO: Replace if entry is already present? // Some directories may need special ordering, or may/may not support multiple entries for certain ids... - return mEntries.add(pEntry); + return entries.add(entry); } @SuppressWarnings({"SuspiciousMethodCalls"}) - public boolean remove(final Object pEntry) { + public boolean remove(final Object entry) { assertMutable(); - return mEntries.remove(pEntry); + return entries.remove(entry); } public int size() { - return mEntries.size(); + return entries.size(); } /** @@ -118,7 +118,7 @@ public abstract class AbstractDirectory implements Directory { @Override public int hashCode() { - return mEntries.hashCode(); + return entries.hashCode(); } @Override @@ -127,18 +127,18 @@ public abstract class AbstractDirectory implements Directory { return true; } - if (getClass() != pOther.getClass()) { + if (pOther == null || getClass() != pOther.getClass()) { return false; } // Safe cast, as it must be a subclass for the classes to be equal AbstractDirectory other = (AbstractDirectory) pOther; - return mEntries.equals(other.mEntries); + return entries.equals(other.entries); } @Override public String toString() { - return String.format("%s%s", getClass().getSimpleName(), mEntries.toString()); + return String.format("%s%s", getClass().getSimpleName(), entries.toString()); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java index 75b8b456..9b5170ab 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java @@ -29,8 +29,10 @@ package com.twelvemonkeys.imageio.metadata; import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.util.CollectionUtil; import java.lang.reflect.Array; +import java.util.Arrays; /** * AbstractEntry @@ -41,18 +43,29 @@ import java.lang.reflect.Array; */ public abstract class AbstractEntry implements Entry { - private final Object mIdentifier; - private final Object mValue; // TODO: Might need to be mutable.. + private final Object identifier; + private final Object value; // TODO: Might need to be mutable.. - protected AbstractEntry(final Object pIdentifier, final Object pValue) { - Validate.notNull(pIdentifier, "identifier"); + protected AbstractEntry(final Object identifier, final Object value) { + Validate.notNull(identifier, "identifier"); - mIdentifier = pIdentifier; - mValue = pValue; + this.identifier = identifier; + this.value = value; } public final Object getIdentifier() { - return mIdentifier; + return identifier; + } + + /** + * Returns a format-native identifier. + * For example {@code "2:00"} for IPTC "Record Version" field, or {@code "0x040c"} for PSD "Thumbnail" resource. + * This default implementation simply returns {@code String.valueOf(getIdentifier())}. + * + * @return a format-native identifier. + */ + protected String getNativeIdentifier() { + return String.valueOf(getIdentifier()); } /** @@ -65,37 +78,88 @@ public abstract class AbstractEntry implements Entry { } public Object getValue() { - return mValue; + return value; } public String getValueAsString() { - return String.valueOf(mValue); + if (valueCount() > 1) { + if (valueCount() < 16) { + return arrayToString(value); + } + else { + String first = arrayToString(CollectionUtil.subArray(value, 0, 4)); + String last = arrayToString(CollectionUtil.subArray(value, valueCount() - 4, 4)); + return String.format("%s ... %s (%d)", first.substring(0, first.length() - 1), last.substring(1), valueCount()); + } + } + + if (value != null && value.getClass().isArray() && Array.getLength(value) == 1) { + return String.valueOf(Array.get(value, 0)); + } + + return String.valueOf(value); + } + + private static String arrayToString(final Object value) { + Class type = value.getClass().getComponentType(); + + if (type.isPrimitive()) { + if (type.equals(boolean.class)) { + return Arrays.toString((boolean[]) value); + } + else if (type.equals(byte.class)) { + return Arrays.toString((byte[]) value); + } + else if (type.equals(char.class)) { + return new String((char[]) value); + } + else if (type.equals(double.class)) { + return Arrays.toString((double[]) value); + } + else if (type.equals(float.class)) { + return Arrays.toString((float[]) value); + } + else if (type.equals(int.class)) { + return Arrays.toString((int[]) value); + } + else if (type.equals(long.class)) { + return Arrays.toString((long[]) value); + } + else if (type.equals(short.class)) { + return Arrays.toString((short[]) value); + } + else { + // Fall through should never happen + throw new AssertionError("Unknown type: " + type); + } + } + else { + return Arrays.toString((Object[]) value); + } } public String getTypeName() { - if (mValue == null) { + if (value == null) { return null; } - return mValue.getClass().getSimpleName(); + return value.getClass().getSimpleName(); } public int valueCount() { // TODO: Collection support? - if (mValue != null && mValue.getClass().isArray()) { - return Array.getLength(mValue); + if (value != null && value.getClass().isArray()) { + return Array.getLength(value); } return 1; } - /// Object - @Override public int hashCode() { - return mIdentifier.hashCode() + 31 * mValue.hashCode(); + return identifier.hashCode() + (value != null ? 31 * value.hashCode() : 0); } @Override @@ -109,8 +173,8 @@ public abstract class AbstractEntry implements Entry { AbstractEntry other = (AbstractEntry) pOther; - return mIdentifier.equals(other.mIdentifier) && ( - mValue == null && other.mValue == null || mValue != null && mValue.equals(other.mValue) + return identifier.equals(other.identifier) && ( + value == null && other.value == null || value != null && value.equals(other.value) ); } @@ -122,6 +186,6 @@ public abstract class AbstractEntry implements Entry { String type = getTypeName(); String typeStr = type != null ? " (" + type + ")" : ""; - return String.format("%s%s: %s%s", getIdentifier(), nameStr, getValueAsString(), typeStr); + return String.format("%s%s: %s%s", getNativeIdentifier(), nameStr, getValueAsString(), typeStr); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/CompoundDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/CompoundDirectory.java new file mode 100644 index 00000000..7bde3ae2 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/CompoundDirectory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +/** + * CompoundDirectory + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CompoundDirectory.java,v 1.0 02.01.12 12:37 haraldk Exp$ + */ +public interface CompoundDirectory extends Directory { + Directory getDirectory(int index); + + int directoryCount(); +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java index e1766f54..8d08a89b 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java @@ -38,9 +38,9 @@ package com.twelvemonkeys.imageio.metadata; public interface Directory extends Iterable { // TODO: Spec when more entries exist? Or make Entry support multi-values!? // For multiple entries with same id in directory, the first entry (using the order from the stream) will be returned - Entry getEntryById(Object pIdentifier); + Entry getEntryById(Object identifier); - Entry getEntryByFieldName(String pName); + Entry getEntryByFieldName(String fieldName); // Iterator containing the entries in //Iterator getBestEntries(Object pIdentifier, Object pQualifier, String pLanguage); @@ -51,9 +51,9 @@ public interface Directory extends Iterable { // boolean replace(Entry pEntry)?? // boolean contains(Object pIdentifier)? - boolean add(Entry pEntry); + boolean add(Entry entry); - boolean remove(Object pEntry); // Object in case we retro-fit Collection/Map.. + boolean remove(Object entry); // Object in case we retro-fit Collection/Map.. int size(); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java index 8681382e..ce7c6074 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java @@ -39,7 +39,7 @@ public interface Entry { // "tag" identifier from spec Object getIdentifier(); - // Human readable "tag" (field) name from sepc + // Human readable "tag" (field) name from spec String getFieldName(); // The internal "tag" value as stored in the stream, may be a Directory diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java index 01457bf4..2888a5f0 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java @@ -39,5 +39,5 @@ import java.io.IOException; * @version $Id: MetadataReader.java,v 1.0 Nov 13, 2009 8:38:11 PM haraldk Exp$ */ public abstract class MetadataReader { - public abstract Directory read(ImageInputStream pInput) throws IOException; + public abstract Directory read(ImageInputStream input) throws IOException; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java index 46e22826..9ebda2ab 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java @@ -36,7 +36,62 @@ package com.twelvemonkeys.imageio.metadata.exif; * @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$ */ public interface EXIF { + // See http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html + int TAG_EXPOSURE_TIME = 33434; + int TAG_F_NUMBER = 33437; + int TAG_EXPOSURE_PROGRAM = 34850; + int TAG_SPECTRAL_SENSITIVITY = 34852; + int TAG_ISO_SPEED_RATINGS = 34855; + int TAG_OECF = 34856; + int TAG_EXIF_VERSION = 36864; + int TAG_DATE_TIME_ORIGINAL = 36867; + int TAG_DATE_TIME_DIGITIZED = 36868; + int TAG_COMPONENTS_CONFIGURATION = 37121; + int TAG_COMPRESSED_BITS_PER_PIXEL = 37122; + int TAG_SHUTTER_SPEED_VALUE = 37377; + int TAG_APERTURE_VALUE = 37378; + int TAG_BRIGHTNESS_VALUE = 37379; + int TAG_EXPOSURE_BIAS_VALUE = 37380; + int TAG_MAX_APERTURE_VALUE = 37381; + int TAG_SUBJECT_DISTANCE = 37382; + int TAG_METERING_MODE = 37383; + int TAG_LIGHT_SOURCE = 37384; + int TAG_FLASH = 37385; + int TAG_FOCAL_LENGTH = 37386; + int TAG_IMAGE_NUMBER = 37393; + int TAG_SUBJECT_AREA = 37396; + int TAG_MAKER_NOTE = 37500; + int TAG_USER_COMMENT = 37510; + int TAG_SUBSEC_TIME = 37520; + int TAG_SUBSEC_TIME_ORIGINAL = 37521; + int TAG_SUBSEC_TIME_DIGITIZED = 37522; + int TAG_FLASHPIX_VERSION = 40960; int TAG_COLOR_SPACE = 40961; int TAG_PIXEL_X_DIMENSION = 40962; int TAG_PIXEL_Y_DIMENSION = 40963; + int TAG_RELATED_SOUND_FILE = 40964; + int TAG_FLASH_ENERGY = 41483; + int TAG_SPATIAL_FREQUENCY_RESPONSE = 41484; + int TAG_FOCAL_PLANE_X_RESOLUTION = 41486; + int TAG_FOCAL_PLANE_Y_RESOLUTION = 41487; + int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 41488; + int TAG_SUBJECT_LOCATION = 41492; + int TAG_EXPOSURE_INDEX = 41493; + int TAG_SENSING_METHOD = 41495; + int TAG_FILE_SOURCE = 41728; + int TAG_SCENE_TYPE = 41729; + int TAG_CFA_PATTERN = 41730; + int TAG_CUSTOM_RENDERED = 41985; + int TAG_EXPOSURE_MODE = 41986; + int TAG_WHITE_BALANCE = 41987; + int TAG_DIGITAL_ZOOM_RATIO = 41988; + int TAG_FOCAL_LENGTH_IN_35_MM_FILM = 41989; + int TAG_SCENE_CAPTURE_TYPE = 41990; + int TAG_GAIN_CONTROL = 41991; + int TAG_CONTRAST = 41992; + int TAG_SATURATION = 41993; + int TAG_SHARPNESS = 41994; + int TAG_DEVICE_SETTING_DESCRIPTION = 41995; + int TAG_SUBJECT_DISTANCE_RANGE = 41996; + int TAG_IMAGE_UNIQUE_ID = 42016; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java index d1ce5930..1ac2a027 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java @@ -28,8 +28,8 @@ package com.twelvemonkeys.imageio.metadata.exif; -import com.twelvemonkeys.imageio.metadata.AbstractDirectory; -import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.AbstractCompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; import java.util.Collection; @@ -40,8 +40,8 @@ import java.util.Collection; * @author last modified by $Author: haraldk$ * @version $Id: EXIFDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$ */ -final class EXIFDirectory extends AbstractDirectory { - EXIFDirectory(final Collection pEntries) { - super(pEntries); +final class EXIFDirectory extends AbstractCompoundDirectory { + EXIFDirectory(final Collection directories) { + super(directories); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java index fef71f37..efa13180 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -38,43 +38,168 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry; * @version $Id: EXIFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$ */ final class EXIFEntry extends AbstractEntry { - final private short mType; + final private short type; - EXIFEntry(final int pIdentifier, final Object pValue, final short pType) { - super(pIdentifier, pValue); + EXIFEntry(final int identifier, final Object value, final short type) { + super(identifier, value); - if (pType < 1 || pType > TIFF.TYPE_NAMES.length) { - throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", pType)); + if (type < 1 || type > TIFF.TYPE_NAMES.length) { + throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", type)); } - mType = pType; + this.type = type; + } + + public short getType() { + return type; } @Override public String getFieldName() { switch ((Integer) getIdentifier()) { + case TIFF.TAG_EXIF_IFD: + return "EXIF"; + case TIFF.TAG_INTEROP_IFD: + return "Interoperability"; + case TIFF.TAG_GPS_IFD: + return "GPS"; + case TIFF.TAG_XMP: + return "XMP"; + case TIFF.TAG_IPTC: + return "IPTC"; + case TIFF.TAG_PHOTOSHOP: + return "Adobe"; + case TIFF.TAG_ICC_PROFILE: + return "ICCProfile"; + + case TIFF.TAG_IMAGE_WIDTH: + return "ImageWidth"; + case TIFF.TAG_IMAGE_HEIGHT: + return "ImageHeight"; + case TIFF.TAG_BITS_PER_SAMPLE: + return "BitsPerSample"; case TIFF.TAG_COMPRESSION: return "Compression"; + case TIFF.TAG_PHOTOMETRIC_INTERPRETATION: + return "PhotometricInterpretation"; + case TIFF.TAG_IMAGE_DESCRIPTION: + return "ImageDescription"; + case TIFF.TAG_STRIP_OFFSETS: + return "StripOffsets"; case TIFF.TAG_ORIENTATION: return "Orientation"; + case TIFF.TAG_SAMPLES_PER_PIXELS: + return "SamplesPerPixels"; + case TIFF.TAG_ROWS_PER_STRIP: + return "RowsPerStrip"; case TIFF.TAG_X_RESOLUTION: return "XResolution"; case TIFF.TAG_Y_RESOLUTION: return "YResolution"; + case TIFF.TAG_PLANAR_CONFIGURATION: + return "PlanarConfiguration"; case TIFF.TAG_RESOLUTION_UNIT: return "ResolutionUnit"; case TIFF.TAG_JPEG_INTERCHANGE_FORMAT: return "JPEGInterchangeFormat"; case TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH: return "JPEGInterchangeFormatLength"; + case TIFF.TAG_MAKE: + return "Make"; + case TIFF.TAG_MODEL: + return "Model"; case TIFF.TAG_SOFTWARE: return "Software"; case TIFF.TAG_DATE_TIME: return "DateTime"; case TIFF.TAG_ARTIST: return "Artist"; + case TIFF.TAG_HOST_COMPUTER: + return "HostComputer"; case TIFF.TAG_COPYRIGHT: return "Copyright"; + case TIFF.TAG_YCBCR_SUB_SAMPLING: + return "YCbCrSubSampling"; + case TIFF.TAG_YCBCR_POSITIONING: + return "YCbCrPositioning"; + case TIFF.TAG_COLOR_MAP: + return "ColorMap"; + case TIFF.TAG_EXTRA_SAMPLES: + return "ExtraSamples"; + + case EXIF.TAG_EXPOSURE_TIME: + return "ExposureTime"; + case EXIF.TAG_F_NUMBER: + return "FNUmber"; + case EXIF.TAG_EXPOSURE_PROGRAM: + return "ExposureProgram"; + case EXIF.TAG_ISO_SPEED_RATINGS: + return "ISOSpeedRatings"; + case EXIF.TAG_SHUTTER_SPEED_VALUE: + return "ShutterSpeedValue"; + case EXIF.TAG_APERTURE_VALUE: + return "ApertureValue"; + case EXIF.TAG_BRIGHTNESS_VALUE: + return "BrightnessValue"; + case EXIF.TAG_EXPOSURE_BIAS_VALUE: + return "ExposureBiasValue"; + case EXIF.TAG_MAX_APERTURE_VALUE: + return "MaxApertureValue"; + case EXIF.TAG_SUBJECT_DISTANCE: + return "SubjectDistance"; + case EXIF.TAG_METERING_MODE: + return "MeteringMode"; + case EXIF.TAG_LIGHT_SOURCE: + return "LightSource"; + case EXIF.TAG_FLASH: + return "Flash"; + case EXIF.TAG_FOCAL_LENGTH: + return "FocalLength"; + case EXIF.TAG_FILE_SOURCE: + return "FileSource"; + case EXIF.TAG_SCENE_TYPE: + return "SceneType"; + case EXIF.TAG_CFA_PATTERN: + return "CFAPattern"; + case EXIF.TAG_CUSTOM_RENDERED: + return "CustomRendered"; + case EXIF.TAG_EXPOSURE_MODE: + return "ExposureMode"; + case EXIF.TAG_WHITE_BALANCE: + return "WhiteBalance"; + case EXIF.TAG_DIGITAL_ZOOM_RATIO: + return "DigitalZoomRation"; + case EXIF.TAG_FOCAL_LENGTH_IN_35_MM_FILM: + return "FocalLengthIn35mmFilm"; + case EXIF.TAG_SCENE_CAPTURE_TYPE: + return "SceneCaptureType"; + case EXIF.TAG_GAIN_CONTROL: + return "GainControl"; + case EXIF.TAG_CONTRAST: + return "Contrast"; + case EXIF.TAG_SATURATION: + return "Saturation"; + case EXIF.TAG_SHARPNESS: + return "Sharpness"; + + case EXIF.TAG_FLASHPIX_VERSION: + return "FlashpixVersion"; + + case EXIF.TAG_EXIF_VERSION: + return "ExifVersion"; + case EXIF.TAG_DATE_TIME_ORIGINAL: + return "DateTimeOriginal"; + case EXIF.TAG_DATE_TIME_DIGITIZED: + return "DateTimeDigitized"; + case EXIF.TAG_IMAGE_NUMBER: + return "ImageNumber"; + case EXIF.TAG_USER_COMMENT: + return "UserComment"; + + case EXIF.TAG_COMPONENTS_CONFIGURATION: + return "ComponentsConfiguration"; + case EXIF.TAG_COMPRESSED_BITS_PER_PIXEL: + return "CompressedBitsPerPixel"; case EXIF.TAG_COLOR_SPACE: return "ColorSpace"; @@ -82,6 +207,7 @@ final class EXIFEntry extends AbstractEntry { return "PixelXDimension"; case EXIF.TAG_PIXEL_Y_DIMENSION: return "PixelYDimension"; + // TODO: More field names } @@ -90,6 +216,6 @@ final class EXIFEntry extends AbstractEntry { @Override public String getTypeName() { - return TIFF.TYPE_NAMES[mType - 1]; + return TIFF.TYPE_NAMES[type - 1]; } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java index 2118bc57..8497da35 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java @@ -28,17 +28,21 @@ package com.twelvemonkeys.imageio.metadata.exif; +import com.twelvemonkeys.imageio.metadata.AbstractCompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.MetadataReader; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; +import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; +import java.io.File; import java.io.IOException; import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.List; +import java.nio.charset.Charset; +import java.util.*; /** * EXIFReader @@ -48,92 +52,236 @@ import java.util.List; * @version $Id: EXIFReader.java,v 1.0 Nov 13, 2009 5:42:51 PM haraldk Exp$ */ public final class EXIFReader extends MetadataReader { + static final Collection KNOWN_IFDS = Collections.unmodifiableCollection(Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD)); @Override - public Directory read(final ImageInputStream pInput) throws IOException { + public Directory read(final ImageInputStream input) throws IOException { + Validate.notNull(input, "input"); + byte[] bom = new byte[2]; - pInput.readFully(bom); + input.readFully(bom); + if (bom[0] == 'I' && bom[1] == 'I') { - pInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + input.setByteOrder(ByteOrder.LITTLE_ENDIAN); } - else if (!(bom[0] == 'M' && bom[1] == 'M')) { + else if (bom[0] == 'M' && bom[1] == 'M') { + input.setByteOrder(ByteOrder.BIG_ENDIAN); + } + else { throw new IIOException(String.format("Invalid TIFF byte order mark '%s', expected: 'II' or 'MM'", StringUtil.decode(bom, 0, bom.length, "ASCII"))); } - int magic = pInput.readUnsignedShort(); + // TODO: BigTiff uses version 43 instead of TIFF's 42, and header is slightly different, see + // http://www.awaresystems.be/imaging/tiff/bigtiff.html + int magic = input.readUnsignedShort(); if (magic != TIFF.TIFF_MAGIC) { throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC)); } - long directoryOffset = pInput.readUnsignedInt(); + long directoryOffset = input.readUnsignedInt(); - return readDirectory(pInput, directoryOffset); + return readDirectory(input, directoryOffset); } - private EXIFDirectory readDirectory(final ImageInputStream pInput, final long pOffset) throws IOException { + private Directory readDirectory(final ImageInputStream pInput, final long pOffset) throws IOException { + List ifds = new ArrayList(); List entries = new ArrayList(); pInput.seek(pOffset); + long nextOffset = -1; int entryCount = pInput.readUnsignedShort(); for (int i = 0; i < entryCount; i++) { - entries.add(readEntry(pInput)); + EXIFEntry entry = readEntry(pInput); + + if (entry == null) { +// System.err.println("Expected: " + entryCount + " values, found only " + i); + // TODO: Log warning? + nextOffset = 0; + break; + } + + entries.add(entry); } - long nextOffset = pInput.readUnsignedInt(); + if (nextOffset == -1) { + nextOffset = pInput.readUnsignedInt(); + } + // Read linked IFDs if (nextOffset != 0) { - EXIFDirectory next = readDirectory(pInput, nextOffset); - - for (Entry entry : next) { - entries.add(entry); + // TODO: This is probably not okay anymore.. Replace recursion with while loop + AbstractCompoundDirectory next = (AbstractCompoundDirectory) readDirectory(pInput, nextOffset); + for (int i = 0; i < next.directoryCount(); i++) { + ifds.add((IFD) next.getDirectory(i)); } } - return new EXIFDirectory(entries); + // TODO: Make what sub-IFDs to parse optional? Or leave this to client code? At least skip the non-TIFF data? + // TODO: Put it in the constructor? + readSubdirectories(pInput, entries, + Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD +// , TIFF.TAG_IPTC, TIFF.TAG_XMP +// , TIFF.TAG_ICC_PROFILE +// , TIFF.TAG_PHOTOSHOP +// ,TIFF.TAG_MODI_OLE_PROPERTY_SET + ) + ); + + ifds.add(0, new IFD(entries)); + + return new EXIFDirectory(ifds); + } + +// private Directory readForeignMetadata(final MetadataReader reader, final byte[] bytes) throws IOException { +// return reader.read(ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))); +// } + + // TODO: Might be better to leave this for client code, as it's tempting go really overboard and support any possible embedded format.. + private void readSubdirectories(ImageInputStream input, List entries, List subIFDs) throws IOException { + if (subIFDs == null || subIFDs.isEmpty()) { + return; + } + + for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) { + EXIFEntry entry = (EXIFEntry) entries.get(i); + int tagId = (Integer) entry.getIdentifier(); + + if (subIFDs.contains(tagId)) { + try { + Object directory; + + /* + if (tagId == TIFF.TAG_IPTC) { + directory = readForeignMetadata(new IPTCReader(), (byte[]) entry.getValue()); + } + else if (tagId == TIFF.TAG_XMP) { + directory = readForeignMetadata(new XMPReader(), (byte[]) entry.getValue()); + } + else if (tagId == TIFF.TAG_PHOTOSHOP) { + // TODO: This is waaay too fragile.. Might need registry-based meta data parsers? + try { + Class cl = Class.forName("com.twelvemonkeys.imageio.plugins.psd.PSDImageResource"); + Method method = cl.getMethod("read", ImageInputStream.class); + method.setAccessible(true); + directory = method.invoke(null, ImageIO.createImageInputStream(new ByteArrayInputStream((byte[]) entry.getValue()))); + } + catch (Exception ignore) { + continue; + } + } + else if (tagId == TIFF.TAG_ICC_PROFILE) { + directory = ICC_Profile.getInstance((byte[]) entry.getValue()); + } + else if (tagId == TIFF.TAG_MODI_OLE_PROPERTY_SET) { + // TODO: Encapsulate in something more useful? + directory = new CompoundDocument(new ByteArrayInputStream((byte[]) entry.getValue())).getRootEntry(); + } + else*/ if (KNOWN_IFDS.contains(tagId)) { + directory = ((AbstractCompoundDirectory) readDirectory(input, getPointerOffset(entry))).getDirectory(0); + } + else { + continue; + } + + // Replace the entry with parsed data + entries.set(i, new EXIFEntry(tagId, directory, entry.getType())); + } + catch (IIOException e) { + // TODO: Issue warning without crashing...? + e.printStackTrace(); + } + } + } + } + + private long getPointerOffset(final Entry entry) throws IIOException { + long offset; + Object value = entry.getValue(); + + if (value instanceof Byte) { + offset = (Byte) value & 0xff; + } + else if (value instanceof Short) { + offset = (Short) value & 0xffff; + } + else if (value instanceof Integer) { + offset = (Integer) value & 0xffffffffL; + } + else if (value instanceof Long) { + offset = (Long) value; + } + else { + throw new IIOException(String.format("Unknown pointer type: %s", (value != null ? value.getClass() : null))); + } + + return offset; } private EXIFEntry readEntry(final ImageInputStream pInput) throws IOException { + // TODO: BigTiff entries are different int tagId = pInput.readUnsignedShort(); - short type = pInput.readShort(); + + // This isn't really an entry, and the directory entry count was wrong OR bad data... + if (tagId == 0 && type == 0) { + return null; + } + int count = pInput.readInt(); // Number of values - Object value; + // It's probably a spec violation to have count 0, but we'll be lenient about it + if (count < 0) { + throw new IIOException(String.format("Illegal count %d for tag %s type %s @%08x", count, tagId, type, pInput.getStreamPosition())); + } + + if (type <= 0 || type > 13) { + // Invalid tag, this is just for debugging + long offset = pInput.getStreamPosition() - 8l; + + System.err.printf("Bad EXIF"); + System.err.println("tagId: " + tagId + (tagId <= 0 ? " (INVALID)" : "")); + System.err.println("type: " + type + " (INVALID)"); + System.err.println("count: " + count); - if (tagId == TIFF.IFD_EXIF || tagId == TIFF.IFD_GPS || tagId == TIFF.IFD_INTEROP) { - // Parse sub IFDs - long offset = pInput.readUnsignedInt(); pInput.mark(); + pInput.seek(offset); try { - value = readDirectory(pInput, offset); + byte[] bytes = new byte[8 + Math.max(20, count)]; + int len = pInput.read(bytes); + + System.err.print(HexDump.dump(offset, bytes, 0, len)); + System.err.println(len < count ? "[...]" : ""); } finally { pInput.reset(); } + + return null; + } + + int valueLength = getValueLength(type, count); + + Object value; + // TODO: For BigTiff allow size > 4 && <= 8 in addition + if (valueLength > 0 && valueLength <= 4) { + value = readValueInLine(pInput, type, count); + pInput.skipBytes(4 - valueLength); } else { - int valueLength = getValueLength(type, count); - - if (valueLength > 0 && valueLength <= 4) { - value = readValueInLine(pInput, type, count); - pInput.skipBytes(4 - valueLength); - } - else { - long valueOffset = pInput.readUnsignedInt(); // This is the *value* iff the value size is <= 4 bytes - value = readValue(pInput, valueOffset, type, count); - } + long valueOffset = pInput.readUnsignedInt(); // This is the *value* iff the value size is <= 4 bytes + value = readValueAt(pInput, valueOffset, type, count); } return new EXIFEntry(tagId, value, type); } - private Object readValue(final ImageInputStream pInput, final long pOffset, final short pType, final int pCount) throws IOException { + private Object readValueAt(final ImageInputStream pInput, final long pOffset, final short pType, final int pCount) throws IOException { long pos = pInput.getStreamPosition(); try { pInput.seek(pOffset); - return readValueInLine(pInput, pType, pCount); + return readValue(pInput, pType, pCount); } finally { pInput.seek(pos); @@ -141,53 +289,87 @@ public final class EXIFReader extends MetadataReader { } private Object readValueInLine(final ImageInputStream pInput, final short pType, final int pCount) throws IOException { - return readValueDirect(pInput, pType, pCount); + return readValue(pInput, pType, pCount); } - private static Object readValueDirect(final ImageInputStream pInput, final short pType, final int pCount) throws IOException { + private static Object readValue(final ImageInputStream pInput, final short pType, final int pCount) throws IOException { + // TODO: Review value "widening" for the unsigned types. Right now it's inconsistent. Should we leave it to client code? + + long pos = pInput.getStreamPosition(); + switch (pType) { - case 2: - // TODO: This might be UTF-8 or ISO-8859-1, even though spec says ASCII + case 2: // ASCII + // TODO: This might be UTF-8 or ISO-8859-x, even though spec says NULL-terminated 7 bit ASCII + // TODO: Fail if unknown chars, try parsing with ISO-8859-1 or file.encoding + if (pCount == 0) { + return ""; + } byte[] ascii = new byte[pCount]; pInput.readFully(ascii); - return StringUtil.decode(ascii, 0, ascii.length, "UTF-8"); // UTF-8 is ASCII compatible - case 1: + int len = ascii[ascii.length - 1] == 0 ? ascii.length - 1 : ascii.length; + return StringUtil.decode(ascii, 0, len, "UTF-8"); // UTF-8 is ASCII compatible + case 1: // BYTE if (pCount == 1) { return pInput.readUnsignedByte(); } - case 6: + // else fall through + case 6: // SBYTE if (pCount == 1) { return pInput.readByte(); } - case 7: + // else fall through + case 7: // UNDEFINED byte[] bytes = new byte[pCount]; pInput.readFully(bytes); + + // NOTE: We don't change (unsigned) BYTE array wider Java type, as most often BYTE array means + // binary data and we want to keep that as a byte array for clients to parse futher + return bytes; - case 3: + case 3: // SHORT if (pCount == 1) { return pInput.readUnsignedShort(); } - case 8: + case 8: // SSHORT if (pCount == 1) { return pInput.readShort(); } short[] shorts = new short[pCount]; pInput.readFully(shorts, 0, shorts.length); + + if (pType == 3) { + int[] ints = new int[pCount]; + for (int i = 0; i < pCount; i++) { + ints[i] = shorts[i] & 0xffff; + } + return ints; + } + return shorts; - case 4: + case 13: // IFD + case 4: // LONG if (pCount == 1) { return pInput.readUnsignedInt(); } - case 9: + case 9: // SLONG if (pCount == 1) { return pInput.readInt(); } int[] ints = new int[pCount]; pInput.readFully(ints, 0, ints.length); + + if (pType == 4 || pType == 13) { + long[] longs = new long[pCount]; + for (int i = 0; i < pCount; i++) { + longs[i] = ints[i] & 0xffffffffL; + } + return longs; + } + return ints; - case 11: + case 11: // FLOAT if (pCount == 1) { return pInput.readFloat(); } @@ -195,7 +377,7 @@ public final class EXIFReader extends MetadataReader { float[] floats = new float[pCount]; pInput.readFully(floats, 0, floats.length); return floats; - case 12: + case 12: // DOUBLE if (pCount == 1) { return pInput.readDouble(); } @@ -204,34 +386,65 @@ public final class EXIFReader extends MetadataReader { pInput.readFully(doubles, 0, doubles.length); return doubles; - case 5: + case 5: // RATIONAL if (pCount == 1) { - return new Rational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); + return createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); } Rational[] rationals = new Rational[pCount]; for (int i = 0; i < rationals.length; i++) { - rationals[i] = new Rational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); + rationals[i] = createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); } return rationals; - case 10: + case 10: // SRATIONAL if (pCount == 1) { - return new Rational(pInput.readInt(), pInput.readInt()); + return createSafeRational(pInput.readInt(), pInput.readInt()); } Rational[] srationals = new Rational[pCount]; for (int i = 0; i < srationals.length; i++) { - srationals[i] = new Rational(pInput.readInt(), pInput.readInt()); + srationals[i] = createSafeRational(pInput.readInt(), pInput.readInt()); } return srationals; + // BigTiff: + case 16: // LONG8 + case 17: // SLONG8 + case 18: // IFD8 + // TODO: Assert BigTiff (version == 43) + + if (pCount == 1) { + long val = pInput.readLong(); + if (pType != 17 && val < 0) { + throw new IIOException(String.format("Value > %s", Long.MAX_VALUE)); + } + return val; + } + + long[] longs = new long[pCount]; + for (int i = 0; i < pCount; i++) { + longs[i] = pInput.readLong(); + } + + return longs; + default: - throw new IIOException(String.format("Unknown EXIF type '%s'", pType)); + // Spec says skip unknown values + return new Unknown(pType, pCount, pos); } } + private static Rational createSafeRational(final long numerator, final long denominator) throws IOException { + if (denominator == 0) { + // Bad data. + return Rational.NaN; + } + + return new Rational(numerator, denominator); + } + private int getValueLength(final int pType, final int pCount) { if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) { return TIFF.TYPE_LENGTHS[pType - 1] * pCount; @@ -239,4 +452,115 @@ public final class EXIFReader extends MetadataReader { return -1; } + + public static void main(String[] args) throws IOException { + EXIFReader reader = new EXIFReader(); + ImageInputStream stream = ImageIO.createImageInputStream(new File(args[0])); + + long pos = 0; + if (args.length > 1) { + if (args[1].startsWith("0x")) { + pos = Integer.parseInt(args[1].substring(2), 16); + } + else { + pos = Long.parseLong(args[1]); + } + + stream.setByteOrder(pos < 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); + + pos = Math.abs(pos); + + stream.seek(pos); + } + + try { + Directory directory; + + if (args.length > 1) { + directory = reader.readDirectory(stream, pos); + } + else { + directory = reader.read(stream); + } + + for (Entry entry : directory) { + System.err.println(entry); + + Object value = entry.getValue(); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + System.err.println(HexDump.dump(0, bytes, 0, Math.min(bytes.length, 128))); + } + } + } + finally { + stream.close(); + } + } + + ////////////////////// + // TODO: Stream based hex dump util? + public static class HexDump { + private HexDump() {} + + private static final int WIDTH = 32; + + public static String dump(byte[] bytes) { + return dump(0, bytes, 0, bytes.length); + } + + public static String dump(long offset, byte[] bytes, int off, int len) { + StringBuilder builder = new StringBuilder(); + + int i; + for (i = 0; i < len; i++) { + if (i % WIDTH == 0) { + if (i > 0 ) { + builder.append("\n"); + } + builder.append(String.format("%08x: ", i + off + offset)); + } + else if (i > 0 && i % 2 == 0) { + builder.append(" "); + } + + builder.append(String.format("%02x", bytes[i + off])); + + int next = i + 1; + if (next % WIDTH == 0 || next == len) { + int leftOver = (WIDTH - (next % WIDTH)) % WIDTH; + + if (leftOver != 0) { + // Pad: 5 spaces for every 2 bytes... Special care if padding is non-even. + int pad = leftOver / 2; + + if (len % 2 != 0) { + builder.append(" "); + } + + for (int j = 0; j < pad; j++) { + builder.append(" "); + } + } + + builder.append(" "); + builder.append(toAsciiString(bytes, next - (WIDTH - leftOver) + off, next + off)); + } + } + + return builder.toString(); + } + + private static String toAsciiString(final byte[] bytes, final int from, final int to) { + byte[] range = Arrays.copyOfRange(bytes, from, to); + + for (int i = 0; i < range.length; i++) { + if (range[i] < 32 || range[i] > 126) { + range[i] = '.'; // Unreadable char + } + } + + return new String(range, Charset.forName("ascii")); + } + } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/IFD.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/IFD.java new file mode 100644 index 00000000..0340517d --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/IFD.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.imageio.metadata.AbstractDirectory; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** + * IFD + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IFD.java,v 1.0 23.12.11 16:24 haraldk Exp$ + */ +final class IFD extends AbstractDirectory { + protected IFD(final Collection pEntries) { + super(pEntries); + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java index ffe69ae0..b0e0a247 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java @@ -53,9 +53,15 @@ public final class Rational extends Number implements Comparable { // TODO: Move to com.tm.lang? // Inspired by http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html and java.lang.Integer static final Rational ZERO = new Rational(0, 1); + static final Rational NaN = new Rational(); // TODO: This field needs thoughts/tests/spec/consistency check, see Float.NaN - private final long mNumerator; - private final long mDenominator; + private final long numerator; + private final long denominator; + + private Rational() { + numerator = 0; + denominator = 0; + } public Rational(final long pNumber) { this(pNumber, 1); @@ -74,8 +80,8 @@ public final class Rational extends Number implements Comparable { long num = pNumerator / gcd; long den = pDenominator / gcd; - mNumerator = pDenominator >= 0 ? num : -num; - mDenominator = pDenominator >= 0 ? den : -den; + numerator = pDenominator >= 0 ? num : -num; + denominator = pDenominator >= 0 ? den : -den; } private static long gcd(final long m, final long n) { @@ -95,11 +101,11 @@ public final class Rational extends Number implements Comparable { } public long numerator() { - return mNumerator; + return numerator; } public long denominator() { - return mDenominator; + return denominator; } /// Number implementation @@ -121,7 +127,11 @@ public final class Rational extends Number implements Comparable { @Override public double doubleValue() { - return mNumerator / (double) mDenominator; + if (this == NaN) { + return Double.NaN; + } + + return numerator / (double) denominator; } /// Comparable implementation @@ -147,7 +157,11 @@ public final class Rational extends Number implements Comparable { @Override public String toString() { - return mDenominator == 1 ? Long.toString(mNumerator) : String.format("%s/%s", mNumerator, mDenominator); + if (this == NaN) { + return "NaN"; + } + + return denominator == 1 ? Long.toString(numerator) : String.format("%s/%s", numerator, denominator); } /// Operations (adapted from http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html) @@ -161,10 +175,10 @@ public final class Rational extends Number implements Comparable { } // reduce p1/q2 and p2/q1, then multiply, where a = p1/q1 and b = p2/q2 - Rational c = new Rational(mNumerator, pOther.mDenominator); - Rational d = new Rational(pOther.mNumerator, mDenominator); + Rational c = new Rational(numerator, pOther.denominator); + Rational d = new Rational(pOther.numerator, denominator); - return new Rational(c.mNumerator * d.mNumerator, c.mDenominator * d.mDenominator); + return new Rational(c.numerator * d.numerator, c.denominator * d.denominator); } // return a + b, staving off overflow @@ -178,20 +192,20 @@ public final class Rational extends Number implements Comparable { } // Find gcd of numerators and denominators - long f = gcd(mNumerator, pOther.mNumerator); - long g = gcd(mDenominator, pOther.mDenominator); + long f = gcd(numerator, pOther.numerator); + long g = gcd(denominator, pOther.denominator); // add cross-product terms for numerator // multiply back in return new Rational( - ((mNumerator / f) * (pOther.mDenominator / g) + (pOther.mNumerator / f) * (mDenominator / g)) * f, - lcm(mDenominator, pOther.mDenominator) + ((numerator / f) * (pOther.denominator / g) + (pOther.numerator / f) * (denominator / g)) * f, + lcm(denominator, pOther.denominator) ); } // return -a public Rational negate() { - return new Rational(-mNumerator, mDenominator); + return new Rational(-numerator, denominator); } // return a - b @@ -200,7 +214,7 @@ public final class Rational extends Number implements Comparable { } public Rational reciprocal() { - return new Rational(mDenominator, mNumerator); + return new Rational(denominator, numerator); } // return a / b diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java index c7fc6e27..b60e2955 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java @@ -47,7 +47,7 @@ public interface TIFF { 5 = RATIONAL Two LONGs: the first represents the numerator of a fraction; the second, the denominator. - TIFF 6.0 and above: + TIFF 6.0 and above: 6 = SBYTE An 8-bit signed (twos-complement) integer. 7 = UNDEFINED An 8-bit byte that may contain anything, depending on the definition of the field. @@ -57,21 +57,39 @@ public interface TIFF { fraction, the second the denominator. 11 = FLOAT Single precision (4-byte) IEEE format. 12 = DOUBLE Double precision (8-byte) IEEE format. + + TODO: Verify IFD type + See http://www.awaresystems.be/imaging/tiff/tifftags/subifds.html + 13 = IFD, same as LONG + + TODO: BigTiff specifies more types + See http://www.awaresystems.be/imaging/tiff/bigtiff.html, http://www.remotesensing.org/libtiff/bigtiffdesign.html + (what about 14-15??) + 16 = TIFF_LONG8, being unsigned 8byte integer + 17 = TIFF_SLONG8, being signed 8byte integer + 18 = TIFF_IFD8, being a new unsigned 8byte IFD offset. + Should probably all map to Java long (and fail if high bit is set for the unsigned types???) */ String[] TYPE_NAMES = { "BYTE", "ASCII", "SHORT", "LONG", "RATIONAL", - "SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE", + "IFD", + null, null, + "LONG8", "SLONG8", "IFD8" }; int[] TYPE_LENGTHS = { 1, 1, 2, 4, 8, - 1, 1, 2, 4, 8, 4, 8, + 4, + -1, -1, + 8, 8, 8 }; - int IFD_EXIF = 0x8769; - int IFD_GPS = 0x8825; - int IFD_INTEROP = 0xA005; + /// EXIF defined TIFF tags + + int TAG_EXIF_IFD = 34665; + int TAG_GPS_IFD = 34853; + int TAG_INTEROP_IFD = 40965; /// A. Tags relating to image data structure: @@ -83,6 +101,7 @@ public interface TIFF { int TAG_ORIENTATION = 274; int TAG_SAMPLES_PER_PIXELS = 277; int TAG_PLANAR_CONFIGURATION = 284; + int TAG_SAMPLE_FORMAT = 339; int TAG_YCBCR_SUB_SAMPLING = 530; int TAG_YCBCR_POSITIONING = 531; int TAG_X_RESOLUTION = 282; @@ -100,8 +119,11 @@ public interface TIFF { /// C. Tags relating to image data characteristics int TAG_TRANSFER_FUNCTION = 301; + int TAG_PREDICTOR = 317; int TAG_WHITE_POINT = 318; int TAG_PRIMARY_CHROMATICITIES = 319; + int TAG_COLOR_MAP = 320; + int TAG_EXTRA_SAMPLES = 338; int TAG_YCBCR_COEFFICIENTS = 529; int TAG_REFERENCE_BLACK_WHITE = 532; @@ -113,5 +135,31 @@ public interface TIFF { int TAG_MODEL = 272; int TAG_SOFTWARE = 305; int TAG_ARTIST = 315; + int TAG_HOST_COMPUTER = 316; int TAG_COPYRIGHT = 33432; + + int TAG_SUB_IFD = 330; + + int TAG_XMP = 700; + int TAG_IPTC = 33723; + int TAG_PHOTOSHOP = 34377; + int TAG_ICC_PROFILE = 34675; + + // Microsoft Office Document Imaging (MODI) + // http://msdn.microsoft.com/en-us/library/aa167596%28office.11%29.aspx + int TAG_MODI_BLC = 34718; + int TAG_MODI_VECTOR = 34719; + int TAG_MODI_PTC = 34720; + + // http://blogs.msdn.com/b/openspecification/archive/2009/12/08/details-of-three-tiff-tag-extensions-that-microsoft-office-document-imaging-modi-software-may-write-into-the-tiff-files-it-generates.aspx + int TAG_MODI_PLAIN_TEXT = 37679; + int TAG_MODI_OLE_PROPERTY_SET = 37680; + int TAG_MODI_TEXT_POS_INFO = 37681; + + int TAG_TILE_WIDTH = 322; + int TAG_TILE_HEIGTH = 323; + int TAG_TILE_OFFSETS = 324; + int TAG_TILE_BYTE_COUNTS = 325; + + int TAG_JPEG_TABLES = 347; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Unknown.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Unknown.java new file mode 100644 index 00000000..d8bbbf54 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Unknown.java @@ -0,0 +1,46 @@ +package com.twelvemonkeys.imageio.metadata.exif; + +/** + * Unknown + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Unknown.java,v 1.0 Oct 8, 2010 3:38:45 PM haraldk Exp$ + * @see We also know there are known unknowns + */ +final class Unknown { + private final short type; + private final int count; + private final long pos; + + public Unknown(final short type, final int count, final long pos) { + this.type = type; + this.count = count; + this.pos = pos; + } + + @Override + public int hashCode() { + return (int) (pos ^ (pos >>> 32)) + count * 37 + type * 97; + } + + @Override + public boolean equals(final Object other) { + if (other != null && other.getClass() == getClass()) { + Unknown unknown = (Unknown) other; + return pos == unknown.pos && type == unknown.type && count == unknown.count; + } + + return false; + } + + @Override + public String toString() { + if (count == 1) { + return String.format("Unknown(%d)@%08x", type, pos); + } + else { + return String.format("Unknown(%d)[%d]@%08x", type, count, pos); + } + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java index a3fb3325..133f07bc 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java @@ -41,7 +41,7 @@ import java.util.Collection; * @version $Id: IPTCDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$ */ final class IPTCDirectory extends AbstractDirectory { - IPTCDirectory(final Collection pEntries) { - super(pEntries); + IPTCDirectory(final Collection entries) { + super(entries); } } \ No newline at end of file diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java index 3b1175a2..be129c3e 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java @@ -38,13 +38,15 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry; * @version $Id: IPTCEntry.java,v 1.0 Nov 13, 2009 8:57:04 PM haraldk Exp$ */ class IPTCEntry extends AbstractEntry { - public IPTCEntry(final int pTagId, final Object pValue) { - super(pTagId, pValue); + public IPTCEntry(final int tagId, final Object value) { + super(tagId, value); } @Override public String getFieldName() { switch ((Integer) getIdentifier()) { + case IPTC.TAG_RECORD_VERSION: + return "RecordVersion"; case IPTC.TAG_SOURCE: return "Source"; // TODO: More tags... @@ -52,4 +54,10 @@ class IPTCEntry extends AbstractEntry { return null; } + + @Override + protected String getNativeIdentifier() { + int identifier = (Integer) getIdentifier(); + return String.format("%d:%02d", identifier >> 8, identifier & 0xff); + } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java index a9f23b96..a9fad969 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java @@ -32,6 +32,7 @@ import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.MetadataReader; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; @@ -57,18 +58,20 @@ public final class IPTCReader extends MetadataReader { private static final int ENCODING_UNSPECIFIED = 0; private static final int ENCODING_UTF_8 = 0x1b2547; - private int mEncoding = ENCODING_UNSPECIFIED; + private int encoding = ENCODING_UNSPECIFIED; @Override - public Directory read(final ImageInputStream pInput) throws IOException { - final List entries = new ArrayList(); + public Directory read(final ImageInputStream input) throws IOException { + Validate.notNull(input, "input"); + + List entries = new ArrayList(); // 0x1c identifies start of a tag - while (pInput.read() == 0x1c) { - short tagId = pInput.readShort(); - int tagByteCount = pInput.readUnsignedShort(); - Entry entry = readEntry(pInput, tagId, tagByteCount); + while (input.read() == 0x1c) { + short tagId = input.readShort(); + int tagByteCount = input.readUnsignedShort(); + Entry entry = readEntry(input, tagId, tagByteCount); if (entry != null) { entries.add(entry); @@ -85,7 +88,7 @@ public final class IPTCReader extends MetadataReader { case IPTC.TAG_CODED_CHARACTER_SET: // TODO: Mapping from ISO 646 to Java supported character sets? // TODO: Move somewhere else? - mEncoding = parseEncoding(pInput, pLength); + encoding = parseEncoding(pInput, pLength); return null; case IPTC.TAG_RECORD_VERSION: // A single unsigned short value @@ -140,7 +143,7 @@ public final class IPTCReader extends MetadataReader { return chars.toString(); } catch (CharacterCodingException notUTF8) { - if (mEncoding == ENCODING_UTF_8) { + if (encoding == ENCODING_UTF_8) { throw new IIOException("Wrong encoding of IPTC data, explicitly set to UTF-8 in DataSet 1:90", notUTF8); } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java new file mode 100644 index 00000000..8753c26e --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEG.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +/** + * JPEG + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEG.java,v 1.0 11.02.11 15.51 haraldk Exp$ + */ +public interface JPEG { + /** Start of Image segment marker (SOI). */ + int SOI = 0xFFD8; + /** End of Image segment marker (EOI). */ + int EOI = 0xFFD9; + /** Start of Stream segment marker (SOS). */ + int SOS = 0xFFDA; + + /** Define Quantization Tables segment marker (DQT). */ + int DQT = 0xFFDB; + /** Define Huffman Tables segment marker (DHT). */ + int DHT = 0xFFC4; + + // App segment markers (APPn). + int APP0 = 0xFFE0; + int APP1 = 0xFFE1; + int APP2 = 0xFFE2; + int APP3 = 0xFFE3; + int APP4 = 0xFFE4; + int APP5 = 0xFFE5; + int APP6 = 0xFFE6; + int APP7 = 0xFFE7; + int APP8 = 0xFFE8; + int APP9 = 0xFFE9; + int APP10 = 0xFFEA; + int APP11 = 0xFFEB; + int APP12 = 0xFFEC; + int APP13 = 0xFFED; + int APP14 = 0xFFEE; + int APP15 = 0xFFEF; + + // Start of Frame segment markers (SOFn). + int SOF0 = 0xFFC0; + int SOF1 = 0xFFC1; + int SOF2 = 0xFFC2; + int SOF3 = 0xFFC3; + int SOF5 = 0xFFC5; + int SOF6 = 0xFFC6; + int SOF7 = 0xFFC7; + int SOF9 = 0xFFC9; + int SOF10 = 0xFFCA; + int SOF11 = 0xFFCB; + int SOF13 = 0xFFCD; + int SOF14 = 0xFFCE; + int SOF15 = 0xFFCF; + + // TODO: Known/Important APPn marker identifiers + // "JFIF" APP0 + // "JFXX" APP0 + // "Exif" APP1 + // "ICC_PROFILE" APP2 + // "Adobe" APP14 + + // Possibly + // "http://ns.adobe.com/xap/1.0/" (XMP) + // "Photoshop 3.0" (Contains IPTC) +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java new file mode 100644 index 00000000..77a107d0 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQuality.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.plugins.jpeg.JPEGQTable; +import javax.imageio.stream.ImageInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Determines an approximate JPEG compression quality value from the quantization tables. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGQuality.java,v 1.0 16.02.12 17:07 haraldk Exp$ + */ +public final class JPEGQuality { + static final int NUM_QUANT_TABLES = 4; /* Quantization tables are numbered 0..3 */ + static final int DCT_SIZE_2 = 64; /* DCT_SIZE squared; # of elements in a block */ + + /** + * Determines an approximate JPEG compression quality value from the quantization tables. + * The value will be in the range {@code [0...1]}, where {@code 1} is the best possible value. + * + * @param segments a list of JPEG segments containing the DQT quantization tables. + * @return a float in the range {@code [0...1]}, representing the JPEG quality, + * or {@code -1} if the quality can't be determined. + * @throws IIOException if a JPEG format error is found during parsing. + * @throws IOException if an I/O exception occurs during parsing. + * + * @see javax.imageio.plugins.jpeg.JPEGImageWriteParam#setCompressionQuality(float) + * @see JPEG#DQT + */ + public static float getJPEGQuality(final List segments) throws IOException { + int quality = getJPEGQuality(getQuantizationTables(segments)); + return quality >= 0 ? quality / 100f : quality; + } + + /** + * Determines an approximate JPEG compression quality value from the quantization tables. + * The value will be in the range {@code [0...1]}, where {@code 1} is the best possible value. + * + * @param input an image input stream containing JPEG data. + * @return a float in the range {@code [0...1]}, representing the JPEG quality, + * or {@code -1} if the quality can't be determined. + * @throws IIOException if a JPEG format error is found during parsing. + * @throws IOException if an I/O exception occurs during parsing. + * + * @see javax.imageio.plugins.jpeg.JPEGImageWriteParam#setCompressionQuality(float) + * @see JPEG#DQT + */ + public static float getJPEGQuality(final ImageInputStream input) throws IOException { + return getJPEGQuality(JPEGSegmentUtil.readSegments(input, JPEG.DQT, null)); + } + + // Adapted from ImageMagick coders/jpeg.c & http://blog.apokalyptik.com/2009/09/16/quality-time-with-your-jpegs/ + private static int getJPEGQuality(final int[][] quantizationTables) throws IOException { +// System.err.println("tables: " + Arrays.deepToString(tables)); + + // TODO: Determine lossless JPEG +// if (lossless) { +// return 100; // TODO: Sums can be 100... Is lossless not 100? +// } + + int qvalue; + + // Determine the JPEG compression quality from the quantization tables. + int sum = 0; + for (int i = 0; i < NUM_QUANT_TABLES; i++) { + if (quantizationTables[i] != null) { + for (int j = 0; j < DCT_SIZE_2; j++) { + sum += quantizationTables[i][j]; + } + } + } + + int[] hash, sums; + + if (quantizationTables[0] != null && quantizationTables[1] != null) { + // TODO: Make constant + hash = new int[] { + 1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645, + 632, 623, 613, 607, 600, 594, 589, 585, 581, 571, + 555, 542, 529, 514, 494, 474, 457, 439, 424, 410, + 397, 386, 373, 364, 351, 341, 334, 324, 317, 309, + 299, 294, 287, 279, 274, 267, 262, 257, 251, 247, + 243, 237, 232, 227, 222, 217, 213, 207, 202, 198, + 192, 188, 183, 177, 173, 168, 163, 157, 153, 148, + 143, 139, 132, 128, 125, 119, 115, 108, 104, 99, + 94, 90, 84, 79, 74, 70, 64, 59, 55, 49, + 45, 40, 34, 30, 25, 20, 15, 11, 6, 4, + 0 + }; + sums = new int[] { + 32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, + 27670, 27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946, + 23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998, + 16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702, + 12423, 12056, 11779, 11513, 11135, 10955, 10676, 10392, 10208, + 9928, 9747, 9564, 9369, 9193, 9017, 8822, 8639, 8458, + 8270, 8084, 7896, 7710, 7527, 7347, 7156, 6977, 6788, + 6607, 6422, 6236, 6054, 5867, 5684, 5495, 5305, 5128, + 4945, 4751, 4638, 4442, 4248, 4065, 3888, 3698, 3509, + 3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846, + 1666, 1483, 1297, 1109, 927, 735, 554, 375, 201, + 128, 0 + }; + + qvalue = quantizationTables[0][2] + quantizationTables[0][53] + quantizationTables[1][0] + quantizationTables[1][DCT_SIZE_2 - 1]; + } + else if (quantizationTables[0] != null) { + // TODO: Make constant + hash = new int[] { + 510, 505, 422, 380, 355, 338, 326, 318, 311, 305, + 300, 297, 293, 291, 288, 286, 284, 283, 281, 280, + 279, 278, 277, 273, 262, 251, 243, 233, 225, 218, + 211, 205, 198, 193, 186, 181, 177, 172, 168, 164, + 158, 156, 152, 148, 145, 142, 139, 136, 133, 131, + 129, 126, 123, 120, 118, 115, 113, 110, 107, 105, + 102, 100, 97, 94, 92, 89, 87, 83, 81, 79, + 76, 74, 70, 68, 66, 63, 61, 57, 55, 52, + 50, 48, 44, 42, 39, 37, 34, 31, 29, 26, + 24, 21, 18, 16, 13, 11, 8, 6, 3, 2, + 0 + }; + sums = new int[] { + 16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859, + 12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679, + 9368, 9056, 8680, 8331, 7995, 7668, 7376, 7084, 6823, + 6562, 6345, 6125, 5939, 5756, 5571, 5421, 5240, 5086, + 4976, 4829, 4719, 4616, 4463, 4393, 4280, 4166, 4092, + 3980, 3909, 3835, 3755, 3688, 3621, 3541, 3467, 3396, + 3323, 3247, 3170, 3096, 3021, 2952, 2874, 2804, 2727, + 2657, 2583, 2509, 2437, 2362, 2290, 2211, 2136, 2068, + 1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477, 1398, + 1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736, + 667, 592, 518, 441, 369, 292, 221, 151, 86, + 64, 0 + }; + + qvalue = quantizationTables[0][2] + quantizationTables[0][53]; + } + else { + return -1; + } + + for (int i = 0; i < 100; i++) { + if (qvalue < hash[i] && sum < sums[i]) { + continue; + } + + if (qvalue <= hash[i] && sum <= sums[i] || i >= 50) { + return i + 1; + } + + break; + } + + return -1; + } + + public static JPEGQTable[] getQTables(final List segments) throws IOException { + int[][] tables = getQuantizationTables(segments); + + List qTables = new ArrayList(); + for (int[] table : tables) { + if (table != null) { + qTables.add(new JPEGQTable(table)); + } + } + + return qTables.toArray(new JPEGQTable[qTables.size()]); + } + + private static int[][] getQuantizationTables(final List dqtSegments) throws IOException { + Validate.notNull(dqtSegments, "segments"); + + int[][] tables = new int[4][]; + + // JPEG may contain multiple DQT marker segments + for (JPEGSegment segment : dqtSegments) { + if (segment.marker() != JPEG.DQT) { + continue; + } + + DataInputStream data = new DataInputStream(segment.data()); + int read = 0; + + // A single DQT marker segment may contain multiple tables + while (read < segment.length()) { + int qtInfo = data.read(); + read++; +// System.err.printf("qtInfo: 0x%02x\n", qtInfo); + + int num = qtInfo & 0x0f; // 0-3 + int bits = qtInfo >> 4; // 0 == 8 bits, 1 == 16 bits + + if (num >= 4) { + throw new IIOException("Bad DQT table index: " + num); + } + else if (tables[num] != null) { + throw new IIOException("Duplicate DQT table index: " + num); + } + + if (bits > 1) { + throw new IIOException("Bad DQT bit info: " + bits); + } + + byte[] qtData = new byte[DCT_SIZE_2 * (bits + 1)]; + data.readFully(qtData); + read += qtData.length; + tables[num] = new int[DCT_SIZE_2]; + + // Expand (this is slightly inefficient) + switch (bits) { + case 0: + for (int j = 0, qtDataLength = qtData.length; j < qtDataLength; j++) { + tables[num][j] = (short) (qtData[j] & 0xff); + } + break; + case 1: + for (int j = 0, qtDataLength = qtData.length; j < qtDataLength; j += 2) { + tables[num][j / 2] = (short) ((qtData[j] & 0xff) << 8 | (qtData[j + 1] & 0xff)); + } + break; + } + } + } + + return tables; + } + + public static void main(String[] args) throws IOException { + for (String arg : args) { + float quality = getJPEGQuality(ImageIO.createImageInputStream(new File(arg))); + System.err.println(arg + " quality: " + quality + "/" + (int) (quality * 100)); + } + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java new file mode 100644 index 00000000..2d667b6c --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Serializable; +import java.util.Arrays; + +/** + * Represents a JPEG segment. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGSegment.java,v 1.0 02.03.11 10.44 haraldk Exp$ + */ +public final class JPEGSegment implements Serializable { + final int marker; + final byte[] data; + final int length; + + private transient String id; + + JPEGSegment(int marker, byte[] data, int length) { + this.marker = marker; + this.data = data; + this.length = length; + } + + int segmentLength() { + // This is the length field as read from the stream + return length; + } + + public int marker() { + return marker; + } + + public String identifier() { + if (id == null) { + if (isAppSegmentMarker(marker)) { + // Only for APPn markers + id = JPEGSegmentUtil.asNullTerminatedAsciiString(data, 0); + } + } + + return id; + } + + static boolean isAppSegmentMarker(final int marker) { + return marker >= 0xFFE0 && marker <= 0xFFEF; + } + + public InputStream data() { + return data != null ? new ByteArrayInputStream(data, offset(), length()) : null; + } + + public int length() { + return data != null ? data.length - offset() : 0; + } + + private int offset() { + String identifier = identifier(); + + return identifier == null ? 0 : identifier.length() + 1; + } + + @Override + public String toString() { + String identifier = identifier(); + + if (identifier != null) { + return String.format("JPEGSegment[%04x/%s size: %d]", marker, identifier, segmentLength()); + } + + return String.format("JPEGSegment[%04x size: %d]", marker, segmentLength()); + } + + @Override + public int hashCode() { + String identifier = identifier(); + + return marker() << 16 | (identifier != null ? identifier.hashCode() : 0) & 0xFFFF; + } + + @Override + public boolean equals(final Object other) { + return other instanceof JPEGSegment && + ((JPEGSegment) other).marker == marker && Arrays.equals(((JPEGSegment) other).data, data); + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java new file mode 100644 index 00000000..6372737b --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.psd.PSDReader; +import com.twelvemonkeys.imageio.metadata.xmp.XMP; +import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import java.io.*; +import java.nio.charset.Charset; +import java.util.*; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * JPEGSegmentUtil + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGSegmentUtil.java,v 1.0 24.01.11 17.37 haraldk Exp$ + */ +public final class JPEGSegmentUtil { + public static final List ALL_IDS = Collections.unmodifiableList(new AllIdsList()); + public static final Map> ALL_SEGMENTS = Collections.unmodifiableMap(new AllSegmentsMap()); + public static final Map> APP_SEGMENTS = Collections.unmodifiableMap(new AllAppSegmentsMap()); + + private JPEGSegmentUtil() {} + + /** + * Reads the requested JPEG segments from the stream. + * The stream position must be directly before the SOI marker, and only segments for the current image is read. + * + * @param stream the stream to read from. + * @param marker the segment marker to read + * @param identifier the identifier to read, or {@code null} to match any segment + * @return a list of segments with the given app marker and optional identifier. If no segments are found, an + * empty list is returned. + * @throws IIOException if a JPEG format exception occurs during reading + * @throws IOException if an I/O exception occurs during reading + */ + public static List readSegments(final ImageInputStream stream, final int marker, final String identifier) throws IOException { + return readSegments(stream, Collections.singletonMap(marker, identifier != null ? Collections.singletonList(identifier) : ALL_IDS)); + } + + /** + * Reads the requested JPEG segments from the stream. + * The stream position must be directly before the SOI marker, and only segments for the current image is read. + * + * @param stream the stream to read from. + * @param segmentIdentifiers the segment identifiers + * @return a list of segments with the given app markers and optional identifiers. If no segments are found, an + * empty list is returned. + * @throws IIOException if a JPEG format exception occurs during reading + * @throws IOException if an I/O exception occurs during reading + * + * @see #ALL_SEGMENTS + * @see #APP_SEGMENTS + * @see #ALL_IDS + */ + public static List readSegments(final ImageInputStream stream, final Map> segmentIdentifiers) throws IOException { + readSOI(notNull(stream, "stream")); + + List segments = Collections.emptyList(); + + JPEGSegment segment; + try { + while (!isImageDone(segment = readSegment(stream, segmentIdentifiers))) { +// System.err.println("segment: " + segment); + + if (isRequested(segment, segmentIdentifiers)) { + if (segments == Collections.EMPTY_LIST) { + segments = new ArrayList(); + } + + segments.add(segment); + } + } + } + catch (EOFException ignore) { + // Just end here, in case of malformed stream + } + + // TODO: Should probably skip until EOI, so that multiple invocations succeeds for multiple image streams. + + return segments; + } + + private static boolean isRequested(JPEGSegment segment, Map> segmentIdentifiers) { + return (segmentIdentifiers.containsKey(segment.marker) && + (segment.identifier() == null && segmentIdentifiers.get(segment.marker) == null || containsSafe(segment, segmentIdentifiers))); + } + + private static boolean containsSafe(JPEGSegment segment, Map> segmentIdentifiers) { + List identifiers = segmentIdentifiers.get(segment.marker); + return identifiers != null && identifiers.contains(segment.identifier()); + } + + private static boolean isImageDone(final JPEGSegment segment) { + // We're done with this image if we encounter a SOS, EOI (or a new SOI, but that should never happen) + return segment.marker == JPEG.SOS || segment.marker == JPEG.EOI || segment.marker == JPEG.SOI; + } + + static String asNullTerminatedAsciiString(final byte[] data, final int offset) { + for (int i = 0; i < data.length - offset; i++) { + if (data[offset + i] == 0 || i > 255) { + return asAsciiString(data, offset, offset + i); + } + } + + return null; + } + + static String asAsciiString(final byte[] data, final int offset, final int length) { + return new String(data, offset, length, Charset.forName("ascii")); + } + + static void readSOI(final ImageInputStream stream) throws IOException { + if (stream.readUnsignedShort() != JPEG.SOI) { + throw new IIOException("Not a JPEG stream"); + } + } + + static JPEGSegment readSegment(final ImageInputStream stream, Map> segmentIdentifiers) throws IOException { + int marker = stream.readUnsignedShort(); + int length = stream.readUnsignedShort(); // Length including length field itself + + byte[] data; + + if (segmentIdentifiers.containsKey(marker)) { + data = new byte[length - 2]; + stream.readFully(data); + } + else { + if (JPEGSegment.isAppSegmentMarker(marker)) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(32); + int read; + + // NOTE: Read until null-termination (0) or EOF + while ((read = stream.read()) > 0) { + buffer.write(read); + } + + data = buffer.toByteArray(); + + stream.skipBytes(length - 3 - data.length); + } + else { + data = null; + stream.skipBytes(length - 2); + } + } + + return new JPEGSegment(marker, data, length); + } + + private static class AllIdsList extends ArrayList { + @Override + public String toString() { + return "[All ids]"; + } + + @Override + public boolean contains(Object o) { + return true; + } + } + + private static class AllSegmentsMap extends HashMap> { + @Override + public String toString() { + return "{All segments}"; + } + + @Override + public List get(Object key) { + return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key) ? ALL_IDS : null; + + } + + @Override + public boolean containsKey(Object key) { + return true; + } + } + + private static class AllAppSegmentsMap extends HashMap> { + @Override + public String toString() { + return "{All APPn segments}"; + } + + @Override + public List get(Object key) { + return containsKey(key) ? ALL_IDS : null; + + } + + @Override + public boolean containsKey(Object key) { + return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key); + } + } + + public static void main(String[] args) throws IOException { + List segments = readSegments(ImageIO.createImageInputStream(new File(args[0])), ALL_SEGMENTS); + + for (JPEGSegment segment : segments) { + System.err.println("segment: " + segment); + + if ("Exif".equals(segment.identifier())) { + InputStream data = segment.data(); + //noinspection ResultOfMethodCallIgnored + data.read(); // Pad + + ImageInputStream stream = ImageIO.createImageInputStream(data); + + // Root entry is TIFF, that contains the EXIF sub-IFD + Directory tiff = new EXIFReader().read(stream); + System.err.println("EXIF: " + tiff); + } + else if (XMP.NS_XAP.equals(segment.identifier())) { + Directory xmp = new XMPReader().read(ImageIO.createImageInputStream(segment.data())); + System.err.println("XMP: " + xmp); + } + else if ("Photoshop 3.0".equals(segment.identifier())) { + // TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain + // IPTC metadata. Probably duplicated in the XMP though... + ImageInputStream stream = ImageIO.createImageInputStream(segment.data()); + Directory psd = new PSDReader().read(stream); + System.err.println("PSD: " + psd); + } + else if ("ICC_PROFILE".equals(segment.identifier())) { + // Skip + } + else { + System.err.println(EXIFReader.HexDump.dump(segment.data)); + } + } + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java new file mode 100644 index 00000000..4a1b95dc --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +/** + * PSD + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PSD.java,v 1.0 24.01.12 16:51 haraldk Exp$ + */ +interface PSD { + static final int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M'; + + static final int RES_IPTC_NAA = 0x0404; +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDDirectory.java new file mode 100644 index 00000000..cd9af244 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDDirectory.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +import com.twelvemonkeys.imageio.metadata.AbstractDirectory; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** + * PhotoshopDirectory + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PhotoshopDirectory.java,v 1.0 04.01.12 11:58 haraldk Exp$ + */ +final class PSDDirectory extends AbstractDirectory { + public PSDDirectory(final Collection entries) { + super(entries); + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java new file mode 100644 index 00000000..bd444e39 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntry.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +import com.twelvemonkeys.imageio.metadata.AbstractEntry; +import com.twelvemonkeys.lang.StringUtil; + +/** + * PhotoshopEntry + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PhotoshopEntry.java,v 1.0 04.01.12 11:58 haraldk Exp$ + */ +class PSDEntry extends AbstractEntry { + private final String name; + + public PSDEntry(final int resourceId, String name, final Object value) { + super(resourceId, value); + this.name = StringUtil.isEmpty(name) ? null : name; + } + + @Override + protected String getNativeIdentifier() { + return String.format("0x%04x", (Integer) getIdentifier()); + } + + @Override + public String getFieldName() { + return name; + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java new file mode 100644 index 00000000..a9c9cb92 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.MetadataReader; +import com.twelvemonkeys.imageio.stream.SubImageInputStream; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.io.DataInput; +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * PhotoshopReader + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PhotoshopReader.java,v 1.0 04.01.12 11:56 haraldk Exp$ + */ +public final class PSDReader extends MetadataReader { + + // TODO: Add constructor to allow optional parsing of resources + // TODO: Maybe this should be modelled more like the JPEG segment parsing, as it's all binary data... + // - Segment/SegmentReader + List + + @Override + public Directory read(final ImageInputStream input) throws IOException { + Validate.notNull(input, "input"); + + List entries = new ArrayList(); + + while (true) { + try { + int type = input.readInt(); + + if (type != PSD.RESOURCE_TYPE) { + throw new IIOException(String.format("Wrong image resource type, expected '8BIM': '%08x'", type)); + } + + short id = input.readShort(); + + PSDResource resource = new PSDResource(id, input); + entries.add(new PSDEntry(id, resource.name(), resource.data())); + + } + catch (EOFException e) { + break; + } + } + + return new PSDDirectory(entries); + } + + protected static class PSDResource { + static String readPascalString(final DataInput pInput) throws IOException { + int length = pInput.readUnsignedByte(); + + if (length == 0) { + return ""; + } + + byte[] bytes = new byte[length]; + pInput.readFully(bytes); + + return StringUtil.decode(bytes, 0, bytes.length, "ASCII"); + } + + final short id; + final String name; + final long size; + + byte[] data; + + PSDResource(final short resourceId, final ImageInputStream input) throws IOException { + id = resourceId; + + name = readPascalString(input); + + // Skip pad + int nameSize = name.length() + 1; + if (nameSize % 2 != 0) { + input.readByte(); + } + + size = input.readUnsignedInt(); + long startPos = input.getStreamPosition(); + + readData(new SubImageInputStream(input, size)); + + // NOTE: This should never happen, however it's safer to keep it here for future compatibility + if (input.getStreamPosition() != startPos + size) { + input.seek(startPos + size); + } + + // Data is even-padded (word aligned) + if (size % 2 != 0) { + input.read(); + } + } + + protected void readData(final ImageInputStream pInput) throws IOException { + // TODO: This design is ugly, as subclasses readData is invoked BEFORE their respective constructor... + data = new byte[(int) size]; + pInput.readFully(data); + } + + public final byte[] data() { + return data; + } + + public String name() { + return name; + } + + @Override + public String toString() { + StringBuilder builder = toStringBuilder(); + + builder.append(", data length: "); + builder.append(size); + builder.append("]"); + + return builder.toString(); + } + + protected StringBuilder toStringBuilder() { + StringBuilder builder = new StringBuilder(getClass().getSimpleName()); + + builder.append("[ID: 0x"); + builder.append(Integer.toHexString(id)); + if (name != null && name.trim().length() != 0) { + builder.append(", name: \""); + builder.append(name); + builder.append("\""); + } + + return builder; + } + } + +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/RDFDescription.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/RDFDescription.java new file mode 100644 index 00000000..2e6c7eda --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/RDFDescription.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.xmp; + +import com.twelvemonkeys.imageio.metadata.AbstractDirectory; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** +* RDFDescription +* +* @author Harald Kuhr +* @author last modified by $Author: haraldk$ +* @version $Id: RDFDescription.java,v 1.0 Nov 17, 2009 9:38:58 PM haraldk Exp$ +*/ +final class RDFDescription extends AbstractDirectory { + private final String namespace; + + // TODO: Store size of root directory, to allow serializing + // TODO: XMPDirectory, maybe not even an AbstractDirectory + // - Keeping the Document would allow for easier serialization + // TODO: Or use direct SAX parsing + public RDFDescription(Collection entries) { + this(null, entries); + } + + public RDFDescription(String key, Collection entries) { + super(entries); + + namespace = key; + } + + @Override + public String toString() { + return namespace != null ? + super.toString().replaceAll("^RDFDescription\\[", String.format("%s[%s|%s, ", getClass().getSimpleName(), XMP.DEFAULT_NS_MAPPING.get(namespace), namespace)) : + super.toString(); + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMP.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMP.java index d36cc6c4..c624afe6 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMP.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMP.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.metadata.xmp; import java.util.Collections; import java.util.Map; +import java.util.Set; /** * XMP @@ -59,6 +60,10 @@ public interface XMP { String NS_XAP_MM = "http://ns.adobe.com/xap/1.0/mm/"; + String NS_X = "adobe:ns:meta/"; + /** Contains the mapping from URI to default namespace prefix. */ - Map DEFAULT_NS_MAPPING = Collections.unmodifiableMap(new XMPNamespaceMapping()); + Map DEFAULT_NS_MAPPING = Collections.unmodifiableMap(new XMPNamespaceMapping(true)); + + Set ELEMENTS = Collections.unmodifiableSet(new XMPNamespaceMapping(false).keySet()); } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectory.java index 8f4d53a2..565f7d76 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectory.java @@ -28,10 +28,10 @@ package com.twelvemonkeys.imageio.metadata.xmp; -import com.twelvemonkeys.imageio.metadata.AbstractDirectory; -import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.AbstractCompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; -import java.util.List; +import java.util.Collection; /** * XMPDirectory @@ -40,12 +40,30 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: XMPDirectory.java,v 1.0 Nov 17, 2009 9:38:58 PM haraldk Exp$ */ -final class XMPDirectory extends AbstractDirectory { - // TODO: Store size of root directory, to allow serializing +final class XMPDirectory extends AbstractCompoundDirectory { + // TODO: Allow lookup of directories by namespace? + // TODO: Allow merge/sync/comparison with IPTC/EXIF/TIFF metadata + // TODO: Store size of root directory, to allow easy serializing (see isReadOnly comment) // TODO: XMPDirectory, maybe not even an AbstractDirectory // - Keeping the Document would allow for easier serialization // TODO: Or use direct SAX parsing - public XMPDirectory(List pEntries) { - super(pEntries); + private final String toolkit; + + public XMPDirectory(Collection entries, String toolkit) { + super(entries); + + this.toolkit = toolkit; + } + + // TODO: Expose x:xmptk (getXMPToolkit(): String) + /*public*/ String getWriterToolkit() { + return toolkit; + } + + @Override + public boolean isReadOnly() { + // TODO: Depend on / for writable/read-only respectively? + // Spec says allow writing (even if "r"), if the container format is understood (ie. single file, known format, update checksums etc) + return super.isReadOnly(); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java index 202a0ce7..c4c7f1da 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java @@ -38,20 +38,36 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry; * @version $Id: XMPEntry.java,v 1.0 Nov 17, 2009 9:38:39 PM haraldk Exp$ */ final class XMPEntry extends AbstractEntry { - private final String mFieldName; + private final String fieldName; - public XMPEntry(final String pIdentifier, final Object pValue) { - this(pIdentifier, null, pValue); + // TODO: Rewrite to use namespace + field instead of identifier (for the nativeIdentifier) method + public XMPEntry(final String identifier, final Object pValue) { + this(identifier, null, pValue); } - public XMPEntry(final String pIdentifier, final String pFieldName, final Object pValue) { - super(pIdentifier, pValue); - mFieldName = pFieldName; + public XMPEntry(final String identifier, final String fieldName, final Object value) { + super(identifier, value); + this.fieldName = fieldName; + } + + @Override + protected String getNativeIdentifier() { + String identifier = (String) getIdentifier(); + String namespace = fieldName != null && identifier.endsWith(fieldName) ? XMP.DEFAULT_NS_MAPPING.get(identifier.substring(0, identifier.length() - fieldName.length())) : null; + return namespace != null ? namespace + ":" + fieldName : identifier; } @SuppressWarnings({"SuspiciousMethodCalls"}) @Override public String getFieldName() { - return mFieldName != null ? mFieldName : XMP.DEFAULT_NS_MAPPING.get(getIdentifier()); + return fieldName != null ? fieldName : XMP.DEFAULT_NS_MAPPING.get(getIdentifier()); + } + + @Override + public String toString() { + String type = getTypeName(); + String typeStr = type != null ? " (" + type + ")" : ""; + + return String.format("%s: %s%s", getNativeIdentifier(), getValueAsString(), typeStr); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPNamespaceMapping.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPNamespaceMapping.java index 0170acb3..af1729c9 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPNamespaceMapping.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPNamespaceMapping.java @@ -38,8 +38,12 @@ import java.util.HashMap; * @version $Id: XMPNamespaceMapping.java,v 1.0 Nov 17, 2009 6:35:21 PM haraldk Exp$ */ final class XMPNamespaceMapping extends HashMap { - public XMPNamespaceMapping() { - put(XMP.NS_RDF, "rdf"); + public XMPNamespaceMapping(boolean includeNonElements) { + if (includeNonElements) { + put(XMP.NS_RDF, "rdf"); + put(XMP.NS_X, "x"); + } + put(XMP.NS_DC, "dc"); put(XMP.NS_EXIF, "exif"); put(XMP.NS_PHOTOSHOP, "photoshop"); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java index c1c46486..e43d7557 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java @@ -32,6 +32,7 @@ import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.MetadataReader; import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.lang.Validate; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; @@ -55,17 +56,13 @@ import java.util.*; * @version $Id: XMPReader.java,v 1.0 Nov 14, 2009 11:04:30 PM haraldk Exp$ */ public final class XMPReader extends MetadataReader { + // See http://www.scribd.com/doc/56852716/XMPSpecificationPart1 + + // TODO: Types? Probably defined in XMP/RDF XML schema. Or are we happy that everything is a string? + @Override - public Directory read(final ImageInputStream pInput) throws IOException { -// pInput.mark(); -// -// BufferedReader reader = new BufferedReader(new InputStreamReader(IIOUtil.createStreamAdapter(pInput), Charset.forName("UTF-8"))); -// String line; -// while ((line = reader.readLine()) != null) { -// System.out.println(line); -// } -// -// pInput.reset(); + public Directory read(final ImageInputStream input) throws IOException { + Validate.notNull(input, "input"); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); @@ -75,19 +72,16 @@ public final class XMPReader extends MetadataReader { // TODO: Determine encoding and parse using a Reader... // TODO: Refactor scanner to return inputstream? DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(pInput))); + Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(input))); // XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding")); // serializer.serialize(document); - - // Each rdf:Description is a Directory (but we can't really rely on that structure.. it's only convention) - // - Each element inside the rdf:Desc is an Entry - + String toolkit = getToolkit(document); Node rdfRoot = document.getElementsByTagNameNS(XMP.NS_RDF, "RDF").item(0); NodeList descriptions = document.getElementsByTagNameNS(XMP.NS_RDF, "Description"); - return parseDirectories(rdfRoot, descriptions); + return parseDirectories(rdfRoot, descriptions, toolkit); } catch (SAXException e) { throw new IIOException(e.getMessage(), e); @@ -97,7 +91,19 @@ public final class XMPReader extends MetadataReader { } } - private XMPDirectory parseDirectories(final Node pParentNode, NodeList pNodes) { + private String getToolkit(Document document) { + NodeList xmpmeta = document.getElementsByTagNameNS(XMP.NS_X, "xmpmeta"); + + if (xmpmeta == null || xmpmeta.getLength() <= 0) { + return null; + } + + Node toolkit = xmpmeta.item(0).getAttributes().getNamedItemNS(XMP.NS_X, "xmptk"); + + return toolkit != null ? toolkit.getNodeValue() : null; + } + + private XMPDirectory parseDirectories(final Node pParentNode, NodeList pNodes, String toolkit) { Map> subdirs = new LinkedHashMap>(); for (Node desc : asIterable(pNodes)) { @@ -105,6 +111,9 @@ public final class XMPReader extends MetadataReader { continue; } + // Support attribute short-hand syntax + parseAttributesForKnownElements(subdirs, desc); + for (Node node : asIterable(desc.getChildNodes())) { if (node.getNodeType() != Node.ELEMENT_NODE) { continue; @@ -121,6 +130,7 @@ public final class XMPReader extends MetadataReader { Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType"); if (parseType != null && "Resource".equals(parseType.getNodeValue())) { + // See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource List entries = new ArrayList(); for (Node child : asIterable(node.getChildNodes())) { @@ -130,60 +140,119 @@ public final class XMPReader extends MetadataReader { entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child))); } - value = new XMPDirectory(entries); + + value = new RDFDescription(entries); } else { - // TODO: Support alternative RDF syntax (short-form), using attributes on desc -// NamedNodeMap attributes = node.getAttributes(); -// -// for (Node attr : asIterable(attributes)) { -// System.out.println("attr.getNodeName(): " + attr.getNodeName()); -// System.out.println("attr.getNodeValue(): " + attr.getNodeValue()); -// } + // TODO: This method contains loads of duplication an should be cleaned up... + // Support attribute short-hand syntax + Map> subsubdirs = new LinkedHashMap>(); - value = getChildTextValue(node); + parseAttributesForKnownElements(subsubdirs, node); + + if (!subsubdirs.isEmpty()) { + List entries = new ArrayList(); + + for (Map.Entry> entry : subsubdirs.entrySet()) { + entries.addAll(entry.getValue()); + } + + value = new RDFDescription(entries); + } + else { + value = getChildTextValue(node); + } } - XMPEntry entry = new XMPEntry(node.getNamespaceURI() + node.getLocalName(), node.getLocalName(), value); - dir.add(entry); + dir.add(new XMPEntry(node.getNamespaceURI() + node.getLocalName(), node.getLocalName(), value)); } } - // TODO: Consider flattening the somewhat artificial directory structure - List entries = new ArrayList(); + List entries = new ArrayList(); + // TODO: Should we still allow asking for a subdirectory by item id? for (Map.Entry> entry : subdirs.entrySet()) { - entries.add(new XMPEntry(entry.getKey(), new XMPDirectory(entry.getValue()))); + entries.add(new RDFDescription(entry.getKey(), entry.getValue())); } - return new XMPDirectory(entries); + return new XMPDirectory(entries, toolkit); } - private Object getChildTextValue(Node node) { - Object value; - Node child = node.getFirstChild(); + private void parseAttributesForKnownElements(Map> subdirs, Node desc) { + // NOTE: NamedNodeMap does not have any particular order... + NamedNodeMap attributes = desc.getAttributes(); - String strVal = null; - if (child != null) { - strVal = child.getNodeValue(); + for (Node attr : asIterable(attributes)) { + if (!XMP.ELEMENTS.contains(attr.getNamespaceURI())) { + continue; + } + + List dir = subdirs.get(attr.getNamespaceURI()); + + if (dir == null) { + dir = new ArrayList(); + subdirs.put(attr.getNamespaceURI(), dir); + } + + dir.add(new XMPEntry(attr.getNamespaceURI() + attr.getLocalName(), attr.getLocalName(), attr.getNodeValue())); + } + } + + private Object getChildTextValue(final Node node) { + for (Node child : asIterable(node.getChildNodes())) { + if (XMP.NS_RDF.equals(child.getNamespaceURI()) && "Alt".equals(child.getLocalName())) { + // Support for -> return a Map (keyed on xml:lang?) + Map alternatives = new LinkedHashMap(); + for (Node alternative : asIterable(child.getChildNodes())) { + if (XMP.NS_RDF.equals(alternative.getNamespaceURI()) && "li".equals(alternative.getLocalName())) { + //return getChildTextValue(alternative); + NamedNodeMap attributes = alternative.getAttributes(); + Node key = attributes.getNamedItem("xml:lang"); + + alternatives.put(key.getTextContent(), getChildTextValue(alternative)); + } + } + + return alternatives; + } + else if (XMP.NS_RDF.equals(child.getNamespaceURI()) && ("Seq".equals(child.getLocalName()) || "Bag".equals(child.getLocalName()))) { + // Support for -> return array + // Support for -> return array/unordered collection (how can a serialized collection not have order?) + List seq = new ArrayList(); + + for (Node sequence : asIterable(child.getChildNodes())) { + if (XMP.NS_RDF.equals(sequence.getNamespaceURI()) && "li".equals(sequence.getLocalName())) { + Object value = getChildTextValue(sequence); + seq.add(value); + } + } + + // TODO: Strictly a bag should not be a list, but there's no Bag type (or similar) in Java. + // Consider something like Google collections Multiset or Apache commons Bag (the former seems more well-defined) + // Note: Collection does not have defined equals() semantics, and so using + // Collections.unmodifiableCollection() doesn't work for comparing values (uses Object.equals()) + return Collections.unmodifiableList(seq); + } } - value = strVal != null ? strVal.trim() : ""; - return value; + Node child = node.getFirstChild(); + String strVal = child != null ? child.getNodeValue() : null; + + return strVal != null ? strVal.trim() : ""; } private Iterable asIterable(final NamedNodeMap pNodeList) { return new Iterable() { public Iterator iterator() { return new Iterator() { - private int mIndex; + private int index; public boolean hasNext() { - return pNodeList != null && pNodeList.getLength() > mIndex; + return pNodeList != null && pNodeList.getLength() > index; } public Node next() { - return pNodeList.item(mIndex++); + return pNodeList.item(index++); } public void remove() { @@ -198,14 +267,14 @@ public final class XMPReader extends MetadataReader { return new Iterable() { public Iterator iterator() { return new Iterator() { - private int mIndex; + private int index; public boolean hasNext() { - return pNodeList != null && pNodeList.getLength() > mIndex; + return pNodeList != null && pNodeList.getLength() > index; } public Node next() { - return pNodeList.item(mIndex++); + return pNodeList.item(index++); } public void remove() { @@ -215,5 +284,4 @@ public final class XMPReader extends MetadataReader { } }; } - } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScanner.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScanner.java index 07f2b2fa..93f941c0 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScanner.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScanner.java @@ -140,8 +140,8 @@ public final class XMPScanner { // UTF 32 BIG endian cs = Charset.forName("UTF-32BE"); } - else if (bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE && bom[2] == 0x00 && bom[3] == 0x00) { - // TODO: FixMe.. + else if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == 0x00 && bom[3] == (byte) 0xFF && stream.read() == 0xFE) { + stream.skipBytes(2); // Alignment // NOTE: 32-bit character set not supported by default // UTF 32 little endian cs = Charset.forName("UTF-32LE"); @@ -220,8 +220,6 @@ public final class XMPScanner { return -1l; } - //static public XMPDirectory parse(input); - public static void main(final String[] pArgs) throws IOException { ImageInputStream stream = ImageIO.createImageInputStream(new File(pArgs[0])); @@ -236,8 +234,5 @@ public final class XMPScanner { } stream.close(); -// else { -// System.err.println("XMP not found"); -// } } } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractCompoundDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractCompoundDirectoryTest.java new file mode 100644 index 00000000..23b783af --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractCompoundDirectoryTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import java.util.Collection; + +/** + * AbstractCompoundDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractCompoundDirectoryTest.java,v 1.0 02.01.12 15:07 haraldk Exp$ + */ +public class AbstractCompoundDirectoryTest extends CompoundDirectoryAbstractTest { + @Override + protected CompoundDirectory createCompoundDirectory(final Collection directories) { + return new AbstractCompoundDirectory(directories) {}; + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractDirectoryTest.java new file mode 100644 index 00000000..64de19ea --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractDirectoryTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import java.util.Collection; + +/** + * AbstractDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractDirectoryTest.java,v 1.0 02.01.12 15:07 haraldk Exp$ + */ +public class AbstractDirectoryTest extends DirectoryAbstractTest { + @Override + protected Directory createDirectory(final Collection entries) { + return new AbstractDirectory(entries) {}; + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractEntryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractEntryTest.java new file mode 100644 index 00000000..08cff7dd --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/AbstractEntryTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * AbstractEntryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractEntryTest.java,v 1.0 02.01.12 17:22 haraldk Exp$ + */ +public class AbstractEntryTest extends EntryAbstractTest { + @Override + protected final Entry createEntry(final Object value) { + return createEntry("foo", value); + } + + private AbstractEntry createEntry(final String identifier, final Object value) { + return new AbstractEntry(identifier, value) {}; + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateEntryNullId() { + createEntry(null, new Object()); + } + + @Test + public void testAbstractEntry() { + Object value = new Object(); + Entry entry = createEntry("foo", value); + + assertEquals("foo", entry.getIdentifier()); + assertNull(entry.getFieldName()); + assertSame(value, entry.getValue()); + assertNotNull(entry.getValueAsString()); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/CompoundDirectoryAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/CompoundDirectoryAbstractTest.java new file mode 100644 index 00000000..1a58dba7 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/CompoundDirectoryAbstractTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +/** + * CompoundDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CompoundDirectoryTest.java,v 1.0 02.01.12 15:07 haraldk Exp$ + */ +public abstract class CompoundDirectoryAbstractTest extends DirectoryAbstractTest { + protected abstract CompoundDirectory createCompoundDirectory(Collection directories); + + // Override by subclasses that require special kind of directory + protected Directory createSingleDirectory(final Collection entries) { + return new TestDirectory(entries); + } + + @Override + protected final Directory createDirectory(final Collection entries) { + // A compound directory should behave like a normal directory + return createCompoundDirectory(Collections.singleton(createSingleDirectory(entries))); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNullDirectories() { + createCompoundDirectory(Collections.singleton(null)); + } + + @Test + public void testSingle() { + Directory only = createSingleDirectory(null); + + CompoundDirectory directory = createCompoundDirectory(Collections.singleton(only)); + + assertEquals(1, directory.directoryCount()); + assertSame(only, directory.getDirectory(0)); + } + + @Test + public void testMultiple() { + Directory one = createSingleDirectory(null); + Directory two = createSingleDirectory(null); + Directory three = createSingleDirectory(null); + + CompoundDirectory directory = createCompoundDirectory(Arrays.asList(one, two, three)); + + assertEquals(3, directory.directoryCount()); + assertSame(one, directory.getDirectory(0)); + assertSame(two, directory.getDirectory(1)); + assertSame(three, directory.getDirectory(2)); + } + + @Test + public void testEntries() { + Directory one = createSingleDirectory(null); + Directory two = createSingleDirectory(null); + Directory three = createSingleDirectory(null); + + CompoundDirectory directory = createCompoundDirectory(Arrays.asList(one, two, three)); + + assertEquals(3, directory.directoryCount()); + assertSame(one, directory.getDirectory(0)); + assertSame(two, directory.getDirectory(1)); + assertSame(three, directory.getDirectory(2)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testOutOfBounds() { + Directory only = createSingleDirectory(null); + CompoundDirectory directory = createCompoundDirectory(Collections.singleton(only)); + + directory.getDirectory(directory.directoryCount()); + } + + protected static final class TestDirectory extends AbstractDirectory { + public TestDirectory(final Collection entries) { + super(entries); + } + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/DirectoryAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/DirectoryAbstractTest.java new file mode 100644 index 00000000..44bac310 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/DirectoryAbstractTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + +/** + * DirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: DirectoryTest.java,v 1.0 02.01.12 15:07 haraldk Exp$ + */ +public abstract class DirectoryAbstractTest extends ObjectAbstractTestCase { + + @Override + protected Object makeObject() { + return createDirectory(Collections.singleton(createEntry("entry", null))); + } + + protected abstract Directory createDirectory(Collection entries); + + // Override by subclasses that requires special type of entries + protected Entry createEntry(final String identifier, final Object value) { + return new TestEntry(identifier, value); + } + + @Test + public void testCreateNull() { + Directory directory = createDirectory(null); + + assertEquals(0, directory.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNullEntries() { + createDirectory(Collections.singleton(null)); + } + + @Test + public void testSingleEntry() { + Entry entry = createEntry("foo", new Object()); + Directory directory = createDirectory(Collections.singleton(entry)); + + assertEquals(1, directory.size()); + assertSame(entry, directory.getEntryById("foo")); + } + + @Test + public void testNonExistentEntry() { + Entry entry = createEntry("foo", new Object()); + Directory directory = createDirectory(Collections.singleton(entry)); + assertNull(directory.getEntryById("bar")); + } + + @Test + public void testMultipleEntries() { + Entry one = createEntry("foo", new Object()); + Entry two = createEntry("bar", new Object()); + + Directory directory = createDirectory(Arrays.asList(one, two)); + + assertEquals(2, directory.size()); + + assertSame(one, directory.getEntryById("foo")); + assertSame(two, directory.getEntryById("bar")); + } + + @Test + public void testEmptyIterator() { + Directory directory = createDirectory(null); + + assertEquals(0, directory.size()); + + assertFalse(directory.iterator().hasNext()); + } + + @Test + public void testSingleIterator() { + Entry one = createEntry("foo", new Object()); + + Directory directory = createDirectory(Arrays.asList(one)); + + int count = 0; + for (Entry entry : directory) { + assertSame(one, entry); + count++; + } + + assertEquals(1, count); + } + + @Test + public void testIteratorMutability() { + Entry one = createEntry("foo", new Object()); + Entry two = createEntry("bar", new Object()); + + Directory directory = createDirectory(Arrays.asList(one, two)); + + assertEquals(2, directory.size()); + + Iterator entries = directory.iterator(); + if (!directory.isReadOnly()) { + while (entries.hasNext()) { + entries.next(); + entries.remove(); + } + + assertEquals(0, directory.size()); + } + else { + while (entries.hasNext()) { + try { + entries.next(); + entries.remove(); + fail("Expected UnsupportedOperationException"); + } + catch (UnsupportedOperationException expected) { + } + } + + assertEquals(2, directory.size()); + } + } + + @Test + public void testMultipleIterator() { + Entry one = createEntry("foo", new Object()); + Entry two = createEntry("bar", new Object()); + Entry three = createEntry("baz", new Object()); + + List all = Arrays.asList(one, two, three); + Directory directory = createDirectory(all); + + // Test that each element is contained, and only once + List entries = new ArrayList(all); + + int count = 0; + for (Entry entry : directory) { + assertTrue(entries.contains(entry)); + assertTrue(entries.remove(entry)); + + count++; + } + + assertTrue(entries.isEmpty()); + assertEquals(3, count); + } + + protected static class TestEntry extends AbstractEntry { + public TestEntry(final String identifier, final Object value) { + super(identifier, value); + } + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/EntryAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/EntryAbstractTest.java new file mode 100644 index 00000000..c98a6546 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/EntryAbstractTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * EntryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: EntryTest.java,v 1.0 02.01.12 17:33 haraldk Exp$ + */ +public abstract class EntryAbstractTest extends ObjectAbstractTestCase { + @Override + protected Object makeObject() { + return createEntry(new Object()); + } + + protected abstract Entry createEntry(Object value); + + @Test + public void testCreateEntryNullValue() { + Entry foo = createEntry(null); + + assertNotNull(foo.getIdentifier()); + assertEquals(null, foo.getValue()); + + assertEquals("null", foo.getValueAsString()); + } + + @Test + public void testEntryStringValue() { + Entry foo = createEntry("bar"); + + assertNotNull(foo.getIdentifier()); + assertEquals("bar", foo.getValue()); + assertEquals("bar", foo.getValueAsString()); + } + + @Test + public void testEntryValue() { + Entry foo = createEntry(77); + + assertNotNull(foo.getIdentifier()); + assertEquals(77, foo.getValue()); + assertEquals("77", foo.getValueAsString()); + } + + @Test + public void testNullValueHashCode() { + Entry foo = createEntry(null); + + // Doesn't really matter, as long as it doesn't throw NPE, but this should hold for all entries + assertNotSame(0, foo.hashCode()); + } + + @Test + public void testArrayValueHashCode() { + // Doesn't really matter, as long as it doesn't throw NPE, but this should hold for all entries + assertNotSame(0, createEntry(new int[0]).hashCode()); + assertNotSame(0, createEntry(new int[1]).hashCode()); + } + + @Test + public void testArrayValue() { + int[] array = {42, -1, 77, 99, 55}; + Entry foo = createEntry(array); + + assertEquals(5, foo.valueCount()); + assertArrayEquals(array, (int[]) foo.getValue()); + + // Not strictly necessary, but nice + assertEquals(Arrays.toString(array), foo.getValueAsString()); + } + + @Test + public void testCharArrayValue() { + char[] array = {'f', '0', '0'}; + Entry foo = createEntry(array); + + assertEquals(3, foo.valueCount()); + assertArrayEquals(array, (char[]) foo.getValue()); + + // Not strictly necessary, but nice + assertEquals("f00", foo.getValueAsString()); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java new file mode 100644 index 00000000..6f220006 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata; + +import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.junit.internal.matchers.TypeSafeMatcher; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * ReaderAbstractTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ReaderAbstractTest.java,v 1.0 04.01.12 09:40 haraldk Exp$ + */ +public abstract class MetadataReaderAbstractTest { + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + } + + protected final URL getResource(final String name) throws IOException { + return getClass().getResource(name); + } + + protected final ImageInputStream getDataAsIIS() throws IOException { + return ImageIO.createImageInputStream(getData()); + } + + protected abstract InputStream getData() throws IOException; + + protected abstract MetadataReader createReader(); + + @Test(expected = IllegalArgumentException.class) + public void testReadNull() throws IOException { + createReader().read(null); + } + + @Test + public void testRead() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + assertNotNull(directory); + } + + protected final Matcher hasValue(final Object value) { + return new EntryHasValue(value); + } + + private static class EntryHasValue extends TypeSafeMatcher { + private final Object value; + + public EntryHasValue(final Object value) { + this.value = value; + } + + @Override + public boolean matchesSafely(final Entry entry) { + return entry != null && (value == null ? entry.getValue() == null : valueEquals(value, entry.getValue())); + } + + private static boolean valueEquals(final Object expected, final Object actual) { + return expected.getClass().isArray() ? arrayEquals(expected, actual) : expected.equals(actual); + } + + private static boolean arrayEquals(final Object expected, final Object actual) { + Class componentType = expected.getClass().getComponentType(); + + if (actual == null || !actual.getClass().isArray() || actual.getClass().getComponentType() != componentType) { + return false; + } + + return componentType.isPrimitive() ? primitiveArrayEquals(componentType, expected, actual) : Arrays.equals((Object[]) expected, (Object[]) actual); + } + + private static boolean primitiveArrayEquals(Class componentType, Object expected, Object actual) { + if (componentType == boolean.class) { + return Arrays.equals((boolean[]) expected, (boolean[]) actual); + } + else if (componentType == byte.class) { + return Arrays.equals((byte[]) expected, (byte[]) actual); + } + else if (componentType == char.class) { + return Arrays.equals((char[]) expected, (char[]) actual); + } + else if (componentType == double.class) { + return Arrays.equals((double[]) expected, (double[]) actual); + } + else if (componentType == float.class) { + return Arrays.equals((float[]) expected, (float[]) actual); + } + else if (componentType == int.class) { + return Arrays.equals((int[]) expected, (int[]) actual); + } + else if (componentType == long.class) { + return Arrays.equals((long[]) expected, (long[]) actual); + } + else if (componentType == short.class) { + return Arrays.equals((short[]) expected, (short[]) actual); + } + + throw new AssertionError("Unsupported type:" + componentType); + } + + public void describeTo(final Description description) { + description.appendText("has value "); + description.appendValue(value); + } + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectoryTest.java new file mode 100644 index 00000000..bc583c44 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectoryTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.CompoundDirectoryAbstractTest; +import com.twelvemonkeys.imageio.metadata.Directory; + +import java.util.Collection; + +/** + * EXIFDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: EXIFDirectoryTest.java,v 1.0 02.01.12 16:41 haraldk Exp$ + */ +public class EXIFDirectoryTest extends CompoundDirectoryAbstractTest { + @Override + protected CompoundDirectory createCompoundDirectory(Collection directories) { + return new EXIFDirectory(directories); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntryTest.java new file mode 100644 index 00000000..bc34b811 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntryTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.EntryAbstractTest; +import org.junit.Test; + +/** + * EXIFEntryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: EXIFEntryTest.java,v 1.0 02.01.12 17:35 haraldk Exp$ + */ +public class EXIFEntryTest extends EntryAbstractTest { + @Override + protected Entry createEntry(final Object value) { + return createEXIFEntry(TIFF.TAG_COPYRIGHT, value, (short) 2); + } + + private EXIFEntry createEXIFEntry(final int identifier, final Object value, final int type) { + return new EXIFEntry(identifier, value, (short) type); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateEXIFEntryIllegalType() { + createEXIFEntry(0, null, -1); + } + + // TODO: TIFF/EXIF specific tests +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java new file mode 100644 index 00000000..5e184bd4 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest; +import com.twelvemonkeys.imageio.stream.SubImageInputStream; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.*; + +/** + * EXIFReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: EXIFReaderTest.java,v 1.0 23.12.11 13:50 haraldk Exp$ + */ +public class EXIFReaderTest extends MetadataReaderAbstractTest { + @Override + protected InputStream getData() throws IOException { + return getResource("/exif/exif-jpeg-segment.bin").openStream(); + } + + @Override + protected EXIFReader createReader() { + return new EXIFReader(); + } + + @Test + public void testIsCompoundDirectory() throws IOException { + Directory exif = createReader().read(getDataAsIIS()); + assertThat(exif, instanceOf(CompoundDirectory.class)); + } + + @Test + public void testDirectory() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + assertEquals(2, exif.directoryCount()); + assertNotNull(exif.getDirectory(0)); + assertNotNull(exif.getDirectory(1)); + assertEquals(exif.size(), exif.getDirectory(0).size() + exif.getDirectory(1).size()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testDirectoryOutOfBounds() throws IOException { + InputStream data = getData(); + + CompoundDirectory exif = (CompoundDirectory) createReader().read(ImageIO.createImageInputStream(data)); + + assertEquals(2, exif.directoryCount()); + assertNotNull(exif.getDirectory(exif.directoryCount())); + } + + @Test + public void testEntries() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + // From IFD0 + assertNotNull(exif.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals("Adobe Photoshop CS2 Macintosh", exif.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + assertEquals(exif.getEntryById(TIFF.TAG_SOFTWARE), exif.getEntryByFieldName("Software")); + + // From IFD1 + assertNotNull(exif.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT)); + assertEquals((long) 418, exif.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT).getValue()); + assertEquals(exif.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT), exif.getEntryByFieldName("JPEGInterchangeFormat")); + } + + @Test + public void testIFD0() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + Directory ifd0 = exif.getDirectory(0); + assertNotNull(ifd0); + + assertNotNull(ifd0.getEntryById(TIFF.TAG_IMAGE_WIDTH)); + assertEquals(3601, ifd0.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()); + + assertNotNull(ifd0.getEntryById(TIFF.TAG_IMAGE_HEIGHT)); + assertEquals(4176, ifd0.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue()); + + // Assert 'uncompressed' (there's no TIFF image here, really) + assertNotNull(ifd0.getEntryById(TIFF.TAG_COMPRESSION)); + assertEquals(1, ifd0.getEntryById(TIFF.TAG_COMPRESSION).getValue()); + } + + @Test + public void testIFD1() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + Directory ifd1 = exif.getDirectory(1); + assertNotNull(ifd1); + + // Assert 'JPEG compression' (thumbnail only) + assertNotNull(ifd1.getEntryById(TIFF.TAG_COMPRESSION)); + assertEquals(6, ifd1.getEntryById(TIFF.TAG_COMPRESSION).getValue()); + + assertNull(ifd1.getEntryById(TIFF.TAG_IMAGE_WIDTH)); + assertNull(ifd1.getEntryById(TIFF.TAG_IMAGE_HEIGHT)); + } + + @Test + public void testReadBadDataZeroCount() throws IOException { + // This image seems to contain bad Exif. But as other tools are able to read, so should we.. + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); + stream.seek(12); + Directory directory = createReader().read(new SubImageInputStream(stream, 21674)); + + assertEquals(22, directory.size()); + + // Special case: Ascii string with count == 0, not ok according to spec (?), but we'll let it pass + assertEquals("", directory.getEntryById(TIFF.TAG_IMAGE_DESCRIPTION).getValue()); + } + + @Test + public void testReadBadDataRationalZeroDenominator() throws IOException { + // This image seems to contain bad Exif. But as other tools are able to read, so should we.. + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); + stream.seek(12); + Directory directory = createReader().read(new SubImageInputStream(stream, 21674)); + + // Special case: Rational with zero-denominator inside EXIF data + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + Entry entry = exif.getEntryById(EXIF.TAG_COMPRESSED_BITS_PER_PIXEL); + assertNotNull(entry); + assertEquals(Rational.NaN, entry.getValue()); + } + + @Test + public void testReadBadDirectoryCount() throws IOException { + // This image seems to contain bad Exif. But as other tools are able to read, so should we.. + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-bad-directory-entry-count.jpg")); + stream.seek(4424 + 10); + + Directory directory = createReader().read(new SubImageInputStream(stream, 214 - 6)); + assertEquals(7, directory.size()); // TIFF structure says 8, but the last entry isn't there + + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(3, exif.size()); + } + + @Test + public void testTIFFWithBadExifIFD() throws IOException { + // This image seems to contain bad TIFF data. But as other tools are able to read, so should we.. + // It seems that the EXIF data (at offset 494196 or 0x78a74) overlaps with a custom + // Microsoft 'OLE Property Set' entry at 0x78a70 (UNDEFINED, count 5632)... + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/tiff/chifley_logo.tif")); + Directory directory = createReader().read(stream); + assertEquals(22, directory.size()); + + // Some (all?) of the EXIF data is duplicated in the XMP, meaning PhotoShop can probably re-create it + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(0, exif.size()); // EXIFTool reports "Warning: Bad ExifIFD directory" + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/IFDTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/IFDTest.java new file mode 100644 index 00000000..6d49f46a --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/IFDTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.DirectoryAbstractTest; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** + * IFDTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IFDTest.java,v 1.0 02.01.12 16:42 haraldk Exp$ + */ +public class IFDTest extends DirectoryAbstractTest { + @Override + protected Directory createDirectory(final Collection entries) { + return new IFD(entries); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/UnknownTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/UnknownTest.java new file mode 100644 index 00000000..06239ba9 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/UnknownTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +/** + * UnknownTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: UnknownTest.java,v 1.0 03.01.12 17:21 haraldk Exp$ + */ +public class UnknownTest extends ObjectAbstractTestCase { + @Override + protected Object makeObject() { + return new Unknown((short) 42, 77, (long) (Math.random() * (long) Integer.MAX_VALUE)); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectoryTest.java new file mode 100644 index 00000000..4b7e82cc --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectoryTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.iptc; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.DirectoryAbstractTest; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** + * IPTCDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IPTCDirectoryTest.java,v 1.0 03.01.12 09:40 haraldk Exp$ + */ +public class IPTCDirectoryTest extends DirectoryAbstractTest { + @Override + protected Directory createDirectory(final Collection entries) { + return new IPTCDirectory(entries); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntryTest.java new file mode 100644 index 00000000..860f422d --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntryTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.iptc; + +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.EntryAbstractTest; + +/** + * IPTCEntryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IPTCEntryTest.java,v 1.0 03.01.12 09:39 haraldk Exp$ + */ +public class IPTCEntryTest extends EntryAbstractTest { + @Override + protected Entry createEntry(Object value) { + return new IPTCEntry(IPTC.TAG_BY_LINE, value); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReaderTest.java new file mode 100644 index 00000000..8a68fab3 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReaderTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.iptc; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest; +import org.junit.Test; + +import javax.imageio.ImageIO; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; + +/** + * IPTCReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IPTCReaderTest.java,v 1.0 04.01.12 09:43 haraldk Exp$ + */ +public class IPTCReaderTest extends MetadataReaderAbstractTest { + @Override + protected InputStream getData() throws IOException { + return getResource("/iptc/iptc-jpeg-segment.bin").openStream(); + } + + @Override + protected IPTCReader createReader() { + return new IPTCReader(); + } + + @Test + public void testDirectoryContent() throws IOException { + Directory directory = createReader().read(ImageIO.createImageInputStream(getData())); + + assertEquals(4, directory.size()); + + assertThat(directory.getEntryById(IPTC.TAG_RECORD_VERSION), hasValue(2)); // Mandatory + assertThat(directory.getEntryById(IPTC.TAG_CAPTION), hasValue("Picture 71146")); + assertThat(directory.getEntryById(IPTC.TAG_DATE_CREATED), hasValue("20080701")); + + // Weirdness: An undefined tag 2:56/0x0238 ?? + // Looks like it should be 2:60/TAG_TIME_CREATED, but doesn't match the time in the corresponding XMP/EXIF tags + assertThat(directory.getEntryById(IPTC.APPLICATION_RECORD | 56), hasValue("155029+0100")); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQualityTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQualityTest.java new file mode 100644 index 00000000..4648e2bc --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGQualityTest.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import org.junit.Ignore; +import org.junit.Test; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.plugins.jpeg.JPEGQTable; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; + +import static org.junit.Assert.*; + +/** + * JPEGQualityTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGQualityTest.java,v 1.0, 10.04.12, 12:39 haraldk Exp$ + */ +public class JPEGQualityTest { + + private static final float DELTA = .000001f; + + @Test + public void testGetQuality() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/jpeg/9788245605525.jpg")); + + try { + assertEquals(.92f, JPEGQuality.getJPEGQuality(stream), DELTA); + } + finally { + stream.close(); + } + } + + @Test + public void testGetQualityAltSample1() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); + + try { + assertEquals(.79f, JPEGQuality.getJPEGQuality(stream), DELTA); + } + finally { + stream.close(); + } + } + + @Test + public void testGetQualityAltSample2() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/jpeg/ts_open_300dpi.jpg")); + + try { + assertEquals(.99f, JPEGQuality.getJPEGQuality(stream), DELTA); + } + finally { + stream.close(); + } + } + + @Ignore("Need a JPEG test image with bad DQT data...") + @Test + public void testGetQualityBadData() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/bad-data")); + + try { + assertEquals(-1f, JPEGQuality.getJPEGQuality(stream), DELTA); + } + finally { + stream.close(); + } + } + + @Test + public void testWriteWithQualitySettingMatchesGetQuality() throws IOException { + ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/jpeg").next(); // If this fails, we have a more serious problem + + for (int i = 0; i < 10; i++) { + // TODO: Figure out why we get -1 for input quality 0.1 and 0.3... + if (i == 0 || i == 2) { + continue; + } + + // Set quality + float quality = (i + 1f) / 10f; + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(quality); + + // Write image + ByteArrayOutputStream temp = new ByteArrayOutputStream(); + ImageOutputStream output = ImageIO.createImageOutputStream(temp); + + try { + writer.setOutput(output); + writer.write(null, new IIOImage(createTestImage(), null, null), param); + } + finally { + output.close(); + } + + // Test quality + ImageInputStream input = new ByteArrayImageInputStream(temp.toByteArray()); + + try { + assertEquals(quality, JPEGQuality.getJPEGQuality(input), 0f); + } + finally { + input.close(); + } + } + } + + @Test + public void testGetQTables() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/jpeg/9788245605525.jpg")); + + try { + JPEGQTable[] tables = JPEGQuality.getQTables(JPEGSegmentUtil.readSegments(stream, JPEG.DQT, null)); + assertEquals(1, tables.length); + int[] table = { + 3, 2, 2, 2, 2, 2, 3, 2, + 2, 2, 3, 3, 3, 3, 4, 6, + 4, 4, 4, 4, 4, 8, 6, 6, + 5, 6, 9, 8, 10, 10, 9, 8, + 9, 9, 10, 12, 15, 12, 10, 11, + 14, 11, 9, 9, 13, 17, 13, 14, + 15, 16, 16, 17, 16, 10, 12, 18, + 19, 18, 16, 19, 15, 16, 16, 16 + }; + assertArrayEquals(table, tables[0].getTable()); + + // JPEGQTable has no useful equals method.. + // assertArrayEquals(new JPEGQTable[] {new JPEGQTable(table)}, tables); + } + finally { + stream.close(); + } + } + + @Test + public void testGetQTablesAlt1() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); + + try { + JPEGQTable[] tables = JPEGQuality.getQTables(JPEGSegmentUtil.readSegments(stream, JPEG.DQT, null)); + assertEquals(2, tables.length); + + assertArrayEquals( + new int[] { + 6, 6, 6, 6, 6, 6, 7, 7, + 7, 7, 9, 8, 8, 8, 9, 12, + 10, 11, 11, 10, 12, 15, 13, 13, + 14, 13, 13, 15, 19, 16, 15, 17, + 17, 15, 16, 19, 20, 19, 20, 22, + 20, 19, 20, 23, 24, 26, 26, 24, + 23, 29, 32, 34, 32, 29, 38, 42, + 42, 38, 50, 53, 50, 66, 66, 86, + }, + tables[0].getTable() + ); + assertArrayEquals( + new int[] { + 6, 6, 6, 6, 6, 6, 7, 6, + 6, 7, 8, 7, 8, 7, 8, 10, + 9, 9, 9, 9, 10, 13, 11, 11, + 12, 11, 11, 13, 16, 14, 13, 15, + 15, 13, 14, 16, 17, 16, 17, 18, + 17, 16, 17, 20, 20, 22, 22, 20, + 20, 24, 26, 27, 26, 24, 30, 33, + 33, 30, 39, 41, 39, 50, 50, 63, + }, + tables[1].getTable() + ); + } + finally { + stream.close(); + } + } + + @Test + public void testGetQTablesAlt2() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getClass().getResourceAsStream("/jpeg/ts_open_300dpi.jpg")); + + try { + JPEGQTable[] tables = JPEGQuality.getQTables(JPEGSegmentUtil.readSegments(stream, JPEG.DQT, null)); + assertEquals(2, tables.length); + + assertArrayEquals( + new int[] { + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + }, + tables[0].getTable() + ); + assertArrayEquals( + new int[] { + 1, 1, 1, 1, 1, 1, 2, 1, + 1, 2, 3, 2, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + }, + tables[1].getTable() + ); + } + finally { + stream.close(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testGetQTablesNull() throws IOException { + JPEGQuality.getQTables(null); + } + + @Test + public void testGetQTablesEmpty() throws IOException { + JPEGQTable[] tables = JPEGQuality.getQTables(Collections.emptyList()); + assertEquals(0, tables.length); + } + + private BufferedImage createTestImage() { + BufferedImage image = new BufferedImage(90, 60, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = image.createGraphics(); + + try { + g.setColor(Color.WHITE); + g.fillOval(15, 0, 60, 60); + g.setColor(Color.RED); + g.fill(new Polygon(new int[] {0, 90, 0, 0}, new int[] {0, 0, 60, 0}, 4)); + } + finally { + g.dispose(); + } + + return image; + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java new file mode 100644 index 00000000..42a9abe5 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import org.junit.Test; + +import java.nio.charset.Charset; + +import static org.junit.Assert.assertEquals; + +/** + * JPEGSegmentTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGSegmentTest.java,v 1.0 02.03.11 10.46 haraldk Exp$ + */ +public class JPEGSegmentTest extends ObjectAbstractTestCase { + @Test + public void testCreate() { + byte[] bytes = new byte[14]; + System.arraycopy("JFIF".getBytes(Charset.forName("ascii")), 0, bytes, 0, 4); + + JPEGSegment segment = new JPEGSegment(0xFFE0, bytes, 16); + + assertEquals(0xFFE0, segment.marker()); + assertEquals("JFIF", segment.identifier()); + assertEquals(16, segment.segmentLength()); + assertEquals(bytes.length - 5, segment.length()); + } + + @Test + public void testToStringAppSegment() { + byte[] bytes = new byte[14]; + System.arraycopy("JFIF".getBytes(Charset.forName("ascii")), 0, bytes, 0, 4); + JPEGSegment segment = new JPEGSegment(0xFFE0, bytes, 16); + + assertEquals("JPEGSegment[ffe0/JFIF size: 16]", segment.toString()); + } + + @Test + public void testToStringNonAppSegment() { + byte[] bytes = new byte[40]; + JPEGSegment segment = new JPEGSegment(0xFFC4, bytes, 42); + + assertEquals("JPEGSegment[ffc4 size: 42]", segment.toString()); + } + + @Override + protected Object makeObject() { + byte[] bytes = new byte[11]; + System.arraycopy("Exif".getBytes(Charset.forName("ascii")), 0, bytes, 0, 4); + return new JPEGSegment(0xFFE1, bytes, 16); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java new file mode 100644 index 00000000..2a69d8b1 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtilTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.jpeg; + +import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; +import org.junit.Test; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.awt.color.ICC_Profile; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * JPEGSegmentUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGSegmentUtilTest.java,v 1.0 01.03.11 16.22 haraldk Exp$ + */ +public class JPEGSegmentUtilTest { + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + } + + protected ImageInputStream getData(final String name) throws IOException { + return ImageIO.createImageInputStream(getClass().getResource(name)); + } + + @Test + public void testReadAPP0JFIF() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEG.APP0, "JFIF"); + + assertEquals(1, segments.size()); + JPEGSegment segment = segments.get(0); + assertEquals(JPEG.APP0, segment.marker()); + assertEquals("JFIF", segment.identifier()); + assertEquals(16, segment.segmentLength()); + } + + @Test + public void testReadAPP1Exif() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEG.APP1, "Exif"); + + assertEquals(1, segments.size()); + JPEGSegment segment = segments.get(0); + assertEquals(JPEG.APP1, segment.marker()); + assertEquals("Exif", segment.identifier()); + } + + @Test + public void testReadAPP1XMP() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEG.APP1, "http://ns.adobe.com/xap/1.0/"); + + assertEquals(1, segments.size()); + JPEGSegment segment = segments.get(0); + assertEquals(JPEG.APP1, segment.marker()); + assertEquals("http://ns.adobe.com/xap/1.0/", segment.identifier()); + } + + @Test + public void testReadAPP13Photoshop() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEG.APP13, "Photoshop 3.0"); + + assertEquals(1, segments.size()); + JPEGSegment segment = segments.get(0); + assertEquals(0xFFED, segment.marker()); + assertEquals("Photoshop 3.0", segment.identifier()); + } + + @Test + public void testReadAPP14Adobe() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEG.APP14, "Adobe"); + + assertEquals(1, segments.size()); + JPEGSegment segment = segments.get(0); + assertEquals(JPEG.APP14, segment.marker()); + assertEquals("Adobe", segment.identifier()); + assertEquals(14, segment.segmentLength()); + } + + @Test + public void testReadAPP2ICC_PROFILE() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEG.APP2, "ICC_PROFILE"); + + assertEquals(18, segments.size()); + + for (JPEGSegment segment : segments) { + assertEquals(JPEG.APP2, segment.marker()); + assertEquals("ICC_PROFILE", segment.identifier()); + } + + // Test that we can actually read the chunked ICC profile + DataInputStream stream = new DataInputStream(segments.get(0).data()); + int chunkNumber = stream.readUnsignedByte(); + int chunkCount = stream.readUnsignedByte(); + + InputStream[] streams = new InputStream[chunkCount]; + streams[chunkNumber - 1] = stream; + + for (int i = 1; i < chunkCount; i++) { + stream = new DataInputStream(segments.get(i).data()); + + chunkNumber = stream.readUnsignedByte(); + if (stream.readUnsignedByte() != chunkCount) { + throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks.")); + } + + streams[chunkNumber - 1] = stream; + } + + ICC_Profile profile = ICC_Profile.getInstance(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams)))); + assertNotNull("Profile could not be read, probably bad data", profile); + } + + @Test + public void testReadAll() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEGSegmentUtil.ALL_SEGMENTS); + assertEquals(6, segments.size()); + + assertEquals(segments.toString(), JPEG.SOF0, segments.get(3).marker()); + assertEquals(segments.toString(), null, segments.get(3).identifier()); + } + + @Test + public void testReadAllAlt() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEGSegmentUtil.ALL_SEGMENTS); + assertEquals(26, segments.size()); + + assertEquals(segments.toString(), JPEG.SOF0, segments.get(23).marker()); + assertEquals(segments.toString(), null, segments.get(23).identifier()); + } + + @Test + public void testReadAppMarkers() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/9788245605525.jpg"), JPEGSegmentUtil.APP_SEGMENTS); + assertEquals(2, segments.size()); + + assertEquals(JPEG.APP0, segments.get(0).marker()); + assertEquals("JFIF", segments.get(0).identifier()); + assertEquals(JPEG.APP14, segments.get(1).marker()); + assertEquals("Adobe", segments.get(1).identifier()); + } + + @Test + public void testReadAppMarkersAlt() throws IOException { + List segments = JPEGSegmentUtil.readSegments(getData("/jpeg/ts_open_300dpi.jpg"), JPEGSegmentUtil.APP_SEGMENTS); + assertEquals(22, segments.size()); + + assertEquals(JPEG.APP1, segments.get(0).marker()); + assertEquals("Exif", segments.get(0).identifier()); + assertEquals(0xFFED, segments.get(1).marker()); + assertEquals("Photoshop 3.0", segments.get(1).identifier()); + assertEquals(JPEG.APP1, segments.get(2).marker()); + assertEquals("http://ns.adobe.com/xap/1.0/", segments.get(2).identifier()); + assertEquals(JPEG.APP2, segments.get(3).marker()); + assertEquals("ICC_PROFILE", segments.get(3).identifier()); + // ... + assertEquals(JPEG.APP14, segments.get(21).marker()); + assertEquals("Adobe", segments.get(21).identifier()); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDDirectoryTest.java new file mode 100644 index 00000000..5cf4e846 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDDirectoryTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.DirectoryAbstractTest; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** + * PhotoshopDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PhotoshopDirectoryTest.java,v 1.0 04.01.12 12:01 haraldk Exp$ + */ +public class PSDDirectoryTest extends DirectoryAbstractTest { + @Override + protected Directory createDirectory(final Collection entries) { + return new PSDDirectory(entries); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntryTest.java new file mode 100644 index 00000000..50f9558f --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDEntryTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.EntryAbstractTest; + +/** + * PhotoshopEntryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PhotoshopEntryTest.java,v 1.0 04.01.12 12:00 haraldk Exp$ + */ +public class PSDEntryTest extends EntryAbstractTest { + @Override + protected Entry createEntry(final Object value) { + return new PSDEntry(0x404, "", value); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDReaderTest.java new file mode 100644 index 00000000..90ca64fb --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/psd/PSDReaderTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.psd; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; + +/** + * PhotoshopReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PhotoshopReaderTest.java,v 1.0 04.01.12 12:01 haraldk Exp$ + */ +public class PSDReaderTest extends MetadataReaderAbstractTest { + @Override + protected InputStream getData() throws IOException { + return getResource("/psd/psd-jpeg-segment.bin").openStream(); + } + + @Override + protected PSDReader createReader() { + return new PSDReader(); + } + + @Test + public void testPhotoshopDirectoryContents() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertEquals(23, directory.size()); + + assertNotNull(directory.getEntryById(0x0404)); + assertNotNull(directory.getEntryById(0x0425)); + assertNotNull(directory.getEntryById(0x03ea)); + assertNotNull(directory.getEntryById(0x03e9)); + assertNotNull(directory.getEntryById(0x03ed)); + assertNotNull(directory.getEntryById(0x0426)); + + // TODO: More + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/RDFDescriptionTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/RDFDescriptionTest.java new file mode 100644 index 00000000..c4adc532 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/RDFDescriptionTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.xmp; + +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.DirectoryAbstractTest; +import com.twelvemonkeys.imageio.metadata.Entry; + +import java.util.Collection; + +/** + * XMPDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: XMPDirectoryTest.java,v 1.0 03.01.12 09:42 haraldk Exp$ + */ +public class RDFDescriptionTest extends DirectoryAbstractTest { + @Override + protected Directory createDirectory(final Collection entries) { + return new RDFDescription(entries); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectoryTest.java new file mode 100644 index 00000000..883c0550 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPDirectoryTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.xmp; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.CompoundDirectoryAbstractTest; +import com.twelvemonkeys.imageio.metadata.Directory; + +import java.util.Collection; + +/** + * XMPDirectoryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: XMPDirectoryTest.java,v 1.0 03.01.12 09:42 haraldk Exp$ + */ +public class XMPDirectoryTest extends CompoundDirectoryAbstractTest { + @Override + protected CompoundDirectory createCompoundDirectory(final Collection directories) { + return new XMPDirectory(directories, null); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntryTest.java new file mode 100644 index 00000000..176561c5 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntryTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.xmp; + +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.EntryAbstractTest; + +/** + * XMPEntryTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: XMPEntryTest.java,v 1.0 03.01.12 09:41 haraldk Exp$ + */ +public class XMPEntryTest extends EntryAbstractTest { + @Override + protected Entry createEntry(Object value) { + return new XMPEntry(XMP.NS_XAP + ":foo", value); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReaderTest.java new file mode 100644 index 00000000..b66b2bd9 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReaderTest.java @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.metadata.xmp; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.*; + +/** + * XMPReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: XMPReaderTest.java,v 1.0 04.01.12 10:47 haraldk Exp$ + */ +public class XMPReaderTest extends MetadataReaderAbstractTest { + + @Override + protected InputStream getData() throws IOException { + return getResource("/xmp/xmp-jpeg-example.xml").openStream(); + } + + private ImageInputStream getResourceAsIIS(final String name) throws IOException { + return ImageIO.createImageInputStream(getResource(name)); + } + + @Override + protected XMPReader createReader() { + return new XMPReader(); + } + + @Test + public void testDirectoryContent() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertEquals(29, directory.size()); + + // photoshop|http://ns.adobe.com/photoshop/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/DateCreated"), hasValue("2008-07-01")); + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/ColorMode"), hasValue("4")); + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/ICCProfile"), hasValue("U.S. Web Coated (SWOP) v2")); + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/History"), hasValue("")); + + // xapMM|http://ns.adobe.com/xap/1.0/mm/ + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/DocumentID"), hasValue("uuid:54A8D5F8654711DD9226A85E1241887A")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/InstanceID"), hasValue("uuid:54A8D5F9654711DD9226A85E1241887A")); + + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/DerivedFrom"), hasValue( + new RDFDescription(Arrays.asList( + // stRef|http://ns.adobe.com/xap/1.0/sType/ResourceRef# + new XMPEntry("http://ns.adobe.com/xap/1.0/sType/ResourceRef#instanceID", "instanceID", "uuid:3B52F3610F49DD118831FCA29C13B8DE"), + new XMPEntry("http://ns.adobe.com/xap/1.0/sType/ResourceRef#documentID", "documentID", "uuid:3A52F3610F49DD118831FCA29C13B8DE") + )) + )); + + // dc|http://purl.org/dc/elements/1.1/ + assertThat(directory.getEntryById("http://purl.org/dc/elements/1.1/description"), hasValue(Collections.singletonMap("x-default", "Picture 71146"))); + assertThat(directory.getEntryById("http://purl.org/dc/elements/1.1/format"), hasValue("image/jpeg")); + + // tiff|http://ns.adobe.com/tiff/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/ImageWidth"), hasValue("3601")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/ImageLength"), hasValue("4176")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/BitsPerSample"), hasValue(Arrays.asList("8", "8", "8"))); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/Compression"), hasValue("1")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/PhotometricInterpretation"), hasValue("2")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/SamplesPerPixel"), hasValue("3")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/PlanarConfiguration"), hasValue("1")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/XResolution"), hasValue("3000000/10000")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/YResolution"), hasValue("3000000/10000")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/ResolutionUnit"), hasValue("2")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/Orientation"), hasValue("1")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/NativeDigest"), hasValue("256,257,258,259,262,274,277,284,530,531,282,283,296,301,318,319,529,532,306,270,271,272,305,315,33432;C21EE6D33E4CCA3712ECB1F5E9031A49")); + + // xap|http://ns.adobe.com/xap/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/ModifyDate"), hasValue("2008-08-06T12:43:05+10:00")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/CreatorTool"), hasValue("Adobe Photoshop CS2 Macintosh")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/CreateDate"), hasValue("2008-08-06T12:43:05+10:00")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/MetadataDate"), hasValue("2008-08-06T12:43:05+10:00")); + + // exif|http://ns.adobe.com/exif/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/ColorSpace"), hasValue("-1")); // SIC + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/PixelXDimension"), hasValue("3601")); + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/PixelYDimension"), hasValue("4176")); + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/NativeDigest"), hasValue("36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;297AD344CC15F29D5283460ED026368F")); + } + + @Test + public void testCompoundDirectory() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + assertEquals(6, compound.directoryCount()); + + int size = 0; + for (int i = 0; i < compound.directoryCount(); i++) { + Directory sub = compound.getDirectory(i); + assertNotNull(sub); + size += sub.size(); + } + + assertEquals(directory.size(), size); + } + + @Test + public void testCompoundDirectoryContentPhotoshop() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // photoshop|http://ns.adobe.com/photoshop/1.0/ + Directory photoshop = compound.getDirectory(0); + assertEquals(4, photoshop.size()); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/DateCreated"), hasValue("2008-07-01")); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/ColorMode"), hasValue("4")); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/ICCProfile"), hasValue("U.S. Web Coated (SWOP) v2")); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/History"), hasValue("")); + + } + + @Test + public void testCompoundDirectoryContentMM() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // xapMM|http://ns.adobe.com/xap/1.0/mm/ + Directory mm = compound.getDirectory(1); + assertEquals(3, mm.size()); + assertThat(mm.getEntryById("http://ns.adobe.com/xap/1.0/mm/DocumentID"), hasValue("uuid:54A8D5F8654711DD9226A85E1241887A")); + assertThat(mm.getEntryById("http://ns.adobe.com/xap/1.0/mm/InstanceID"), hasValue("uuid:54A8D5F9654711DD9226A85E1241887A")); + assertThat(mm.getEntryById("http://ns.adobe.com/xap/1.0/mm/DerivedFrom"), hasValue( + new RDFDescription(Arrays.asList( + // stRef|http://ns.adobe.com/xap/1.0/sType/ResourceRef# + new XMPEntry("http://ns.adobe.com/xap/1.0/sType/ResourceRef#instanceID", "instanceID", "uuid:3B52F3610F49DD118831FCA29C13B8DE"), + new XMPEntry("http://ns.adobe.com/xap/1.0/sType/ResourceRef#documentID", "documentID", "uuid:3A52F3610F49DD118831FCA29C13B8DE") + )) + )); + + } + + @Test + public void testCompoundDirectoryContentDC() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // dc|http://purl.org/dc/elements/1.1/ + Directory dc = compound.getDirectory(2); + assertEquals(2, dc.size()); + assertThat(dc.getEntryById("http://purl.org/dc/elements/1.1/description"), hasValue(Collections.singletonMap("x-default", "Picture 71146"))); + assertThat(dc.getEntryById("http://purl.org/dc/elements/1.1/format"), hasValue("image/jpeg")); + + } + + @Test + public void testCompoundDirectoryContentTIFF() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // tiff|http://ns.adobe.com/tiff/1.0/ + Directory tiff = compound.getDirectory(3); + assertEquals(12, tiff.size()); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/ImageWidth"), hasValue("3601")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/ImageLength"), hasValue("4176")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/BitsPerSample"), hasValue(Arrays.asList("8", "8", "8"))); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/Compression"), hasValue("1")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/PhotometricInterpretation"), hasValue("2")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/SamplesPerPixel"), hasValue("3")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/PlanarConfiguration"), hasValue("1")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/XResolution"), hasValue("3000000/10000")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/YResolution"), hasValue("3000000/10000")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/ResolutionUnit"), hasValue("2")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/Orientation"), hasValue("1")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/NativeDigest"), hasValue("256,257,258,259,262,274,277,284,530,531,282,283,296,301,318,319,529,532,306,270,271,272,305,315,33432;C21EE6D33E4CCA3712ECB1F5E9031A49")); + } + + @Test + public void testCompoundDirectoryContentXAP() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // xap|http://ns.adobe.com/xap/1.0/ + Directory xap = compound.getDirectory(4); + assertEquals(4, xap.size()); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/ModifyDate"), hasValue("2008-08-06T12:43:05+10:00")); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/CreatorTool"), hasValue("Adobe Photoshop CS2 Macintosh")); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/CreateDate"), hasValue("2008-08-06T12:43:05+10:00")); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/MetadataDate"), hasValue("2008-08-06T12:43:05+10:00")); + } + + @Test + public void testCompoundDirectoryContentEXIF() throws IOException { + Directory directory = createReader().read(getDataAsIIS()); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // exif|http://ns.adobe.com/exif/1.0/ + Directory exif = compound.getDirectory(5); + assertEquals(4, exif.size()); + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/ColorSpace"), hasValue("-1")); // SIC. Same as unsigned short 65535, meaning "uncalibrated"? + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/PixelXDimension"), hasValue("3601")); + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/PixelYDimension"), hasValue("4176")); + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/NativeDigest"), hasValue("36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;297AD344CC15F29D5283460ED026368F")); + } + + @Test + public void testRDFBag() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-bag-example.xml")); + + assertEquals(1, directory.size()); + assertThat(directory.getEntryById("http://purl.org/dc/elements/1.1/subject"), hasValue(Arrays.asList("XMP", "metadata", "ISO standard"))); // Order does not matter + } + + @Test + public void testRDFSeq() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-seq-example.xml")); + + assertEquals(1, directory.size()); + assertThat(directory.getEntryById("http://purl.org/dc/elements/1.1/subject"), hasValue(Arrays.asList("XMP", "metadata", "ISO standard"))); + } + + @Test + public void testRDFAlt() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-alt-example.xml")); + + assertEquals(1, directory.size()); + assertThat(directory.getEntryById("http://purl.org/dc/elements/1.1/subject"), hasValue(new HashMap() {{ + put("x-default", "One"); + put("en-us", "One"); + put("de", "Ein"); + put("no-nb", "En"); + }})); + } + + @Test + public void testRDFAttributeSyntax() throws IOException { + // Alternate RDF syntax, using attribute values instead of nested tags + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertEquals(20, directory.size()); + + // dc|http://purl.org/dc/elements/1.1/ + assertThat(directory.getEntryById("http://purl.org/dc/elements/1.1/format"), hasValue("image/jpeg")); + + // xap|http://ns.adobe.com/xap/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/ModifyDate"), hasValue("2008-07-16T14:44:49-07:00")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/CreatorTool"), hasValue("Adobe Photoshop CS3 Windows")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/CreateDate"), hasValue("2008-07-16T14:44:49-07:00")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/MetadataDate"), hasValue("2008-07-16T14:44:49-07:00")); + + // tiff|http://ns.adobe.com/tiff/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/Orientation"), hasValue("1")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/XResolution"), hasValue("720000/10000")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/YResolution"), hasValue("720000/10000")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/ResolutionUnit"), hasValue("2")); + assertThat(directory.getEntryById("http://ns.adobe.com/tiff/1.0/NativeDigest"), hasValue("256,257,258,259,262,274,277,284,530,531,282,283,296,301,318,319,529,532,306,270,271,272,305,315,33432;C08D8E93274C4BEE83E86CF999955A87")); + + // exif|http://ns.adobe.com/exif/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/ColorSpace"), hasValue("-1")); // SIC. Same as unsigned short 65535, meaning "uncalibrated"? + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/PixelXDimension"), hasValue("426")); + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/PixelYDimension"), hasValue("550")); + assertThat(directory.getEntryById("http://ns.adobe.com/exif/1.0/NativeDigest"), hasValue("36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;A7F21D25E2C562F152B2C4ECC9E534DA")); + + // photoshop|http://ns.adobe.com/photoshop/1.0/ + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/ColorMode"), hasValue("1")); + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/ICCProfile"), hasValue("Dot Gain 20%")); + assertThat(directory.getEntryById("http://ns.adobe.com/photoshop/1.0/History"), hasValue("")); + + // xapMM|http://ns.adobe.com/xap/1.0/mm/ + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/DocumentID"), hasValue("uuid:6DCA50CC7D53DD119F20F5A7EA4C9BEC")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/InstanceID"), hasValue("uuid:6ECA50CC7D53DD119F20F5A7EA4C9BEC")); + + // Custom test, as NamedNodeMap does not preserve order (tests can't rely on XML impl specifics) + Entry derivedFrom = directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/DerivedFrom"); + assertNotNull(derivedFrom); + assertThat(derivedFrom.getValue(), instanceOf(RDFDescription.class)); + + // stRef|http://ns.adobe.com/xap/1.0/sType/ResourceRef# + RDFDescription stRef = (RDFDescription) derivedFrom.getValue(); + assertThat(stRef.getEntryById("http://ns.adobe.com/xap/1.0/sType/ResourceRef#instanceID"), hasValue("uuid:74E1C905B405DD119306A1902BA5AA28")); + assertThat(stRef.getEntryById("http://ns.adobe.com/xap/1.0/sType/ResourceRef#documentID"), hasValue("uuid:7A6C79768005DD119306A1902BA5AA28")); + } + + @Test + public void testRDFAttributeSyntaxCompound() throws IOException { + // Alternate RDF syntax, using attribute values instead of nested tags + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + assertEquals(6, compound.directoryCount()); + + int size = 0; + for (int i = 0; i < compound.directoryCount(); i++) { + Directory sub = compound.getDirectory(i); + assertNotNull(sub); + size += sub.size(); + } + + assertEquals(directory.size(), size); + } + + private Directory getDirectoryByNS(final CompoundDirectory compound, final String namespace) { + for (int i = 0; i < compound.directoryCount(); i++) { + Directory candidate = compound.getDirectory(i); + + Iterator entries = candidate.iterator(); + if (entries.hasNext()) { + Entry entry = entries.next(); + if (entry.getIdentifier() instanceof String && ((String) entry.getIdentifier()).startsWith(namespace)) { + return candidate; + } + } + } + + return null; + } + + @Test + public void testRDFAttributeSyntaxCompoundDirectoryContentPhotoshop() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // photoshop|http://ns.adobe.com/photoshop/1.0/ + Directory photoshop = getDirectoryByNS(compound, XMP.NS_PHOTOSHOP); + + assertEquals(3, photoshop.size()); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/ColorMode"), hasValue("1")); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/ICCProfile"), hasValue("Dot Gain 20%")); + assertThat(photoshop.getEntryById("http://ns.adobe.com/photoshop/1.0/History"), hasValue("")); + } + + @Test + public void testRDFAttributeSyntaxCompoundDirectoryContentMM() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // xapMM|http://ns.adobe.com/xap/1.0/mm/ + Directory mm = getDirectoryByNS(compound, XMP.NS_XAP_MM); + assertEquals(3, mm.size()); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/DocumentID"), hasValue("uuid:6DCA50CC7D53DD119F20F5A7EA4C9BEC")); + assertThat(directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/InstanceID"), hasValue("uuid:6ECA50CC7D53DD119F20F5A7EA4C9BEC")); + + // Custom test, as NamedNodeMap does not preserve order (tests can't rely on XML impl specifics) + Entry derivedFrom = directory.getEntryById("http://ns.adobe.com/xap/1.0/mm/DerivedFrom"); + assertNotNull(derivedFrom); + assertThat(derivedFrom.getValue(), instanceOf(RDFDescription.class)); + + // stRef|http://ns.adobe.com/xap/1.0/sType/ResourceRef# + RDFDescription stRef = (RDFDescription) derivedFrom.getValue(); + assertThat(stRef.getEntryById("http://ns.adobe.com/xap/1.0/sType/ResourceRef#instanceID"), hasValue("uuid:74E1C905B405DD119306A1902BA5AA28")); + assertThat(stRef.getEntryById("http://ns.adobe.com/xap/1.0/sType/ResourceRef#documentID"), hasValue("uuid:7A6C79768005DD119306A1902BA5AA28")); + } + + @Test + public void testRDFAttributeSyntaxCompoundDirectoryContentDC() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // dc|http://purl.org/dc/elements/1.1/ + Directory dc = getDirectoryByNS(compound, XMP.NS_DC); + assertEquals(1, dc.size()); + + assertThat(dc.getEntryById("http://purl.org/dc/elements/1.1/format"), hasValue("image/jpeg")); + } + + @Test + public void testRDFAttributeSyntaxCompoundDirectoryContentTIFF() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // tiff|http://ns.adobe.com/tiff/1.0/ + Directory tiff = getDirectoryByNS(compound, XMP.NS_TIFF); + assertEquals(5, tiff.size()); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/Orientation"), hasValue("1")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/XResolution"), hasValue("720000/10000")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/YResolution"), hasValue("720000/10000")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/ResolutionUnit"), hasValue("2")); + assertThat(tiff.getEntryById("http://ns.adobe.com/tiff/1.0/NativeDigest"), hasValue("256,257,258,259,262,274,277,284,530,531,282,283,296,301,318,319,529,532,306,270,271,272,305,315,33432;C08D8E93274C4BEE83E86CF999955A87")); + } + + @Test + public void testRDFAttributeSyntaxCompoundDirectoryContentXAP() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // xap|http://ns.adobe.com/xap/1.0/ + Directory xap = getDirectoryByNS(compound, XMP.NS_XAP); + assertEquals(4, xap.size()); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/ModifyDate"), hasValue("2008-07-16T14:44:49-07:00")); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/CreatorTool"), hasValue("Adobe Photoshop CS3 Windows")); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/CreateDate"), hasValue("2008-07-16T14:44:49-07:00")); + assertThat(xap.getEntryById("http://ns.adobe.com/xap/1.0/MetadataDate"), hasValue("2008-07-16T14:44:49-07:00")); + } + + @Test + public void testRDFAttributeSyntaxCompoundDirectoryContentEXIF() throws IOException { + Directory directory = createReader().read(getResourceAsIIS("/xmp/rdf-attribute-shorthand.xml")); + + assertThat(directory, instanceOf(CompoundDirectory.class)); + CompoundDirectory compound = (CompoundDirectory) directory; + + // exif|http://ns.adobe.com/exif/1.0/ + Directory exif = getDirectoryByNS(compound, XMP.NS_EXIF); + assertEquals(4, exif.size()); + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/ColorSpace"), hasValue("-1")); // SIC. Same as unsigned short 65535, meaning "uncalibrated"? + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/PixelXDimension"), hasValue("426")); + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/PixelYDimension"), hasValue("550")); + assertThat(exif.getEntryById("http://ns.adobe.com/exif/1.0/NativeDigest"), hasValue("36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;A7F21D25E2C562F152B2C4ECC9E534DA")); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java index 9d4e9a96..450d4d8b 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.imageio.metadata.xmp; -import junit.framework.TestCase; +import org.junit.Test; import java.io.*; import java.nio.charset.UnsupportedCharsetException; @@ -8,6 +8,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Random; +import static org.junit.Assert.*; + /** * XMPScannerTestCase * @@ -15,8 +17,7 @@ import java.util.Random; * @author last modified by $Author: haraldk$ * @version $Id: XMPScannerTestCase.java,v 1.0 Nov 13, 2009 3:59:43 PM haraldk Exp$ */ -public class XMPScannerTestCase extends TestCase { - +public class XMPScannerTestCase { static final String XMP = "" + "\n"+ @@ -33,11 +34,11 @@ public class XMPScannerTestCase extends TestCase { "" + ""; - final Random mRandom = new Random(4934638567l); + final Random random = new Random(4934638567l); private InputStream createRandomStream(final int pLength) { byte[] bytes = new byte[pLength]; - mRandom.nextBytes(bytes); + random.nextBytes(bytes); return new ByteArrayInputStream(bytes); } @@ -60,6 +61,7 @@ public class XMPScannerTestCase extends TestCase { } } + @Test public void testScanForUTF8() throws IOException { InputStream stream = createXMPStream(XMP, "UTF-8"); @@ -68,14 +70,16 @@ public class XMPScannerTestCase extends TestCase { assertNotNull(reader); } - public void testScanForUTF8singleQuote() throws IOException { - InputStream stream = createXMPStream(XMP, "UTF-8".replace("\"", "'")); + @Test + public void testScanForUTF8SingleQuote() throws IOException { + InputStream stream = createXMPStream(XMP.replace("\"", "'"), "UTF-8"); Reader reader = XMPScanner.scanForXMPPacket(stream); assertNotNull(reader); } + @Test public void testScanForUTF16BE() throws IOException { InputStream stream = createXMPStream(XMP, "UTF-16BE"); @@ -84,14 +88,16 @@ public class XMPScannerTestCase extends TestCase { assertNotNull(reader); } - public void testScanForUTF16BEsingleQuote() throws IOException { - InputStream stream = createXMPStream(XMP, "UTF-16BE".replace("\"", "'")); + @Test + public void testScanForUTF16BESingleQuote() throws IOException { + InputStream stream = createXMPStream(XMP.replace("\"", "'"), "UTF-16BE"); Reader reader = XMPScanner.scanForXMPPacket(stream); assertNotNull(reader); } + @Test public void testScanForUTF16LE() throws IOException { InputStream stream = createXMPStream(XMP, "UTF-16LE"); @@ -100,28 +106,40 @@ public class XMPScannerTestCase extends TestCase { assertNotNull(reader); } - public void testScanForUTF16LEsingleQuote() throws IOException { - InputStream stream = createXMPStream(XMP, "UTF-16LE".replace("\"", "'")); + @Test + public void testScanForUTF16LESingleQuote() throws IOException { + InputStream stream = createXMPStream(XMP.replace("\"", "'"), "UTF-16LE"); Reader reader = XMPScanner.scanForXMPPacket(stream); assertNotNull(reader); } - // TODO: Default Java installation on OS X don't seem to have UTF-32 installed. Hmmm.. -// public void testUTF32BE() throws IOException { -// InputStream stream = createXMPStream("UTF-32BE"); -// -// Reader reader = XMPScanner.scanForXMPPacket(stream); -// -// assertNotNull(reader); -// } -// -// public void testUTF32LE() throws IOException { -// InputStream stream = createXMPStream("UTF-32LE"); -// -// Reader reader = XMPScanner.scanForXMPPacket(stream); -// -// assertNotNull(reader); -// } + @Test + public void testUTF32BE() throws IOException { + try { + InputStream stream = createXMPStream(XMP, "UTF-32BE"); + + Reader reader = XMPScanner.scanForXMPPacket(stream); + + assertNotNull(reader); + } + catch (UnsupportedCharsetException ignore) { + System.err.println("Warning: Unsupported charset. Test skipped. " + ignore); + } + } + + @Test + public void testUTF32LE() throws IOException { + try { + InputStream stream = createXMPStream(XMP, "UTF-32LE"); + + Reader reader = XMPScanner.scanForXMPPacket(stream); + + assertNotNull(reader); + } + catch (UnsupportedCharsetException ignore) { + System.err.println("Warning: Unsupported charset. Test skipped. " + ignore); + } + } } diff --git a/imageio/imageio-metadata/src/test/resources/exif/exif-jpeg-segment.bin b/imageio/imageio-metadata/src/test/resources/exif/exif-jpeg-segment.bin new file mode 100644 index 00000000..1e8f482b Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/exif/exif-jpeg-segment.bin differ diff --git a/imageio/imageio-metadata/src/test/resources/iptc/iptc-jpeg-segment.bin b/imageio/imageio-metadata/src/test/resources/iptc/iptc-jpeg-segment.bin new file mode 100644 index 00000000..fc9d6173 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/iptc/iptc-jpeg-segment.bin differ diff --git a/imageio/imageio-metadata/src/test/resources/jpeg/9788245605525.jpg b/imageio/imageio-metadata/src/test/resources/jpeg/9788245605525.jpg new file mode 100644 index 00000000..295234be Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/jpeg/9788245605525.jpg differ diff --git a/imageio/imageio-metadata/src/test/resources/jpeg/exif-bad-directory-entry-count.jpg b/imageio/imageio-metadata/src/test/resources/jpeg/exif-bad-directory-entry-count.jpg new file mode 100644 index 00000000..9a747e9c Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/jpeg/exif-bad-directory-entry-count.jpg differ diff --git a/imageio/imageio-metadata/src/test/resources/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg b/imageio/imageio-metadata/src/test/resources/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg new file mode 100644 index 00000000..6ad31099 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg differ diff --git a/imageio/imageio-metadata/src/test/resources/jpeg/ts_open_300dpi.jpg b/imageio/imageio-metadata/src/test/resources/jpeg/ts_open_300dpi.jpg new file mode 100644 index 00000000..6e95a9d6 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/jpeg/ts_open_300dpi.jpg differ diff --git a/imageio/imageio-metadata/src/test/resources/psd/psd-jpeg-segment.bin b/imageio/imageio-metadata/src/test/resources/psd/psd-jpeg-segment.bin new file mode 100644 index 00000000..ebaeece3 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/psd/psd-jpeg-segment.bin differ diff --git a/imageio/imageio-metadata/src/test/resources/tiff/chifley_logo.tif b/imageio/imageio-metadata/src/test/resources/tiff/chifley_logo.tif new file mode 100644 index 00000000..90fb5eb3 Binary files /dev/null and b/imageio/imageio-metadata/src/test/resources/tiff/chifley_logo.tif differ diff --git a/imageio/imageio-metadata/src/test/resources/xmp/rdf-alt-example.xml b/imageio/imageio-metadata/src/test/resources/xmp/rdf-alt-example.xml new file mode 100644 index 00000000..54c681b2 --- /dev/null +++ b/imageio/imageio-metadata/src/test/resources/xmp/rdf-alt-example.xml @@ -0,0 +1,13 @@ + + + + + + One + One + Ein + En + + + + diff --git a/imageio/imageio-metadata/src/test/resources/xmp/rdf-attribute-shorthand.xml b/imageio/imageio-metadata/src/test/resources/xmp/rdf-attribute-shorthand.xml new file mode 100644 index 00000000..6c1306ca --- /dev/null +++ b/imageio/imageio-metadata/src/test/resources/xmp/rdf-attribute-shorthand.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/imageio/imageio-metadata/src/test/resources/xmp/rdf-bag-example.xml b/imageio/imageio-metadata/src/test/resources/xmp/rdf-bag-example.xml new file mode 100644 index 00000000..193fb660 --- /dev/null +++ b/imageio/imageio-metadata/src/test/resources/xmp/rdf-bag-example.xml @@ -0,0 +1,12 @@ + + + + + + XMP + metadata + ISO standard + + + + diff --git a/imageio/imageio-metadata/src/test/resources/xmp/rdf-seq-example.xml b/imageio/imageio-metadata/src/test/resources/xmp/rdf-seq-example.xml new file mode 100644 index 00000000..c1112e92 --- /dev/null +++ b/imageio/imageio-metadata/src/test/resources/xmp/rdf-seq-example.xml @@ -0,0 +1,12 @@ + + + + + + XMP + metadata + ISO standard + + + + diff --git a/imageio/imageio-metadata/src/test/resources/xmp/xmp-jpeg-example.xml b/imageio/imageio-metadata/src/test/resources/xmp/xmp-jpeg-example.xml new file mode 100644 index 00000000..b3f3e412 --- /dev/null +++ b/imageio/imageio-metadata/src/test/resources/xmp/xmp-jpeg-example.xml @@ -0,0 +1,187 @@ + + + + + 2008-07-01 + 4 + U.S. Web Coated (SWOP) v2 + + + + uuid:54A8D5F8654711DD9226A85E1241887A + uuid:54A8D5F9654711DD9226A85E1241887A + + uuid:3B52F3610F49DD118831FCA29C13B8DE + uuid:3A52F3610F49DD118831FCA29C13B8DE + + + + + + Picture 71146 + + + image/jpeg + + + 3601 + 4176 + + + 8 + 8 + 8 + + + 1 + 2 + 3 + 1 + 3000000/10000 + 3000000/10000 + 2 + 1 + 256,257,258,259,262,274,277,284,530,531,282,283,296,301,318,319,529,532,306,270,271,272,305,315,33432;C21EE6D33E4CCA3712ECB1F5E9031A49 + + + 2008-08-06T12:43:05+10:00 + Adobe Photoshop CS2 Macintosh + 2008-08-06T12:43:05+10:00 + 2008-08-06T12:43:05+10:00 + + + -1 + 3601 + 4176 + 36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;297AD344CC15F29D5283460ED026368F + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/imageio/imageio-pict/license.txt b/imageio/imageio-pict/license.txt index 033542bd..30d47b8d 100755 --- a/imageio/imageio-pict/license.txt +++ b/imageio/imageio-pict/license.txt @@ -27,9 +27,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Parts of this software is based on JVG/JIS. See http://www.cs.hut.fi/~framling/JVG/index.html for more information. -Redistribution under BSD authorized by Kary Främling: +Redistribution under BSD authorized by Kary Främling: -Copyright (c) 2003, Kary Främling +Copyright (c) 2003, Kary Främling All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java index bce32188..b1270186 100644 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java @@ -28,9 +28,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Parts of this software is based on JVG/JIS. See http://www.cs.hut.fi/~framling/JVG/index.html for more information. -Redistribution under BSD authorized by Kary Främling: +Redistribution under BSD authorized by Kary Främling: -Copyright (c) 2003, Kary Främling +Copyright (c) 2003, Kary Främling All rights reserved. Redistribution and use in source and binary forms, with or without @@ -85,7 +85,7 @@ import java.util.List; *

    * * @author Harald Kuhr - * @author Kary Främling (original PICT/QuickDraw parsing) + * @author Kary Främling (original PICT/QuickDraw parsing) * @author Matthias Wiesmann (original embedded QuickTime parsing) * @version $Id: PICTReader.java,v 1.0 05.apr.2006 15:20:48 haku Exp$ */ @@ -110,23 +110,23 @@ public class PICTImageReader extends ImageReaderBase { static boolean DEBUG = false; // Private fields - private QuickDrawContext mContext; - private Rectangle mFrame; + private QuickDrawContext context; + private Rectangle frame; - private int mVersion; + private int version; // Variables for storing draw status - private Point mPenPosition = new Point(0, 0); - private Rectangle mLastRectangle = new Rectangle(0, 0); + private Point penPosition = new Point(0, 0); + private Rectangle lastRectangle = new Rectangle(0, 0); // Ratio between the screen resolution and the image resolution - private double mScreenImageXRatio; - private double mScreenImageYRatio; + private double screenImageXRatio; + private double screenImageYRatio; // List of images created during image import - private List mImages = new ArrayList(); - private long mImageStartStreamPos; - protected int mPicSize; + private List images = new ArrayList(); + private long imageStartStreamPos; + protected int picSize; public PICTImageReader() { this(null); @@ -137,9 +137,9 @@ public class PICTImageReader extends ImageReaderBase { } protected void resetMembers() { - mContext = null; - mFrame = null; - mImages.clear(); + context = null; + frame = null; + images.clear(); } /** @@ -149,14 +149,16 @@ public class PICTImageReader extends ImageReaderBase { * @throws IOException if an I/O error occurs while reading the image. */ private Rectangle getPICTFrame() throws IOException { - if (mFrame == null) { + if (frame == null) { // Read in header information - readPICTHeader(mImageInput); + readPICTHeader(imageInput); + if (DEBUG) { System.out.println("Done reading PICT header!"); } } - return mFrame; + + return frame; } /** @@ -184,9 +186,10 @@ public class PICTImageReader extends ImageReaderBase { private void readPICTHeader0(final ImageInputStream pStream) throws IOException { // Get size - mPicSize = pStream.readUnsignedShort(); + picSize = pStream.readUnsignedShort(); + if (DEBUG) { - System.out.println("picSize: " + mPicSize); + System.out.println("picSize: " + picSize); } // Get frame at 72 dpi @@ -197,17 +200,17 @@ public class PICTImageReader extends ImageReaderBase { int h = pStream.readUnsignedShort(); int w = pStream.readUnsignedShort(); - mFrame = new Rectangle(x, y, w - x, h - y); - if (mFrame.width < 0 || mFrame.height < 0) { - throw new IIOException("Error in PICT header: Invalid frame " + mFrame); + frame = new Rectangle(x, y, w - x, h - y); + if (frame.width < 0 || frame.height < 0) { + throw new IIOException("Error in PICT header: Invalid frame " + frame); } if (DEBUG) { - System.out.println("mFrame: " + mFrame); + System.out.println("frame: " + frame); } // Set default display ratios. 72 dpi is the standard Macintosh resolution. - mScreenImageXRatio = 1.0; - mScreenImageYRatio = 1.0; + screenImageXRatio = 1.0; + screenImageYRatio = 1.0; // Get the version, since the way of reading the rest depends on it boolean isExtendedV2 = false; @@ -217,10 +220,10 @@ public class PICTImageReader extends ImageReaderBase { } if (version == (PICT.OP_VERSION << 8) + 0x01) { - mVersion = 1; + this.version = 1; } else if (version == PICT.OP_VERSION && pStream.readShort() == PICT.OP_VERSION_2) { - mVersion = 2; + this.version = 2; // Read in version 2 header op and test that it is valid: HeaderOp 0x0C00 if (pStream.readShort() != PICT.OP_HEADER_OP) { @@ -249,10 +252,10 @@ public class PICTImageReader extends ImageReaderBase { // int h (fixed point) double h2 = PICTUtil.readFixedPoint(pStream); - mScreenImageXRatio = (w - x) / (w2 - x2); - mScreenImageYRatio = (h - y) / (h2 - y2); + screenImageXRatio = (w - x) / (w2 - x2); + screenImageYRatio = (h - y) / (h2 - y2); - if (mScreenImageXRatio < 0 || mScreenImageYRatio < 0) { + if (screenImageXRatio < 0 || screenImageYRatio < 0) { throw new IIOException("Error in PICT header: Invalid bounds " + new Rectangle.Double(x2, y2, w2 - x2, h2 - y2)); } if (DEBUG) { @@ -288,10 +291,10 @@ public class PICTImageReader extends ImageReaderBase { // short w short w2 = pStream.readShort(); - mScreenImageXRatio = (w - x) / (double) (w2 - x2); - mScreenImageYRatio = (h - y) / (double) (h2 - y2); + screenImageXRatio = (w - x) / (double) (w2 - x2); + screenImageYRatio = (h - y) / (double) (h2 - y2); - if (mScreenImageXRatio < 0 || mScreenImageYRatio < 0) { + if (screenImageXRatio < 0 || screenImageYRatio < 0) { throw new IIOException("Error in PICT header: Invalid bounds " + new Rectangle.Double(x2, y2, w2 - x2, h2 - y2)); } if (DEBUG) { @@ -303,8 +306,8 @@ public class PICTImageReader extends ImageReaderBase { } if (DEBUG) { - System.out.println("screenImageXRatio: " + mScreenImageXRatio); - System.out.println("screenImageYRatio: " + mScreenImageYRatio); + System.out.println("screenImageXRatio: " + screenImageXRatio); + System.out.println("screenImageYRatio: " + screenImageYRatio); } } else { @@ -313,13 +316,13 @@ public class PICTImageReader extends ImageReaderBase { } if (DEBUG) { - System.out.println("Version: " + mVersion + (isExtendedV2 ? " extended" : "")); + System.out.println("Version: " + this.version + (isExtendedV2 ? " extended" : "")); } - mImageStartStreamPos = pStream.getStreamPosition(); + imageStartStreamPos = pStream.getStreamPosition(); // Won't need header data again (NOTE: We'll only get here if no exception is thrown) - pStream.flushBefore(mImageStartStreamPos); + pStream.flushBefore(imageStartStreamPos); } static void skipNullHeader(final ImageInputStream pStream) throws IOException { @@ -341,9 +344,9 @@ public class PICTImageReader extends ImageReaderBase { * @throws IOException if an I/O error occurs while reading the image. */ private void drawOnto(Graphics2D pGraphics) throws IOException { - mContext = new QuickDrawContext(pGraphics); + context = new QuickDrawContext(pGraphics); - readPICTopcodes(mImageInput); + readPICTopcodes(imageInput); if (DEBUG) { System.out.println("Done reading PICT body!"); } @@ -360,7 +363,7 @@ public class PICTImageReader extends ImageReaderBase { * @throws java.io.IOException if an I/O error occurs while reading the image. */ private void readPICTopcodes(ImageInputStream pStream) throws IOException { - pStream.seek(mImageStartStreamPos); + pStream.seek(imageStartStreamPos); int opCode, dh, dv, dataLength; byte[] colorBuffer = new byte[3 * PICT.COLOR_COMP_SIZE]; @@ -386,7 +389,7 @@ public class PICTImageReader extends ImageReaderBase { // Read from file until we read the end of picture opcode do { // Read opcode, version 1: byte, version 2: short - if (mVersion == 1) { + if (version == 1) { opCode = pStream.readUnsignedByte(); } else { @@ -431,7 +434,7 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_BK_PAT: // Get the data - mContext.setBackgroundPattern(PICTUtil.readPattern(pStream)); + context.setBackgroundPattern(PICTUtil.readPattern(pStream)); if (DEBUG) { System.out.println("bkPat"); } @@ -490,7 +493,7 @@ public class PICTImageReader extends ImageReaderBase { // Get the two words // NOTE: This is out of order, compared to other Points Dimension pnsize = new Dimension(pStream.readUnsignedShort(), pStream.readUnsignedShort()); - mContext.setPenSize(pnsize); + context.setPenSize(pnsize); if (DEBUG) { System.out.println("pnsize: " + pnsize); } @@ -503,12 +506,12 @@ public class PICTImageReader extends ImageReaderBase { System.out.println("pnMode: " + mode); } - mContext.setPenMode(mode); + context.setPenMode(mode); break; case PICT.OP_PN_PAT: - mContext.setPenPattern(PICTUtil.readPattern(pStream)); + context.setPenPattern(PICTUtil.readPattern(pStream)); if (DEBUG) { System.out.println("pnPat"); } @@ -557,7 +560,7 @@ public class PICTImageReader extends ImageReaderBase { // currentFont = mGraphics.getFont(); // mGraphics.setFont(new Font(currentFont.getName(), currentFont.getStyle(), tx_size)); //} - mContext.setTextSize(tx_size); + context.setTextSize(tx_size); if (DEBUG) { System.out.println("txSize: " + tx_size); } @@ -599,15 +602,15 @@ public class PICTImageReader extends ImageReaderBase { case 0x0012: // BkPixPat bg = PICTUtil.readColorPattern(pStream); - mContext.setBackgroundPattern(bg); + context.setBackgroundPattern(bg); break; case 0x0013: // PnPixPat pen = PICTUtil.readColorPattern(pStream); - mContext.setBackgroundPattern(pen); + context.setBackgroundPattern(pen); break; case 0x0014: // FillPixPat fill = PICTUtil.readColorPattern(pStream); - mContext.setBackgroundPattern(fill); + context.setBackgroundPattern(fill); break; case PICT.OP_PN_LOC_H_FRAC:// TO BE DONE??? @@ -650,7 +653,7 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_HILITE_MODE: // Change color to hilite color - mContext.setPenPattern(new BitMapPattern(hilight)); + context.setPenPattern(new BitMapPattern(hilight)); if (DEBUG) { System.out.println("opHiliteMode"); } @@ -691,14 +694,14 @@ public class PICTImageReader extends ImageReaderBase { y = getYPtCoord(pStream.readUnsignedShort()); x = getXPtCoord(pStream.readUnsignedShort()); - mPenPosition.setLocation(x, y); + penPosition.setLocation(x, y); // Move pen to new position, draw line - mContext.moveTo(origin); - mContext.lineTo(mPenPosition); + context.moveTo(origin); + context.lineTo(penPosition); if (DEBUG) { - System.out.println("line from: " + origin + " to: " + mPenPosition); + System.out.println("line from: " + origin + " to: " + penPosition); } break; @@ -708,10 +711,10 @@ public class PICTImageReader extends ImageReaderBase { x = getXPtCoord(pStream.readUnsignedShort()); // Draw line - mContext.line(x, y); + context.line(x, y); if (DEBUG) { - System.out.println("lineFrom to: " + mPenPosition); + System.out.println("lineFrom to: " + penPosition); } break; @@ -726,8 +729,8 @@ public class PICTImageReader extends ImageReaderBase { dh_dv = new Point(x, y); // Move pen to new position, draw line if we have a graphics - mPenPosition.setLocation(origin.x + dh_dv.x, origin.y + dh_dv.y); - mContext.lineTo(mPenPosition); + penPosition.setLocation(origin.x + dh_dv.x, origin.y + dh_dv.y); + context.lineTo(penPosition); if (DEBUG) { System.out.println("Short line origin: " + origin + ", dh,dv: " + dh_dv); @@ -740,7 +743,7 @@ public class PICTImageReader extends ImageReaderBase { x = getXPtCoord(pStream.readByte()); // Draw line - mContext.line(x, y); + context.line(x, y); if (DEBUG) { System.out.println("Short line from dh,dv: " + x + "," + y); @@ -765,30 +768,30 @@ public class PICTImageReader extends ImageReaderBase { y = getYPtCoord(pStream.readUnsignedShort()); x = getXPtCoord(pStream.readUnsignedShort()); origin = new Point(x, y); - mPenPosition = origin; - mContext.moveTo(mPenPosition); + penPosition = origin; + context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); // TODO //if (mGraphics != null) { - // mGraphics.drawString(text, mPenPosition.x, mPenPosition.y); + // mGraphics.drawString(text, penPosition.x, penPosition.y); //} - mContext.drawString(text); + context.drawString(text); if (DEBUG) { - System.out.println("longText origin: " + mPenPosition + ", text:" + text); + System.out.println("longText origin: " + penPosition + ", text:" + text); } break; case PICT.OP_DH_TEXT:// OK, not tested // Get dh dh = getXPtCoord(pStream.readByte()); - mPenPosition.translate(dh, 0); - mContext.moveTo(mPenPosition); + penPosition.translate(dh, 0); + context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); // TODO // if (mGraphics != null) { -// mGraphics.drawString(text, mPenPosition.x, mPenPosition.y); +// mGraphics.drawString(text, penPosition.x, penPosition.y); // } - mContext.drawString(text); + context.drawString(text); if (DEBUG) { System.out.println("DHText dh: " + dh + ", text:" + text); } @@ -797,14 +800,14 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_DV_TEXT:// OK, not tested // Get dh dv = getYPtCoord(pStream.readByte()); - mPenPosition.translate(0, dv); - mContext.moveTo(mPenPosition); + penPosition.translate(0, dv); + context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); // TODO //if (mGraphics != null) { - // mGraphics.drawString(text, mPenPosition.x, mPenPosition.y); + // mGraphics.drawString(text, penPosition.x, penPosition.y); //} - mContext.drawString(text); + context.drawString(text); if (DEBUG) { System.out.println("DVText dv: " + dv + ", text:" + text); } @@ -814,16 +817,16 @@ public class PICTImageReader extends ImageReaderBase { // Get dh, dv y = getYPtCoord(pStream.readByte()); x = getXPtCoord(pStream.readByte()); - mPenPosition.translate(x, y); - mContext.moveTo(mPenPosition); + penPosition.translate(x, y); + context.moveTo(penPosition); text = PICTUtil.readPascalString(pStream); // TODO //if (mGraphics != null) { - // mGraphics.drawString(text, mPenPosition.x, mPenPosition.y); + // mGraphics.drawString(text, penPosition.x, penPosition.y); //} - mContext.drawString(text); + context.drawString(text); if (DEBUG) { - System.out.println("DHDVText penPosition: " + mPenPosition + ", text:" + text); + System.out.println("DHDVText penPosition: " + penPosition + ", text:" + text); } break; @@ -843,7 +846,7 @@ public class PICTImageReader extends ImageReaderBase { // mGraphics.setFont(Font.decode(text) // .deriveFont(currentFont.getStyle(), currentFont.getSize())); //} - mContext.drawString(text); + context.drawString(text); if (DEBUG) { System.out.println("fontName: \"" + text +"\""); } @@ -882,7 +885,7 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_INVERT_RECT:// OK, not tested case PICT.OP_FILL_RECT:// OK, not tested // Get the frame rectangle - readRectangle(pStream, mLastRectangle); + readRectangle(pStream, lastRectangle); case PICT.OP_FRAME_SAME_RECT:// OK, not tested case PICT.OP_PAINT_SAME_RECT:// OK, not tested @@ -893,23 +896,23 @@ public class PICTImageReader extends ImageReaderBase { switch (opCode) { case PICT.OP_FRAME_RECT: case PICT.OP_FRAME_SAME_RECT: - mContext.frameRect(mLastRectangle); + context.frameRect(lastRectangle); break; case PICT.OP_PAINT_RECT: case PICT.OP_PAINT_SAME_RECT: - mContext.paintRect(mLastRectangle); + context.paintRect(lastRectangle); break; case PICT.OP_ERASE_RECT: case PICT.OP_ERASE_SAME_RECT: - mContext.eraseRect(mLastRectangle); + context.eraseRect(lastRectangle); break; case PICT.OP_INVERT_RECT: case PICT.OP_INVERT_SAME_RECT: - mContext.invertRect(mLastRectangle); + context.invertRect(lastRectangle); break; case PICT.OP_FILL_RECT: case PICT.OP_FILL_SAME_RECT: - mContext.fillRect(mLastRectangle, fill); + context.fillRect(lastRectangle, fill); break; } @@ -917,34 +920,34 @@ public class PICTImageReader extends ImageReaderBase { if (DEBUG) { switch (opCode) { case PICT.OP_FRAME_RECT: - System.out.println("frameRect: " + mLastRectangle); + System.out.println("frameRect: " + lastRectangle); break; case PICT.OP_PAINT_RECT: - System.out.println("paintRect: " + mLastRectangle); + System.out.println("paintRect: " + lastRectangle); break; case PICT.OP_ERASE_RECT: - System.out.println("eraseRect: " + mLastRectangle); + System.out.println("eraseRect: " + lastRectangle); break; case PICT.OP_INVERT_RECT: - System.out.println("invertRect: " + mLastRectangle); + System.out.println("invertRect: " + lastRectangle); break; case PICT.OP_FILL_RECT: - System.out.println("fillRect: " + mLastRectangle); + System.out.println("fillRect: " + lastRectangle); break; case PICT.OP_FRAME_SAME_RECT: - System.out.println("frameSameRect: " + mLastRectangle); + System.out.println("frameSameRect: " + lastRectangle); break; case PICT.OP_PAINT_SAME_RECT: - System.out.println("paintSameRect: " + mLastRectangle); + System.out.println("paintSameRect: " + lastRectangle); break; case PICT.OP_ERASE_SAME_RECT: - System.out.println("eraseSameRect: " + mLastRectangle); + System.out.println("eraseSameRect: " + lastRectangle); break; case PICT.OP_INVERT_SAME_RECT: - System.out.println("invertSameRect: " + mLastRectangle); + System.out.println("invertSameRect: " + lastRectangle); break; case PICT.OP_FILL_SAME_RECT: - System.out.println("fillSameRect: " + mLastRectangle); + System.out.println("fillSameRect: " + lastRectangle); break; } } @@ -969,7 +972,7 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_INVERT_R_RECT:// OK, not tested case PICT.OP_FILL_R_RECT:// OK, not tested // Get the frame rectangle - readRectangle(pStream, mLastRectangle); + readRectangle(pStream, lastRectangle); case PICT.OP_FRAME_SAME_R_RECT:// OK, not tested case PICT.OP_PAINT_SAME_R_RECT:// OK, not tested @@ -980,23 +983,23 @@ public class PICTImageReader extends ImageReaderBase { switch (opCode) { case PICT.OP_FRAME_R_RECT: case PICT.OP_FRAME_SAME_R_RECT: - mContext.frameRoundRect(mLastRectangle, ovSize.x, ovSize.y); + context.frameRoundRect(lastRectangle, ovSize.x, ovSize.y); break; case PICT.OP_PAINT_R_RECT: case PICT.OP_PAINT_SAME_R_RECT: - mContext.paintRoundRect(mLastRectangle, ovSize.x, ovSize.y); + context.paintRoundRect(lastRectangle, ovSize.x, ovSize.y); break; case PICT.OP_ERASE_R_RECT: case PICT.OP_ERASE_SAME_R_RECT: - mContext.eraseRoundRect(mLastRectangle, ovSize.x, ovSize.y); + context.eraseRoundRect(lastRectangle, ovSize.x, ovSize.y); break; case PICT.OP_INVERT_R_RECT: case PICT.OP_INVERT_SAME_R_RECT: - mContext.invertRoundRect(mLastRectangle, ovSize.x, ovSize.y); + context.invertRoundRect(lastRectangle, ovSize.x, ovSize.y); break; case PICT.OP_FILL_R_RECT: case PICT.OP_FILL_SAME_R_RECT: - mContext.fillRoundRect(mLastRectangle, ovSize.x, ovSize.y, fill); + context.fillRoundRect(lastRectangle, ovSize.x, ovSize.y, fill); break; } @@ -1004,34 +1007,34 @@ public class PICTImageReader extends ImageReaderBase { if (DEBUG) { switch (opCode) { case PICT.OP_FRAME_R_RECT: - System.out.println("frameRRect: " + mLastRectangle); + System.out.println("frameRRect: " + lastRectangle); break; case PICT.OP_PAINT_R_RECT: - System.out.println("paintRRect: " + mLastRectangle); + System.out.println("paintRRect: " + lastRectangle); break; case PICT.OP_ERASE_R_RECT: - System.out.println("eraseRRect: " + mLastRectangle); + System.out.println("eraseRRect: " + lastRectangle); break; case PICT.OP_INVERT_R_RECT: - System.out.println("invertRRect: " + mLastRectangle); + System.out.println("invertRRect: " + lastRectangle); break; case PICT.OP_FILL_R_RECT: - System.out.println("fillRRect: " + mLastRectangle); + System.out.println("fillRRect: " + lastRectangle); break; case PICT.OP_FRAME_SAME_R_RECT: - System.out.println("frameSameRRect: " + mLastRectangle); + System.out.println("frameSameRRect: " + lastRectangle); break; case PICT.OP_PAINT_SAME_R_RECT: - System.out.println("paintSameRRect: " + mLastRectangle); + System.out.println("paintSameRRect: " + lastRectangle); break; case PICT.OP_ERASE_SAME_R_RECT: - System.out.println("eraseSameRRect: " + mLastRectangle); + System.out.println("eraseSameRRect: " + lastRectangle); break; case PICT.OP_INVERT_SAME_R_RECT: - System.out.println("invertSameRRect: " + mLastRectangle); + System.out.println("invertSameRRect: " + lastRectangle); break; case PICT.OP_FILL_SAME_R_RECT: - System.out.println("fillSameRRect: " + mLastRectangle); + System.out.println("fillSameRRect: " + lastRectangle); break; } } @@ -1048,7 +1051,7 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_INVERT_OVAL:// OK, not tested case PICT.OP_FILL_OVAL:// OK, not tested // Get the frame rectangle - readRectangle(pStream, mLastRectangle); + readRectangle(pStream, lastRectangle); case PICT.OP_FRAME_SAME_OVAL:// OK, not tested case PICT.OP_PAINT_SAME_OVAL:// OK, not tested case PICT.OP_ERASE_SAME_OVAL:// OK, not tested @@ -1058,23 +1061,23 @@ public class PICTImageReader extends ImageReaderBase { switch (opCode) { case PICT.OP_FRAME_OVAL: case PICT.OP_FRAME_SAME_OVAL: - mContext.frameOval(mLastRectangle); + context.frameOval(lastRectangle); break; case PICT.OP_PAINT_OVAL: case PICT.OP_PAINT_SAME_OVAL: - mContext.paintOval(mLastRectangle); + context.paintOval(lastRectangle); break; case PICT.OP_ERASE_OVAL: case PICT.OP_ERASE_SAME_OVAL: - mContext.eraseOval(mLastRectangle); + context.eraseOval(lastRectangle); break; case PICT.OP_INVERT_OVAL: case PICT.OP_INVERT_SAME_OVAL: - mContext.invertOval(mLastRectangle); + context.invertOval(lastRectangle); break; case PICT.OP_FILL_OVAL: case PICT.OP_FILL_SAME_OVAL: - mContext.fillOval(mLastRectangle, fill); + context.fillOval(lastRectangle, fill); break; } @@ -1082,34 +1085,34 @@ public class PICTImageReader extends ImageReaderBase { if (DEBUG) { switch (opCode) { case PICT.OP_FRAME_OVAL: - System.out.println("frameOval: " + mLastRectangle); + System.out.println("frameOval: " + lastRectangle); break; case PICT.OP_PAINT_OVAL: - System.out.println("paintOval: " + mLastRectangle); + System.out.println("paintOval: " + lastRectangle); break; case PICT.OP_ERASE_OVAL: - System.out.println("eraseOval: " + mLastRectangle); + System.out.println("eraseOval: " + lastRectangle); break; case PICT.OP_INVERT_OVAL: - System.out.println("invertOval: " + mLastRectangle); + System.out.println("invertOval: " + lastRectangle); break; case PICT.OP_FILL_OVAL: - System.out.println("fillOval: " + mLastRectangle); + System.out.println("fillOval: " + lastRectangle); break; case PICT.OP_FRAME_SAME_OVAL: - System.out.println("frameSameOval: " + mLastRectangle); + System.out.println("frameSameOval: " + lastRectangle); break; case PICT.OP_PAINT_SAME_OVAL: - System.out.println("paintSameOval: " + mLastRectangle); + System.out.println("paintSameOval: " + lastRectangle); break; case PICT.OP_ERASE_SAME_OVAL: - System.out.println("eraseSameOval: " + mLastRectangle); + System.out.println("eraseSameOval: " + lastRectangle); break; case PICT.OP_INVERT_SAME_OVAL: - System.out.println("invertSameOval: " + mLastRectangle); + System.out.println("invertSameOval: " + lastRectangle); break; case PICT.OP_FILL_SAME_OVAL: - System.out.println("fillSameOval: " + mLastRectangle); + System.out.println("fillSameOval: " + lastRectangle); break; } } @@ -1141,7 +1144,7 @@ public class PICTImageReader extends ImageReaderBase { case PICT.OP_INVERT_ARC:// OK, not tested case PICT.OP_FILL_ARC:// OK, not tested // Get the frame rectangle - readRectangle(pStream, mLastRectangle); + readRectangle(pStream, lastRectangle); case PICT.OP_FRAME_SAME_ARC:// OK, not tested case PICT.OP_PAINT_SAME_ARC:// OK, not tested case PICT.OP_ERASE_SAME_ARC:// OK, not tested @@ -1159,23 +1162,23 @@ public class PICTImageReader extends ImageReaderBase { switch (opCode) { case PICT.OP_FRAME_ARC: case PICT.OP_FRAME_SAME_ARC: - mContext.frameArc(mLastRectangle, arcAngles.x, arcAngles.y); + context.frameArc(lastRectangle, arcAngles.x, arcAngles.y); break; case PICT.OP_PAINT_ARC: case PICT.OP_PAINT_SAME_ARC: - mContext.paintArc(mLastRectangle, arcAngles.x, arcAngles.y); + context.paintArc(lastRectangle, arcAngles.x, arcAngles.y); break; case PICT.OP_ERASE_ARC: case PICT.OP_ERASE_SAME_ARC: - mContext.eraseArc(mLastRectangle, arcAngles.x, arcAngles.y); + context.eraseArc(lastRectangle, arcAngles.x, arcAngles.y); break; case PICT.OP_INVERT_ARC: case PICT.OP_INVERT_SAME_ARC: - mContext.invertArc(mLastRectangle, arcAngles.x, arcAngles.y); + context.invertArc(lastRectangle, arcAngles.x, arcAngles.y); break; case PICT.OP_FILL_ARC: case PICT.OP_FILL_SAME_ARC: - mContext.fillArc(mLastRectangle, arcAngles.x, arcAngles.y, fill); + context.fillArc(lastRectangle, arcAngles.x, arcAngles.y, fill); break; } @@ -1183,34 +1186,34 @@ public class PICTImageReader extends ImageReaderBase { if (DEBUG) { switch (opCode) { case PICT.OP_FRAME_ARC: - System.out.println("frameArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("frameArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_PAINT_ARC: - System.out.println("paintArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("paintArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_ERASE_ARC: - System.out.println("eraseArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("eraseArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_INVERT_ARC: - System.out.println("invertArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("invertArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_FILL_ARC: - System.out.println("fillArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("fillArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_FRAME_SAME_ARC: - System.out.println("frameSameArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("frameSameArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_PAINT_SAME_ARC: - System.out.println("paintSameArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("paintSameArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_ERASE_SAME_ARC: - System.out.println("eraseSameArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("eraseSameArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_INVERT_SAME_ARC: - System.out.println("invertSameArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("invertSameArc: " + lastRectangle + ", angles:" + arcAngles); break; case PICT.OP_FILL_SAME_ARC: - System.out.println("fillSameArc: " + mLastRectangle + ", angles:" + arcAngles); + System.out.println("fillSameArc: " + lastRectangle + ", angles:" + arcAngles); break; } } @@ -1256,23 +1259,23 @@ public class PICTImageReader extends ImageReaderBase { switch (opCode) { case PICT.OP_FRAME_POLY: case PICT.OP_FRAME_SAME_POLY: - mContext.framePoly(polygon); + context.framePoly(polygon); break; case PICT.OP_PAINT_POLY: case PICT.OP_PAINT_SAME_POLY: - mContext.paintPoly(polygon); + context.paintPoly(polygon); break; case PICT.OP_ERASE_POLY: case PICT.OP_ERASE_SAME_POLY: - mContext.erasePoly(polygon); + context.erasePoly(polygon); break; case PICT.OP_INVERT_POLY: case PICT.OP_INVERT_SAME_POLY: - mContext.invertPoly(polygon); + context.invertPoly(polygon); break; case PICT.OP_FILL_POLY: case PICT.OP_FILL_SAME_POLY: - mContext.fillPoly(polygon, fill); + context.fillPoly(polygon, fill); break; } @@ -1346,23 +1349,23 @@ public class PICTImageReader extends ImageReaderBase { switch (opCode) { case PICT.OP_FRAME_RGN: case PICT.OP_FRAME_SAME_RGN: - mContext.frameRegion(new Area(region)); + context.frameRegion(new Area(region)); break; case PICT.OP_PAINT_RGN: case PICT.OP_PAINT_SAME_RGN: - mContext.paintRegion(new Area(region)); + context.paintRegion(new Area(region)); break; case PICT.OP_ERASE_RGN: case PICT.OP_ERASE_SAME_RGN: - mContext.eraseRegion(new Area(region)); + context.eraseRegion(new Area(region)); break; case PICT.OP_INVERT_RGN: case PICT.OP_INVERT_SAME_RGN: - mContext.invertRegion(new Area(region)); + context.invertRegion(new Area(region)); break; case PICT.OP_FILL_RGN: case PICT.OP_FILL_SAME_RGN: - mContext.fillRegion(new Area(region), fill); + context.fillRegion(new Area(region), fill); break; } } @@ -1461,7 +1464,7 @@ public class PICTImageReader extends ImageReaderBase { readRectangle(pStream, dstRect); mode = pStream.readUnsignedShort(); - mContext.setPenMode(mode); // TODO: Or parameter? + context.setPenMode(mode); // TODO: Or parameter? if (DEBUG) { System.out.print("bitsRect, rowBytes: " + rowBytes); @@ -1491,7 +1494,7 @@ public class PICTImageReader extends ImageReaderBase { // Draw pixel data Rectangle rect = new Rectangle(srcRect); rect.translate(-bounds.x, -bounds.y); - mContext.copyBits(image, rect, dstRect, mode, null); + context.copyBits(image, rect, dstRect, mode, null); //mGraphics.drawImage(image, // dstRect.x, dstRect.y, // dstRect.x + dstRect.width, dstRect.y + dstRect.height, @@ -1666,7 +1669,7 @@ public class PICTImageReader extends ImageReaderBase { catch (EOFException e) { String pos; try { - pos = String.format("position %d", mImageInput.getStreamPosition()); + pos = String.format("position %d", imageInput.getStreamPosition()); } catch (IOException ignore) { pos = "unknown position"; @@ -1729,7 +1732,7 @@ public class PICTImageReader extends ImageReaderBase { BufferedImage image = QuickTime.decompress(pStream); if (image != null) { - mContext.copyBits(image, new Rectangle(image.getWidth(), image.getHeight()), destination, QuickDraw.SRC_COPY, null); + context.copyBits(image, new Rectangle(image.getWidth(), image.getHeight()), destination, QuickDraw.SRC_COPY, null); pStream.seek(pos + dataLength); // Might be word-align mismatch here @@ -1977,7 +1980,7 @@ public class PICTImageReader extends ImageReaderBase { unPackBits.readFully(pixArray, pixBufOffset, pBounds.width); /*} else { - mImageInput.readFully(dstBytes); + imageInput.readFully(dstBytes); }*/ // TODO: Use TYPE_USHORT_555_RGB for 16 bit @@ -2068,7 +2071,7 @@ public class PICTImageReader extends ImageReaderBase { // We add all new images to it. If we are just replaying, then // "pPixmapCount" will never be greater than the size of the vector - if (mImages.size() <= pPixmapCount) { + if (images.size() <= pPixmapCount) { // Create BufferedImage and add buffer it for multiple reads // DirectColorModel cm = (DirectColorModel) ColorModel.getRGBdefault(); // DataBuffer db = new DataBufferInt(pixArray, pixArray.length); @@ -2077,15 +2080,15 @@ public class PICTImageReader extends ImageReaderBase { WritableRaster raster = Raster.createPackedRaster(db, pBounds.width, pBounds.height, cmpSize, null); // TODO: last param should ideally be srcRect.getLocation() BufferedImage img = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null); - mImages.add(img); + images.add(img); } // Draw the image - BufferedImage img = mImages.get(pPixmapCount); + BufferedImage img = images.get(pPixmapCount); if (img != null) { // TODO: FixMe.. Seems impossible to create a bufferedImage with a raster not starting at 0,0 srcRect.setLocation(0, 0); // should not require this line.. - mContext.copyBits(img, srcRect, dstRect, transferMode, null); + context.copyBits(img, srcRect, dstRect, transferMode, null); } // Line break at the end @@ -2294,7 +2297,7 @@ public class PICTImageReader extends ImageReaderBase { unPackBits.readFully(dstBytes); } else { - mImageInput.readFully(dstBytes); + imageInput.readFully(dstBytes); } if (packType == 3) { @@ -2378,7 +2381,7 @@ public class PICTImageReader extends ImageReaderBase { // We add all new images to it. If we are just replaying, then // "pPixmapCount" will never be greater than the size of the vector - if (mImages.size() <= pPixmapCount) { + if (images.size() <= pPixmapCount) { // Create BufferedImage and add buffer it for multiple reads DirectColorModel cm; WritableRaster raster; @@ -2396,15 +2399,15 @@ public class PICTImageReader extends ImageReaderBase { BufferedImage img = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - mImages.add(img); + images.add(img); } // Draw the image - BufferedImage img = mImages.get(pPixmapCount); + BufferedImage img = images.get(pPixmapCount); if (img != null) { // TODO: FixMe.. Something wrong here, might be the copyBits methods. srcRect.setLocation(0, 0); // should not require this line.. - mContext.copyBits(img, srcRect, dstRect, transferMode, null); + context.copyBits(img, srcRect, dstRect, transferMode, null); } // Line break at the end @@ -2551,7 +2554,7 @@ public class PICTImageReader extends ImageReaderBase { * image resolution ratio. */ private int getXPtCoord(int pPoint) { - return (int) (pPoint / mScreenImageXRatio); + return (int) (pPoint / screenImageXRatio); } /* @@ -2560,7 +2563,7 @@ public class PICTImageReader extends ImageReaderBase { * image resolution ratio. */ private int getYPtCoord(int pPoint) { - return (int) (pPoint / mScreenImageYRatio); + return (int) (pPoint / screenImageYRatio); } /* @@ -2621,7 +2624,7 @@ public class PICTImageReader extends ImageReaderBase { try { // TODO: Might need to clear background - g.setTransform(AffineTransform.getScaleInstance(mScreenImageXRatio / subX, mScreenImageYRatio / subY)); + g.setTransform(AffineTransform.getScaleInstance(screenImageXRatio / subX, screenImageYRatio / subY)); // try { drawOnto(g); // } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java index 7842cd5d..5f19f0ae 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java @@ -75,8 +75,8 @@ public class PICTImageReaderSpi extends ImageReaderSpi { } ImageInputStream stream = (ImageInputStream) pSource; - stream.mark(); + try { if (isPICT(stream)) { // If PICT Clipping format, return true immediately @@ -87,6 +87,7 @@ public class PICTImageReaderSpi extends ImageReaderSpi { stream.reset(); PICTImageReader.skipNullHeader(stream); } + return isPICT(stream); } catch (EOFException ignore) { diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java index 9149fb5b..a4baa53c 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriter.java @@ -28,9 +28,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Parts of this software is based on JVG/JIS. See http://www.cs.hut.fi/~framling/JVG/index.html for more information. -Redistribution under BSD authorized by Kary Främling: +Redistribution under BSD authorized by Kary Fr�mling: -Copyright (c) 2003, Kary Främling +Copyright (c) 2003, Kary Fr�mling All rights reserved. Redistribution and use in source and binary forms, with or without @@ -79,16 +79,16 @@ import java.io.*; * Images are stored using the "opDirectBitsRect" opcode, which directly * stores RGB values (using PackBits run-length encoding). * - * @author Kary Främling + * @author Kary Främling * @author Harald Kuhr * @version $Id: PICTWriter.java,v 1.0 05.apr.2006 15:20:48 haku Exp$ */ public class PICTImageWriter extends ImageWriterBase { // TODO: Inline these? - private int mRowBytes; - private byte[] mScanlineBytes; - private int mScanWidthLeft; + private int rowBytes; + private byte[] scanlineBytes; + private int scanWidthLeft; public PICTImageWriter() { this(null); @@ -112,137 +112,138 @@ public class PICTImageWriter extends ImageWriterBase { super(pProvider); } - private void writePICTHeader(RenderedImage pImage) throws IOException { + private void writePICTHeader(final RenderedImage pImage) throws IOException { // TODO: Make 512 byte header optional // Write empty 512-byte header byte[] buf = new byte[PICT.PICT_NULL_HEADER_SIZE]; - mImageOutput.write(buf); + imageOutput.write(buf); // Write out the size, leave as 0, this is ok - mImageOutput.writeShort(0); + imageOutput.writeShort(0); // Write image frame (same as image bounds) - mImageOutput.writeShort(0); - mImageOutput.writeShort(0); - mImageOutput.writeShort(pImage.getHeight()); - mImageOutput.writeShort(pImage.getWidth()); + imageOutput.writeShort(0); + imageOutput.writeShort(0); + imageOutput.writeShort(pImage.getHeight()); + imageOutput.writeShort(pImage.getWidth()); // Write version, version 2 - mImageOutput.writeShort(PICT.OP_VERSION); - mImageOutput.writeShort(PICT.OP_VERSION_2); + imageOutput.writeShort(PICT.OP_VERSION); + imageOutput.writeShort(PICT.OP_VERSION_2); // Version 2 HEADER_OP, extended version. - mImageOutput.writeShort(PICT.OP_HEADER_OP); - mImageOutput.writeInt(PICT.HEADER_V2_EXT); // incl 2 bytes reseverd + imageOutput.writeShort(PICT.OP_HEADER_OP); + imageOutput.writeInt(PICT.HEADER_V2_EXT); // incl 2 bytes reseverd // Image resolution, 72 dpi - mImageOutput.writeShort(PICT.MAC_DEFAULT_DPI); - mImageOutput.writeShort(0); - mImageOutput.writeShort(PICT.MAC_DEFAULT_DPI); - mImageOutput.writeShort(0); + imageOutput.writeShort(PICT.MAC_DEFAULT_DPI); + imageOutput.writeShort(0); + imageOutput.writeShort(PICT.MAC_DEFAULT_DPI); + imageOutput.writeShort(0); // Optimal source rectangle (same as image bounds) - mImageOutput.writeShort(0); - mImageOutput.writeShort(0); - mImageOutput.writeShort(pImage.getHeight()); - mImageOutput.writeShort(pImage.getWidth()); + imageOutput.writeShort(0); + imageOutput.writeShort(0); + imageOutput.writeShort(pImage.getHeight()); + imageOutput.writeShort(pImage.getWidth()); // Reserved (4 bytes) - mImageOutput.writeInt(0); + imageOutput.writeInt(0); // TODO: The header really ends here... // Highlight - mImageOutput.writeShort(PICT.OP_DEF_HILITE); + imageOutput.writeShort(PICT.OP_DEF_HILITE); // Set the clip rectangle - mImageOutput.writeShort(PICT.OP_CLIP_RGN); - mImageOutput.writeShort(10); - mImageOutput.writeShort(0); - mImageOutput.writeShort(0); - mImageOutput.writeShort(pImage.getHeight()); - mImageOutput.writeShort(pImage.getWidth()); + imageOutput.writeShort(PICT.OP_CLIP_RGN); + imageOutput.writeShort(10); + imageOutput.writeShort(0); + imageOutput.writeShort(0); + imageOutput.writeShort(pImage.getHeight()); + imageOutput.writeShort(pImage.getWidth()); // Pixmap operation - mImageOutput.writeShort(PICT.OP_DIRECT_BITS_RECT); + imageOutput.writeShort(PICT.OP_DIRECT_BITS_RECT); // PixMap pointer (always 0x000000FF); - mImageOutput.writeInt(0x000000ff); + imageOutput.writeInt(0x000000ff); // Write rowBytes, this is 4 times the width. // Set the high bit, to indicate a PixMap. - mRowBytes = 4 * pImage.getWidth(); - mImageOutput.writeShort(0x8000 | mRowBytes); + rowBytes = 4 * pImage.getWidth(); + imageOutput.writeShort(0x8000 | rowBytes); // Write bounds rectangle (same as image bounds) - mImageOutput.writeShort(0); - mImageOutput.writeShort(0); - mImageOutput.writeShort(pImage.getHeight()); // TODO: Handle overflow? - mImageOutput.writeShort(pImage.getWidth()); + imageOutput.writeShort(0); + imageOutput.writeShort(0); + imageOutput.writeShort(pImage.getHeight()); // TODO: Handle overflow? + imageOutput.writeShort(pImage.getWidth()); // PixMap record version - mImageOutput.writeShort(0); + imageOutput.writeShort(0); // Packing format (always 4: PackBits) - mImageOutput.writeShort(4); + imageOutput.writeShort(4); // Size of packed data (leave as 0) - mImageOutput.writeInt(0); + imageOutput.writeInt(0); // Pixmap resolution, 72 dpi - mImageOutput.writeShort(PICT.MAC_DEFAULT_DPI); - mImageOutput.writeShort(0); - mImageOutput.writeShort(PICT.MAC_DEFAULT_DPI); - mImageOutput.writeShort(0); + imageOutput.writeShort(PICT.MAC_DEFAULT_DPI); + imageOutput.writeShort(0); + imageOutput.writeShort(PICT.MAC_DEFAULT_DPI); + imageOutput.writeShort(0); // Pixel type, 16 is allright for direct pixels - mImageOutput.writeShort(16); + imageOutput.writeShort(16); + // TODO: Support others? // Pixel size - mImageOutput.writeShort(32); + imageOutput.writeShort(32); // TODO: Allow alpha? Allow 5 bit per pixel component (16 bit)? // Pixel component count - mImageOutput.writeShort(3); + imageOutput.writeShort(3); // Pixel component size - mImageOutput.writeShort(8); + imageOutput.writeShort(8); // PlaneBytes, ignored for now - mImageOutput.writeInt(0); + imageOutput.writeInt(0); // TODO: Allow IndexColorModel? // ColorTable record (for RGB direct pixels, just write 0) - mImageOutput.writeInt(0); + imageOutput.writeInt(0); // Reserved (4 bytes) - mImageOutput.writeInt(0); + imageOutput.writeInt(0); // Source and dest rect (both are same as image bounds) - mImageOutput.writeShort(0); - mImageOutput.writeShort(0); - mImageOutput.writeShort(pImage.getHeight()); - mImageOutput.writeShort(pImage.getWidth()); + imageOutput.writeShort(0); + imageOutput.writeShort(0); + imageOutput.writeShort(pImage.getHeight()); + imageOutput.writeShort(pImage.getWidth()); - mImageOutput.writeShort(0); - mImageOutput.writeShort(0); - mImageOutput.writeShort(pImage.getHeight()); - mImageOutput.writeShort(pImage.getWidth()); + imageOutput.writeShort(0); + imageOutput.writeShort(0); + imageOutput.writeShort(pImage.getHeight()); + imageOutput.writeShort(pImage.getWidth()); // Transfer mode - mImageOutput.writeShort(QuickDraw.SRC_COPY); + imageOutput.writeShort(QuickDraw.SRC_COPY); // TODO: Move to writePICTData? // TODO: Alpha support // Set up the buffers for storing scanline bytes - mScanlineBytes = new byte[3 * pImage.getWidth()]; - mScanWidthLeft = pImage.getWidth(); + scanlineBytes = new byte[3 * pImage.getWidth()]; + scanWidthLeft = pImage.getWidth(); } private void writePICTData(int x, int y, int w, int h, ColorModel model, byte[] pixels, int off, int scansize) throws IOException { - ByteArrayOutputStream bytes = new FastByteArrayOutputStream(mScanlineBytes.length / 2); + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(scanlineBytes.length / 2); int components = model.getNumComponents(); @@ -252,45 +253,46 @@ public class PICTImageWriter extends ImageWriterBase { // lines (h > 1) and (w < width). This should never be the case. for (int i = 0; i < h; i++) { // Reduce the counter of bytes left on the scanline. - mScanWidthLeft -= w; + scanWidthLeft -= w; // Treat the scanline. for (int j = 0; j < w; j++) { if (model instanceof ComponentColorModel && model.getColorSpace().getType() == ColorSpace.TYPE_RGB) { - // TODO: Component order? + // NOTE: Assumes component order always (A)BGR // TODO: Alpha support - mScanlineBytes[x + j] = pixels[off + i * scansize * components + components * j + 2]; - mScanlineBytes[x + w + j] = pixels[off + i * scansize * components + components * j + 1]; - mScanlineBytes[x + 2 * w + j] = pixels[off + i * scansize * components + components * j]; + scanlineBytes[x + j] = pixels[off + i * scansize * components + components * j + components - 1]; + scanlineBytes[x + w + j] = pixels[off + i * scansize * components + components * j + components - 2]; + scanlineBytes[x + 2 * w + j] = pixels[off + i * scansize * components + components * j + components - 3]; } else { int rgb = model.getRGB(pixels[off + i * scansize + j] & 0xFF); // Set red, green and blue components. - mScanlineBytes[x + j] = (byte) ((rgb >> 16) & 0xFF); - mScanlineBytes[x + w + j] = (byte) ((rgb >> 8) & 0xFF); - mScanlineBytes[x + 2 * w + j] = (byte) (rgb & 0xFF); + scanlineBytes[x + j] = (byte) ((rgb >> 16) & 0xFF); + scanlineBytes[x + w + j] = (byte) ((rgb >> 8) & 0xFF); + scanlineBytes[x + 2 * w + j] = (byte) ((rgb ) & 0xFF); } - } // If we have a complete scanline, then pack it and write it out. - if (mScanWidthLeft == 0) { + if (scanWidthLeft == 0) { // Pack using PackBitsEncoder/EncoderStream bytes.reset(); DataOutput packBits = new DataOutputStream(new EncoderStream(bytes, new PackBitsEncoder(), true)); - packBits.write(mScanlineBytes); + packBits.write(scanlineBytes); - if (mRowBytes > 250) { - mImageOutput.writeShort(bytes.size()); + if (rowBytes > 250) { + imageOutput.writeShort(bytes.size()); } else { - mImageOutput.writeByte(bytes.size()); + imageOutput.writeByte(bytes.size()); } - bytes.writeTo(IIOUtil.createStreamAdapter(mImageOutput)); + OutputStream adapter = IIOUtil.createStreamAdapter(imageOutput); + bytes.writeTo(adapter); + adapter.flush(); - mScanWidthLeft = w; + scanWidthLeft = w; } } } @@ -298,7 +300,7 @@ public class PICTImageWriter extends ImageWriterBase { private void writePICTData(int x, int y, int w, int h, ColorModel model, int[] pixels, int off, int scansize) throws IOException { - ByteArrayOutputStream bytes = new FastByteArrayOutputStream(mScanlineBytes.length / 2); + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(scanlineBytes.length / 2); // TODO: Clean up, as we only have complete scanlines @@ -306,54 +308,59 @@ public class PICTImageWriter extends ImageWriterBase { // lines (h > 1) and (w < width). This should never be the case. for (int i = 0; i < h; i++) { // Reduce the counter of bytes left on the scanline. - mScanWidthLeft -= w; + scanWidthLeft -= w; // Treat the scanline. for (int j = 0; j < w; j++) { int rgb = model.getRGB(pixels[off + i * scansize + j]); // Set red, green and blue components. - mScanlineBytes[x + j] = (byte) ((rgb >> 16) & 0xFF); - mScanlineBytes[x + w + j] = (byte) ((rgb >> 8) & 0xFF); - mScanlineBytes[x + 2 * w + j] = (byte) (rgb & 0xFF); + scanlineBytes[x + j] = (byte) ((rgb >> 16) & 0xFF); + scanlineBytes[x + w + j] = (byte) ((rgb >> 8) & 0xFF); + scanlineBytes[x + 2 * w + j] = (byte) ((rgb ) & 0xFF); } // If we have a complete scanline, then pack it and write it out. - if (mScanWidthLeft == 0) { + if (scanWidthLeft == 0) { // Pack using PackBitsEncoder/EncoderStream bytes.reset(); DataOutput packBits = new DataOutputStream(new EncoderStream(bytes, new PackBitsEncoder(), true)); - packBits.write(mScanlineBytes); + packBits.write(scanlineBytes); - if (mRowBytes > 250) { - mImageOutput.writeShort(bytes.size()); + if (rowBytes > 250) { + imageOutput.writeShort(bytes.size()); } else { - mImageOutput.writeByte(bytes.size()); + imageOutput.writeByte(bytes.size()); } - bytes.writeTo(IIOUtil.createStreamAdapter(mImageOutput)); + OutputStream adapter = IIOUtil.createStreamAdapter(imageOutput); + bytes.writeTo(adapter); + adapter.flush(); - mScanWidthLeft = w; + scanWidthLeft = w; } + + processImageProgress((100f * i) / h); } } private void writePICTTrailer() throws IOException { // Write out end opcode. Be sure to be word-aligned. - long length = mImageOutput.length(); + long length = imageOutput.length(); if (length == -1) { throw new IIOException("Cannot write trailer without knowing length"); } if ((length & 1) > 0) { - mImageOutput.writeByte(0); + imageOutput.writeByte(0); } - mImageOutput.writeShort(PICT.OP_END_OF_PICTURE); + + imageOutput.writeShort(PICT.OP_END_OF_PICTURE); } - public void write(IIOMetadata pStreamMetadata, IIOImage pImage, ImageWriteParam pParam) throws IOException { + public void write(final IIOMetadata pStreamMetadata, final IIOImage pImage, final ImageWriteParam pParam) throws IOException { assertOutput(); if (pImage.hasRaster()) { @@ -369,14 +376,18 @@ public class PICTImageWriter extends ImageWriterBase { Raster raster = image instanceof BufferedImage ? ((BufferedImage) image).getRaster() : image.getData(); DataBuffer buf = raster.getDataBuffer(); if (buf instanceof DataBufferByte) { - writePICTData(0, 0, image.getWidth(), image.getHeight(), + writePICTData( + 0, 0, image.getWidth(), image.getHeight(), image.getColorModel(), ((DataBufferByte) buf).getData(), - 0, image.getWidth()); + 0, image.getWidth() + ); } else if (buf instanceof DataBufferInt) { - writePICTData(0, 0, image.getWidth(), image.getHeight(), + writePICTData( + 0, 0, image.getWidth(), image.getHeight(), image.getColorModel(), ((DataBufferInt) buf).getData(), - 0, image.getWidth()); + 0, image.getWidth() + ); } else { throw new IIOException("DataBuffer type " + buf.getDataType() + " not supported"); diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java index d7d073d0..32841877 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/Pattern.java @@ -42,16 +42,16 @@ import java.util.Collections; * @version $Id: Pattern.java,v 1.0 Oct 9, 2007 1:21:38 AM haraldk Exp$ */ abstract class Pattern implements Paint { - private final Paint mPaint; + private final Paint paint; Pattern(final Paint pPaint) { - mPaint = pPaint; + paint = pPaint; } public PaintContext createContext(final ColorModel pModel, final Rectangle pDeviceBounds, final Rectangle2D pUserBounds, final AffineTransform pTransform, final RenderingHints pHints) { - return mPaint.createContext( + return paint.createContext( pModel, pDeviceBounds, pUserBounds, pTransform, pHints != null ? pHints : new RenderingHints(Collections.emptyMap()) @@ -59,6 +59,6 @@ abstract class Pattern implements Paint { } public int getTransparency() { - return mPaint.getTransparency(); + return paint.getTransparency(); } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PenState.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PenState.java index 8fd251f0..83b456d9 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PenState.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PenState.java @@ -38,15 +38,15 @@ import java.awt.*; * @version $Id: PenState.java,v 1.0 Oct 9, 2007 1:56:33 AM haraldk Exp$ */ class PenState { - public final Point mPenLocation; /* pen location */ - public final Dimension mPenSize; /* pen size */ - public final int mPenMode; /* pen's pattern mode */ - public final Pattern mPenPattern; /* pen pattern */ + public final Point penLocation; /* pen location */ + public final Dimension penSize; /* pen size */ + public final int penMode; /* pen's pattern mode */ + public final Pattern penPattern; /* pen pattern */ public PenState(final Point pPenLocation, final int pPenMode, final Pattern pPenPattern, final Dimension pPenSize) { - mPenLocation = pPenLocation; - mPenMode = pPenMode; - mPenPattern = pPenPattern; - mPenSize = pPenSize; + penLocation = pPenLocation; + penMode = pPenMode; + penPattern = pPenPattern; + penSize = pPenSize; } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java index 470a04a0..d2108320 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PixMapPattern.java @@ -38,17 +38,17 @@ import java.awt.*; * @version $Id: PixMapPattern.java,v 1.0 Mar 1, 2009 11:36:10 PM haraldk Exp$ */ final class PixMapPattern extends Pattern { - private final Pattern mFallback; + private final Pattern fallback; PixMapPattern(final Paint pPaint, final Pattern pBitMapFallback) { super(pPaint); - mFallback = pBitMapFallback; + fallback = pBitMapFallback; } /** * @return the fallback B/W pattern */ public Pattern getPattern() { - return mFallback; + return fallback; } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java index c6e0d7dc..0bdf6529 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java @@ -43,7 +43,6 @@ import java.io.*; * @version $Id: QTBMPDecompressor.java,v 1.0 Feb 16, 2009 9:18:28 PM haraldk Exp$ */ final class QTBMPDecompressor extends QTDecompressor { - public boolean canDecompress(final QuickTime.ImageDesc pDescription) { return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor) && "WRLE".equals(pDescription.compressorIdentifer) && "bmp ".equals(idString(pDescription.extraDesc, 4)); diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java index 502276cf..cc06ffd2 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java @@ -40,9 +40,8 @@ import java.io.InputStream; * @version $Id: QTDecompressor.java,v 1.0 Feb 16, 2009 7:21:27 PM haraldk Exp$ */ abstract class QTDecompressor { - /** - * Returns wether this decompressor is capable of decompressing the image + * Returns whether this decompressor is capable of decompressing the image * data described by the given image description. * * @param pDescription the image description ({@code 'idsc'} Atom). diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDraw.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDraw.java index 1c6809a3..757a5328 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDraw.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDraw.java @@ -93,7 +93,7 @@ interface QuickDraw { int SUB_OVER = 38; int AD_MIN = 39; int GRAYISH_TEXT_OR = 49; -// int MASK = 64; // ?! From Käry's code.. +// int MASK = 64; // ?! From Käry's code.. /* * Text face masks. diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java index 92d504d0..e9f52f9d 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickDrawContext.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.imageio.plugins.pict; +import com.twelvemonkeys.lang.Validate; + import java.awt.*; import java.awt.image.BufferedImage; import java.awt.geom.*; @@ -101,9 +103,9 @@ class QuickDrawContext { rgnSave: Handle; {region being saved, used internally} polySave: Handle; {polygon being saved, used internally} */ - private final Graphics2D mGraphics; + private final Graphics2D graphics; - private Pattern mBackground; + private Pattern background; // http://developer.apple.com/documentation/mac/quickdraw/QuickDraw-68.html#HEADING68-0 // Upon the creation of a graphics port, QuickDraw assigns these initial @@ -113,23 +115,20 @@ class QuickDrawContext { // graphics pen. // TODO: Consider creating a Pen/PenState class? - private int mPenVisibility = 0; - private Point2D mPenPosition = new Point(); - private Pattern mPenPattern; - private Dimension2D mPenSize = new Dimension(); - private int mPenMode; + private int penVisibility = 0; + private Point2D penPosition = new Point(); + private Pattern penPattern; + private Dimension2D penSize = new Dimension(); + private int penMode; QuickDrawContext(Graphics2D pGraphics) { - if (pGraphics == null) { - throw new IllegalArgumentException("graphics == null"); - } - mGraphics = pGraphics; + graphics = Validate.notNull(pGraphics, "graphics"); setPenNormal(); } protected void dispose() { - mGraphics.dispose(); + graphics.dispose(); } // ClosePicture @@ -139,7 +138,7 @@ class QuickDrawContext { // ClipRgn public void setClipRegion(Shape pClip) { - mGraphics.setClip(pClip); + graphics.setClip(pClip); } // Font number (sic), integer @@ -160,7 +159,7 @@ class QuickDrawContext { } public void setTextSize(int pSize) { - mGraphics.setFont(mGraphics.getFont().deriveFont((float) pSize)); + graphics.setFont(graphics.getFont().deriveFont((float) pSize)); } // Numerator (Point), denominator (Point) @@ -173,16 +172,16 @@ class QuickDrawContext { // TODO: chExtra added width for nonspace characters public void setOrigin(Point2D pOrigin) { - mGraphics.translate(pOrigin.getX(), pOrigin.getY()); + graphics.translate(pOrigin.getX(), pOrigin.getY()); } public void setForeground(Color pColor) { // TODO: Is this really correct? Or does it depend on pattern mode? - mPenPattern = new BitMapPattern(pColor); + penPattern = new BitMapPattern(pColor); } public void setBackground(Color pColor) { - mBackground = new BitMapPattern(pColor); + background = new BitMapPattern(pColor); } /* @@ -197,14 +196,14 @@ class QuickDrawContext { * HidePen Visibility (decrements visibility by one!) */ public void hidePen() { - mPenVisibility--; + penVisibility--; } /** * ShowPen Visibility (increments visibility by one!) */ public void showPen() { - mPenVisibility++; + penVisibility++; } /** @@ -213,7 +212,7 @@ class QuickDrawContext { * @return {@code true} if pen is visible */ private boolean isPenVisible() { - return mPenVisibility >= 0; + return penVisibility >= 0; } /** @@ -223,7 +222,7 @@ class QuickDrawContext { * @return the current pen position */ public Point2D getPenPosition() { - return (Point2D) mPenPosition.clone(); + return (Point2D) penPosition.clone(); } /** @@ -233,8 +232,8 @@ class QuickDrawContext { * @param pSize the new size */ public void setPenSize(Dimension2D pSize) { - mPenSize.setSize(pSize); - mGraphics.setStroke(getStroke(mPenSize)); + penSize.setSize(pSize); + graphics.setStroke(getStroke(penSize)); } /** @@ -274,7 +273,7 @@ class QuickDrawContext { case QuickDraw.SUB_OVER: case QuickDraw.AD_MIN: case QuickDraw.GRAYISH_TEXT_OR: - mPenMode = pPenMode; + penMode = pPenMode; break; default: @@ -288,7 +287,7 @@ class QuickDrawContext { * @param pPattern the new pattern */ public void setPenPattern(final Pattern pPattern) { - mPenPattern = pPattern; + penPattern = pPattern; } /** @@ -299,7 +298,7 @@ class QuickDrawContext { // TODO: What about visibility? Probably not touch setPenPattern(QuickDraw.BLACK); setPenSize(new Dimension(1, 1)); - mPenMode = QuickDraw.SRC_COPY; + penMode = QuickDraw.SRC_COPY; } /* @@ -308,7 +307,7 @@ class QuickDrawContext { *BackPixPat */ public void setBackgroundPattern(Pattern pPaint) { - mBackground = pPaint; + background = pPaint; } private Composite getCompositeFor(final int pMode) { @@ -354,9 +353,9 @@ class QuickDrawContext { * Sets up context for line drawing/painting. */ protected void setupForPaint() { - mGraphics.setPaint(mPenPattern); - mGraphics.setComposite(getCompositeFor(mPenMode)); - //mGraphics.setStroke(getStroke(mPenSize)); + graphics.setPaint(penPattern); + graphics.setComposite(getCompositeFor(penMode)); + //graphics.setStroke(getStroke(penSize)); } private Stroke getStroke(final Dimension2D pPenSize) { @@ -373,19 +372,19 @@ class QuickDrawContext { * @param pPattern the pattern to use for filling. */ protected void setupForFill(final Pattern pPattern) { - mGraphics.setPaint(pPattern); - mGraphics.setComposite(getCompositeFor(QuickDraw.PAT_COPY)); + graphics.setPaint(pPattern); + graphics.setComposite(getCompositeFor(QuickDraw.PAT_COPY)); } protected void setupForErase() { - mGraphics.setPaint(mBackground); - mGraphics.setComposite(getCompositeFor(QuickDraw.PAT_COPY)); // TODO: Check spec + graphics.setPaint(background); + graphics.setComposite(getCompositeFor(QuickDraw.PAT_COPY)); // TODO: Check spec } protected void setupForInvert() { // TODO: Setup for invert - mGraphics.setColor(Color.BLACK); - mGraphics.setXORMode(Color.WHITE); + graphics.setColor(Color.BLACK); + graphics.setXORMode(Color.WHITE); } /* @@ -398,7 +397,7 @@ class QuickDrawContext { */ public void moveTo(final double pX, final double pY) { - mPenPosition.setLocation(pX, pY); + penPosition.setLocation(pX, pY); } public final void moveTo(final Point2D pPosition) { @@ -406,18 +405,18 @@ class QuickDrawContext { } public final void move(final double pDeltaX, final double pDeltaY) { - moveTo(mPenPosition.getX() + pDeltaX, mPenPosition.getY() + pDeltaY); + moveTo(penPosition.getX() + pDeltaX, penPosition.getY() + pDeltaY); } public void lineTo(final double pX, final double pY) { - Shape line = new Line2D.Double(mPenPosition.getX(), mPenPosition.getY(), pX, pY); + Shape line = new Line2D.Double(penPosition.getX(), penPosition.getY(), pX, pY); // TODO: Add line to current shape if recording... if (isPenVisible()) { // NOTE: Workaround for known Mac JDK bug: Paint, not frame - //mGraphics.setStroke(getStroke(mPenSize)); // Make sure we have correct stroke - paintShape(mGraphics.getStroke().createStrokedShape(line)); + //graphics.setStroke(getStroke(penSize)); // Make sure we have correct stroke + paintShape(graphics.getStroke().createStrokedShape(line)); } @@ -429,7 +428,7 @@ class QuickDrawContext { } public final void line(final double pDeltaX, final double pDeltaY) { - lineTo(mPenPosition.getX() + pDeltaX, mPenPosition.getY() + pDeltaY); + lineTo(penPosition.getX() + pDeltaX, penPosition.getY() + pDeltaY); } /* @@ -813,27 +812,27 @@ class QuickDrawContext { // TODO: All other operations can delegate to these! :-) private void frameShape(final Shape pShape) { setupForPaint(); - mGraphics.draw(pShape); + graphics.draw(pShape); } private void paintShape(final Shape pShape) { setupForPaint(); - mGraphics.fill(pShape); + graphics.fill(pShape); } private void fillShape(final Shape pShape, final Pattern pPattern) { setupForFill(pPattern); - mGraphics.fill(pShape); + graphics.fill(pShape); } private void invertShape(final Shape pShape) { setupForInvert(); - mGraphics.fill(pShape); + graphics.fill(pShape); } private void eraseShape(final Shape pShape) { setupForErase(); - mGraphics.fill(pShape); + graphics.fill(pShape); } /* @@ -862,12 +861,12 @@ class QuickDrawContext { * @param pMaskRgn the mask region */ public void copyBits(BufferedImage pSrcBitmap, Rectangle pSrcRect, Rectangle pDstRect, int pMode, Shape pMaskRgn) { - mGraphics.setComposite(getCompositeFor(pMode)); + graphics.setComposite(getCompositeFor(pMode)); if (pMaskRgn != null) { setClipRegion(pMaskRgn); } - mGraphics.drawImage( + graphics.drawImage( pSrcBitmap, pDstRect.x, pDstRect.y, @@ -927,7 +926,7 @@ class QuickDrawContext { * @param pString a Pascal string (a string of length less than or equal to 255 chars). */ public void drawString(String pString) { - mGraphics.drawString(pString, (float) getPenPosition().getX(), (float) getPenPosition().getY()); + graphics.drawString(pString, (float) getPenPosition().getX(), (float) getPenPosition().getY()); } /* @@ -953,14 +952,14 @@ class QuickDrawContext { // Color Constants -ÝwhiteColor =Ý30; -ÝblackColor = 33 -ÝyellowColor = 69; - magentaColor =Ý137; -ÝredColor =Ý205; -ÝcyanColor =Ý273; -ÝgreenColor =Ý341; -ÝblueColor =Ý409; +�whiteColor =�30; +�blackColor = 33 +�yellowColor = 69; + magentaColor =�137; +�redColor =�205; +�cyanColor =�273; +�greenColor =�341; +�blueColor =�409; */ // TODO: Simplify! Extract to upper level class diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/TestPICTClippingApp.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/TestPICTClippingApp.java index 42197511..d8326023 100644 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/TestPICTClippingApp.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/TestPICTClippingApp.java @@ -33,7 +33,6 @@ import java.util.concurrent.Executors; * @version $Id: TestPICTClippingApp.java,v 1.0 Feb 16, 2009 3:05:16 PM haraldk Exp$ */ public class TestPICTClippingApp { - public static void main(final String[] pArgs) { SwingUtilities.invokeLater(new Runnable() { public void run() { @@ -62,12 +61,12 @@ public class TestPICTClippingApp { } private static class ImageDropHandler extends TransferHandler { - private final JLabel mLabel; - private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + private final JLabel label; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); public ImageDropHandler(JLabel pLabel) { super(null); - mLabel = pLabel; + label = pLabel; } private DataFlavor getSupportedFlavor(final DataFlavor[] transferFlavors) { @@ -126,7 +125,7 @@ public class TestPICTClippingApp { if (readers.hasNext()) { final ImageReader imageReader = readers.next(); - mExecutor.execute(new Runnable() { + executor.execute(new Runnable() { public void run() { try { readAndInstallImage(stream, imageReader); @@ -186,7 +185,7 @@ public class TestPICTClippingApp { System.out.print("Scaling image... "); BufferedImage scaled = box(image, maxDimension); System.out.printf("Done (%dx%d).%n", scaled.getWidth(), scaled.getHeight()); - mLabel.setIcon(new BufferedImageIcon(scaled)); + label.setIcon(new BufferedImageIcon(scaled)); } }); } diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTestCase.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java similarity index 88% rename from imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTestCase.java rename to imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java index 8a02dea1..d85e6fb3 100644 --- a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTestCase.java +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderTest.java @@ -1,6 +1,7 @@ package com.twelvemonkeys.imageio.plugins.pict; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import org.junit.Test; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; @@ -8,6 +9,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import static org.junit.Assert.*; + /** * ICOImageReaderTestCase * @@ -15,7 +18,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: ICOImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ */ -public class PICTImageReaderTestCase extends ImageReaderAbstractTestCase { +public class PICTImageReaderTest extends ImageReaderAbstractTestCase { static ImageReaderSpi sProvider = new PICTImageReaderSpi(); @@ -59,11 +62,14 @@ public class PICTImageReaderTestCase extends ImageReaderAbstractTestCaseHarald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PICTImageWriterTest.java,v 1.0 20.01.12 12:26 haraldk Exp$ + */ +public class PICTImageWriterTest extends ImageWriterAbstractTestCase { + private final PICTImageWriterSpi provider = new PICTImageWriterSpi(); + + @Override + protected ImageWriter createImageWriter() { + return new PICTImageWriter(provider); + } + + @Override + protected List getTestData() { + return Arrays.asList( + new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), + new BufferedImage(32, 20, BufferedImage.TYPE_INT_BGR), + new BufferedImage(32, 20, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(30, 20, BufferedImage.TYPE_3BYTE_BGR), + new BufferedImage(30, 20, BufferedImage.TYPE_4BYTE_ABGR), + new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_INDEXED), + new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_GRAY) +// new BufferedImage(32, 20, BufferedImage.TYPE_BYTE_BINARY) // Packed does not work + ); + } + + @Test + public void testWriteReadCompare() throws IOException { + ImageWriter writer = createImageWriter(); + + List testData = getTestData(); + for (int i = 0; i < testData.size(); i++) { + RenderedImage image = testData.get(i); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); + writer.setOutput(stream); + + BufferedImage original = drawSomething((BufferedImage) image); + + try { + writer.write(original); + } + catch (IOException e) { + fail(e.getMessage()); + } + finally { + stream.close(); // Force data to be written + } + + assertTrue("No image data written", buffer.size() > 0); + + ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray())); + BufferedImage written = ImageIO.read(input); + + assertNotNull(written); + assertEquals(original.getWidth(), written.getWidth()); + assertEquals(original.getHeight(), written.getHeight()); + + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + int originalRGB = original.getRGB(x, y); + int writtenRGB = written.getRGB(x, y); + + if (original.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) { + // NOTE: For some reason, gray data seems to be one step off... + assertEquals("Test data " + i + " R(" + x + "," + y + ")", originalRGB & 0xff0000, writtenRGB & 0xff0000, 0x10000); + assertEquals("Test data " + i + " G(" + x + "," + y + ")", originalRGB & 0x00ff00, writtenRGB & 0x00ff00, 0x100); + assertEquals("Test data " + i + " B(" + x + "," + y + ")", originalRGB & 0x0000ff, writtenRGB & 0x0000ff, 0x1); + } + else { + assertEquals("Test data " + i + " R(" + x + "," + y + ")", originalRGB & 0xff0000, writtenRGB & 0xff0000); + assertEquals("Test data " + i + " G(" + x + "," + y + ")", originalRGB & 0x00ff00, writtenRGB & 0x00ff00); + assertEquals("Test data " + i + " B(" + x + "," + y + ")", originalRGB & 0x0000ff, writtenRGB & 0x0000ff); + } + } + } + } + } +} diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java index 88474984..83f7ca32 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java @@ -43,7 +43,7 @@ import java.io.InputStream; * @version $Id: ICCProfile.java,v 1.0 May 20, 2008 6:24:10 PM haraldk Exp$ */ class ICCProfile extends PSDImageResource { - private ICC_Profile mProfile; + private ICC_Profile profile; ICCProfile(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -51,9 +51,9 @@ class ICCProfile extends PSDImageResource { @Override protected void readData(ImageInputStream pInput) throws IOException { - InputStream stream = IIOUtil.createStreamAdapter(pInput, mSize); + InputStream stream = IIOUtil.createStreamAdapter(pInput, size); try { - mProfile = ICC_Profile.getInstance(stream); + profile = ICC_Profile.getInstance(stream); } finally { // Make sure stream has correct position after read @@ -62,17 +62,17 @@ class ICCProfile extends PSDImageResource { } public ICC_Profile getProfile() { - return mProfile; + // Warning: Mutable... + return profile; } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", profile: ").append(mProfile); + builder.append(", profile: ").append(profile); builder.append("]"); return builder.toString(); } - } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java index 9918f429..64067411 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java @@ -41,7 +41,7 @@ import java.util.List; * @version $Id: PSDAlphaChannelInfo.java,v 1.0 May 2, 2008 5:33:40 PM haraldk Exp$ */ class PSDAlphaChannelInfo extends PSDImageResource { - List mNames; + List names; public PSDAlphaChannelInfo(short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -49,12 +49,12 @@ class PSDAlphaChannelInfo extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mNames = new ArrayList(); + names = new ArrayList(); - long left = mSize; + long left = size; while (left > 0) { String name = PSDUtil.readPascalString(pInput); - mNames.add(name); + names.add(name); left -= name.length() + 1; } } @@ -62,7 +62,7 @@ class PSDAlphaChannelInfo extends PSDImageResource { @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", alpha channels: ").append(mNames).append("]"); + builder.append(", alpha channels: ").append(names).append("]"); return builder.toString(); } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelInfo.java index 76beec91..603c3cf5 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelInfo.java @@ -36,8 +36,8 @@ package com.twelvemonkeys.imageio.plugins.psd; * @version $Id: PSDChannelInfo.java,v 1.0 May 6, 2008 2:46:23 PM haraldk Exp$ */ class PSDChannelInfo { - final short mChannelId; - final long mLength; + final short channelId; + final long length; // typedef struct _CLI // { @@ -45,16 +45,16 @@ class PSDChannelInfo { // LONG LengthOfChannelData; /* Channel Length Info field two */ // } CLI; public PSDChannelInfo(short pChannelId, long pLength) { - mChannelId = pChannelId; - mLength = pLength; + channelId = pChannelId; + length = pLength; } @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("["); - builder.append("channelId: ").append(mChannelId); - builder.append(", length: ").append(mLength); + builder.append("channelId: ").append(channelId); + builder.append(", length: ").append(length); builder.append("]"); return builder.toString(); } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelSourceDestinationRange.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelSourceDestinationRange.java index 65735285..b58574ac 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelSourceDestinationRange.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDChannelSourceDestinationRange.java @@ -39,29 +39,29 @@ import java.io.IOException; * @version $Id: PSDChannelSourceDestinationRange.java,v 1.0 May 6, 2008 5:14:13 PM haraldk Exp$ */ class PSDChannelSourceDestinationRange { - private String mChannel; - private short mSourceBlack; - private short mSourceWhite; - private short mDestBlack; - private short mDestWhite; + private String channel; + private short sourceBlack; + private short sourceWhite; + private short destBlack; + private short destWhite; public PSDChannelSourceDestinationRange(ImageInputStream pInput, String pChannel) throws IOException { - mChannel = pChannel; - mSourceBlack = pInput.readShort(); - mSourceWhite = pInput.readShort(); - mDestBlack = pInput.readShort(); - mDestWhite = pInput.readShort(); + channel = pChannel; + sourceBlack = pInput.readShort(); + sourceWhite = pInput.readShort(); + destBlack = pInput.readShort(); + destWhite = pInput.readShort(); } @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); - builder.append("[(").append(mChannel); - builder.append("): sourceBlack: ").append(Integer.toHexString(mSourceBlack & 0xffff)); - builder.append(", sourceWhite: ").append(Integer.toHexString(mSourceWhite & 0xffff)); - builder.append(", destBlack: ").append(Integer.toHexString(mDestBlack & 0xffff)); - builder.append(", destWhite: ").append(Integer.toHexString(mDestWhite & 0xffff)); + builder.append("[(").append(channel); + builder.append("): sourceBlack: ").append(Integer.toHexString(sourceBlack & 0xffff)); + builder.append(", sourceWhite: ").append(Integer.toHexString(sourceWhite & 0xffff)); + builder.append(", destBlack: ").append(Integer.toHexString(destBlack & 0xffff)); + builder.append(", destWhite: ").append(Integer.toHexString(destWhite & 0xffff)); builder.append("]"); return builder.toString(); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java index d8e67cde..3b120af8 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDColorData.java @@ -44,8 +44,8 @@ import java.io.IOException; * @version $Id: PSDColorData.java,v 1.0 Apr 29, 2008 5:33:01 PM haraldk Exp$ */ class PSDColorData { - final byte[] mColors; - private IndexColorModel mColorModel; + final byte[] colors; + private IndexColorModel colorModel; PSDColorData(final ImageInputStream pInput) throws IOException { int length = pInput.readInt(); @@ -57,19 +57,19 @@ class PSDColorData { } // NOTE: Spec says length may only be 768 bytes (256 RGB triplets) - mColors = new byte[length]; - pInput.readFully(mColors); + colors = new byte[length]; + pInput.readFully(colors); // NOTE: Could be a padding byte here, if not even.. } IndexColorModel getIndexColorModel() { - if (mColorModel == null) { - int[] rgb = toInterleavedRGB(mColors); - mColorModel = new InverseColorMapIndexColorModel(8, rgb.length, rgb, 0, false, -1, DataBuffer.TYPE_BYTE); + if (colorModel == null) { + int[] rgb = toInterleavedRGB(colors); + colorModel = new InverseColorMapIndexColorModel(8, rgb.length, rgb, 0, false, -1, DataBuffer.TYPE_BYTE); } - return mColorModel; + return colorModel; } private static int[] toInterleavedRGB(final byte[] pColors) { diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDisplayInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDisplayInfo.java index f74e5a7b..037f2a9a 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDisplayInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDDisplayInfo.java @@ -65,10 +65,10 @@ class PSDDisplayInfo extends PSDImageResource { // BYTE Padding; /* Always zero */ //} DISPLAYINFO; - int mColorSpace; - short[] mColors; - short mOpacity; - byte mKind; + int colorSpace; + short[] colors; + short opacity; + byte kind; PSDDisplayInfo(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -76,46 +76,46 @@ class PSDDisplayInfo extends PSDImageResource { @Override protected void readData(ImageInputStream pInput) throws IOException { - if (mSize % 14 != 0) { - throw new IIOException("Display info length expected to be mod 14: " + mSize); + if (size % 14 != 0) { + throw new IIOException("Display info length expected to be mod 14: " + size); } -// long left = mSize; +// long left = size; // while (left > 0) { - mColorSpace = pInput.readShort(); + colorSpace = pInput.readShort(); // Color[4]...? - mColors = new short[4]; - mColors[0] = pInput.readShort(); - mColors[1] = pInput.readShort(); - mColors[2] = pInput.readShort(); - mColors[3] = pInput.readShort(); + colors = new short[4]; + colors[0] = pInput.readShort(); + colors[1] = pInput.readShort(); + colors[2] = pInput.readShort(); + colors[3] = pInput.readShort(); - mOpacity = pInput.readShort(); + opacity = pInput.readShort(); - mKind = pInput.readByte(); + kind = pInput.readByte(); pInput.readByte(); // Pad // left -= 14; // } - pInput.skipBytes(mSize - 14); + pInput.skipBytes(size - 14); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", ColorSpace: ").append(mColorSpace); + builder.append(", ColorSpace: ").append(colorSpace); builder.append(", Colors: {"); - builder.append(mColors[0]); + builder.append(colors[0]); builder.append(", "); - builder.append(mColors[1]); + builder.append(colors[1]); builder.append(", "); - builder.append(mColors[2]); + builder.append(colors[2]); builder.append(", "); - builder.append(mColors[3]); - builder.append("}, Opacity: ").append(mOpacity); - builder.append(", Kind: ").append(kind(mKind)); + builder.append(colors[3]); + builder.append("}, Opacity: ").append(opacity); + builder.append(", Kind: ").append(kind(kind)); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java index 877bc34d..f765c22e 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java @@ -18,7 +18,7 @@ import java.io.IOException; * @see Adobe TIFF developer resources */ final class PSDEXIF1Data extends PSDImageResource { - protected Directory mDirectory; + protected Directory directory; PSDEXIF1Data(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -28,13 +28,13 @@ final class PSDEXIF1Data extends PSDImageResource { protected void readData(final ImageInputStream pInput) throws IOException { // This is in essence an embedded TIFF file. // TODO: Instead, read the byte data, store for later parsing (or better yet, store offset, and read on request) - mDirectory = new EXIFReader().read(pInput); + directory = new EXIFReader().read(pInput); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", ").append(mDirectory); + builder.append(", ").append(directory); builder.append("]"); return builder.toString(); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java index 4f336eb1..0c75b981 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java @@ -39,25 +39,25 @@ import java.io.IOException; * @version $Id: PSDGlobalLayerMask.java,v 1.0 May 8, 2008 5:33:48 PM haraldk Exp$ */ class PSDGlobalLayerMask { - final int mColorSpace; - final int mColor1; - final int mColor2; - final int mColor3; - final int mColor4; - final int mOpacity; - final int mKind; + final int colorSpace; + final int color1; + final int color2; + final int color3; + final int color4; + final int opacity; + final int kind; PSDGlobalLayerMask(final ImageInputStream pInput) throws IOException { - mColorSpace = pInput.readUnsignedShort(); // Undocumented + colorSpace = pInput.readUnsignedShort(); // Undocumented - mColor1 = pInput.readUnsignedShort(); - mColor2 = pInput.readUnsignedShort(); - mColor3 = pInput.readUnsignedShort(); - mColor4 = pInput.readUnsignedShort(); + color1 = pInput.readUnsignedShort(); + color2 = pInput.readUnsignedShort(); + color3 = pInput.readUnsignedShort(); + color4 = pInput.readUnsignedShort(); - mOpacity = pInput.readUnsignedShort(); // 0-100 + opacity = pInput.readUnsignedShort(); // 0-100 - mKind = pInput.readUnsignedByte(); // 0: Selected (ie inverted), 1: Color protected, 128: Use value stored per layer + kind = pInput.readUnsignedByte(); // 0: Selected (ie inverted), 1: Color protected, 128: Use value stored per layer // TODO: Variable: Filler zeros @@ -68,13 +68,13 @@ class PSDGlobalLayerMask { public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("["); - builder.append("color space: 0x").append(Integer.toHexString(mColorSpace)); - builder.append(", colors: [0x").append(Integer.toHexString(mColor1)); - builder.append(", 0x").append(Integer.toHexString(mColor2)); - builder.append(", 0x").append(Integer.toHexString(mColor3)); - builder.append(", 0x").append(Integer.toHexString(mColor4)); - builder.append("], opacity: ").append(mOpacity); - builder.append(", kind: ").append(mKind); + builder.append("color space: 0x").append(Integer.toHexString(colorSpace)); + builder.append(", colors: [0x").append(Integer.toHexString(color1)); + builder.append(", 0x").append(Integer.toHexString(color2)); + builder.append(", 0x").append(Integer.toHexString(color3)); + builder.append(", 0x").append(Integer.toHexString(color4)); + builder.append("], opacity: ").append(opacity); + builder.append(", kind: ").append(kind); builder.append("]"); return builder.toString(); } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGridAndGuideInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGridAndGuideInfo.java index 2b4e0b14..96e47616 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGridAndGuideInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGridAndGuideInfo.java @@ -25,12 +25,12 @@ final class PSDGridAndGuideInfo extends PSDImageResource { // gchar fDirection; /* Guide orientation */ //} GuideResource; - int mVersion; - int mGridCycleVertical; - int mGridCycleHorizontal; - int mGuideCount; + int version; + int gridCycleVertical; + int gridCycleHorizontal; + int guideCount; - GuideResource[] mGuides; + GuideResource[] guides; PSDGridAndGuideInfo(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -38,21 +38,22 @@ final class PSDGridAndGuideInfo extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mVersion = pInput.readInt(); - mGridCycleVertical = pInput.readInt(); - mGridCycleHorizontal = pInput.readInt(); - mGuideCount = pInput.readInt(); + version = pInput.readInt(); + gridCycleVertical = pInput.readInt(); + gridCycleHorizontal = pInput.readInt(); + guideCount = pInput.readInt(); - mGuides = new GuideResource[mGuideCount]; + guides = new GuideResource[guideCount]; - for (GuideResource guide : mGuides) { - guide.mLocation = pInput.readInt(); - guide.mDirection = pInput.readByte(); + for (int i = 0; i < guides.length; i++) { + guides[i] = new GuideResource(); + guides[i].location = pInput.readInt(); + guides[i].direction = pInput.readByte(); } } static class GuideResource { - int mLocation; - byte mDirection; // 0: vertical, 1: horizontal + int location; + byte direction; // 0: vertical, 1: horizontal } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java index 77e6659a..b12e594a 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java @@ -28,9 +28,6 @@ package com.twelvemonkeys.imageio.plugins.psd; -import org.w3c.dom.Node; - -import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.ImageInputStream; import javax.imageio.IIOException; import java.io.IOException; @@ -58,11 +55,11 @@ class PSDHeader { // WORD Mode; /* Color mode */ // } PSD_HEADER; - final short mChannels; - final int mWidth; - final int mHeight; - final short mBits; - final short mMode; + final short channels; + final int width; + final int height; + final short bits; + final short mode; PSDHeader(final ImageInputStream pInput) throws IOException { int signature = pInput.readInt(); @@ -84,27 +81,27 @@ class PSDHeader { byte[] reserved = new byte[6]; pInput.readFully(reserved); - mChannels = pInput.readShort(); - mHeight = pInput.readInt(); // Rows - mWidth = pInput.readInt(); // Columns - mBits = pInput.readShort(); - mMode = pInput.readShort(); + channels = pInput.readShort(); + height = pInput.readInt(); // Rows + width = pInput.readInt(); // Columns + bits = pInput.readShort(); + mode = pInput.readShort(); } @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("[Channels: "); - builder.append(mChannels); + builder.append(channels); builder.append(", width: "); - builder.append(mWidth); + builder.append(width); builder.append(", height: "); - builder.append(mHeight); + builder.append(height); builder.append(", depth: "); - builder.append(mBits); + builder.append(bits); builder.append(", mode: "); - builder.append(mMode); - switch (mMode) { + builder.append(mode); + switch (mode) { case PSD.COLOR_MODE_MONOCHROME: builder.append(" (Monochrome)"); break; diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java index 59a076e9..066d04cf 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java @@ -14,7 +14,7 @@ import java.io.IOException; * @version $Id: PSDIPTCData.java,v 1.0 Nov 7, 2009 9:52:14 PM haraldk Exp$ */ final class PSDIPTCData extends PSDImageResource { - Directory mDirectory; + Directory directory; PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -23,13 +23,13 @@ final class PSDIPTCData extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { // Read IPTC directory - mDirectory = new IPTCReader().read(pInput); + directory = new IPTCReader().read(pInput); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", ").append(mDirectory); + builder.append(", ").append(directory); builder.append("]"); return builder.toString(); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java index 6adb8e65..c3a588b3 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.color.ColorSpaces; import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; import com.twelvemonkeys.xml.XMLSerializer; import org.w3c.dom.Node; @@ -62,192 +63,258 @@ import java.util.List; * @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$ */ // TODO: Implement ImageIO meta data interface -// TODO: Allow reading the extra alpha channels (index after composite data) +// TODO: Figure out of we should assume Adobe RGB (1998) color model, if no embedded profile? // TODO: Support for PSDVersionInfo hasRealMergedData=false (no real composite data, layers will be in index 0) -// TODO: Support for API for reading separate layers (index after composite data, and optional alpha channels) // TODO: Consider Romain Guy's Java 2D implementation of PS filters for the blending modes in layers // http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/ // See http://www.codeproject.com/KB/graphics/PSDParser.aspx -// See http://www.adobeforums.com/webx?14@@.3bc381dc/0 +// See http://www.adobeforums.com/webx?14@@.3bc381dc/0 +// Done: Allow reading the extra alpha channels (index after composite data) public class PSDImageReader extends ImageReaderBase { - private PSDHeader mHeader; -// private PSDColorData mColorData; -// private List mImageResources; -// private PSDGlobalLayerMask mGlobalLayerMask; -// private List mLayerInfo; - private ICC_ColorSpace mColorSpace; - protected PSDMetadata mMetadata; + private PSDHeader header; + private ICC_ColorSpace colorSpace; + protected PSDMetadata metadata; - protected PSDImageReader(final ImageReaderSpi pOriginatingProvider) { - super(pOriginatingProvider); + protected PSDImageReader(final ImageReaderSpi originatingProvider) { + super(originatingProvider); } protected void resetMembers() { - mHeader = null; -// mColorData = null; -// mImageResources = null; - mMetadata = null; - mColorSpace = null; + header = null; + metadata = null; + colorSpace = null; } - public int getWidth(final int pIndex) throws IOException { - checkBounds(pIndex); + public int getWidth(final int imageIndex) throws IOException { + checkBounds(imageIndex); readHeader(); - return mHeader.mWidth; + + if (imageIndex > 0) { + return getLayerWidth(imageIndex - 1); + } + + return header.width; } - public int getHeight(final int pIndex) throws IOException { - checkBounds(pIndex); + public int getHeight(final int imageIndex) throws IOException { + checkBounds(imageIndex); readHeader(); - return mHeader.mHeight; + + if (imageIndex > 0) { + return getLayerHeight(imageIndex - 1); + } + + return header.height; + } + + private int getLayerWidth(int layerIndex) throws IOException { + if (metadata == null || metadata.layerInfo == null) { + readImageResources(false); + readLayerAndMaskInfo(true); + } + + PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex); + + return layerInfo.right - layerInfo.left; + } + + private int getLayerHeight(int layerIndex) throws IOException { + if (metadata == null || metadata.layerInfo == null) { + readImageResources(false); + readLayerAndMaskInfo(true); + } + + PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex); + + return layerInfo.bottom - layerInfo.top; } @Override - public ImageTypeSpecifier getRawImageType(final int pIndex) throws IOException { - return getRawImageTypeInternal(pIndex); + public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException { + return getRawImageTypeInternal(imageIndex); } - private ImageTypeSpecifier getRawImageTypeInternal(final int pIndex) throws IOException { - checkBounds(pIndex); + private ImageTypeSpecifier getRawImageTypeInternal(final int imageIndex) throws IOException { + checkBounds(imageIndex); readHeader(); - ColorSpace cs; + // Image index above 0, means a layer + if (imageIndex > 0) { + if (metadata == null || metadata.layerInfo == null) { + readImageResources(false); + readLayerAndMaskInfo(true); + } - switch (mHeader.mMode) { + return getRawImageTypeForLayer(imageIndex - 1); + } + + // Otherwise, get the type specifier for the composite layer + return getRawImageTypeForCompositeLayer(); + } + + private ImageTypeSpecifier getRawImageTypeForCompositeLayer() throws IOException { + ColorSpace cs; + switch (header.mode) { case PSD.COLOR_MODE_MONOCHROME: - if (mHeader.mChannels == 1 && mHeader.mBits == 1) { + if (header.channels == 1 && header.bits == 1) { return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY); } throw new IIOException( - String.format("Unsupported channel count/bit depth for Monochrome PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits) + String.format("Unsupported channel count/bit depth for Monochrome PSD: %d channels/%d bits", header.channels, header.bits) ); case PSD.COLOR_MODE_INDEXED: // TODO: 16 bit indexed?! Does it exist? - if (mHeader.mChannels == 1 && mHeader.mBits == 8) { - return IndexedImageTypeSpecifier.createFromIndexColorModel(mMetadata.mColorData.getIndexColorModel()); + if (header.channels == 1 && header.bits == 8) { + return IndexedImageTypeSpecifier.createFromIndexColorModel(metadata.colorData.getIndexColorModel()); } throw new IIOException( - String.format("Unsupported channel count/bit depth for Indexed Color PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits) + String.format("Unsupported channel count/bit depth for Indexed Color PSD: %d channels/%d bits", header.channels, header.bits) ); case PSD.COLOR_MODE_DUOTONE: // NOTE: Duotone (whatever that is) should be treated as gray scale // Fall-through case PSD.COLOR_MODE_GRAYSCALE: - if (mHeader.mChannels == 1 && mHeader.mBits == 8) { + if (header.channels == 1 && header.bits == 8) { return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY); } - else if (mHeader.mChannels == 1 && mHeader.mBits == 16) { + else if (header.channels == 1 && header.bits == 16) { return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY); } throw new IIOException( - String.format("Unsupported channel count/bit depth for Gray Scale PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits) + String.format("Unsupported channel count/bit depth for Gray Scale PSD: %d channels/%d bits", header.channels, header.bits) ); case PSD.COLOR_MODE_RGB: cs = getEmbeddedColorSpace(); if (cs == null) { - cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + // TODO: Should probably be Adobe RGB (1998), not sRGB. Or..? Can't find any spec saying either... + cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); // ColorSpaces.getColorSpace(ColorSpaces.CS_ADOBE_RGB_1998); ? } - if (mHeader.mChannels == 3 && mHeader.mBits == 8) { + if (header.channels == 3 && header.bits == 8) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_BYTE, false, false); } - else if (mHeader.mChannels >= 4 && mHeader.mBits == 8) { + else if (header.channels >= 4 && header.bits == 8) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false); } - else if (mHeader.mChannels == 3 && mHeader.mBits == 16) { + else if (header.channels == 3 && header.bits == 16) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_USHORT, false, false); } - else if (mHeader.mChannels >= 4 && mHeader.mBits == 16) { + else if (header.channels >= 4 && header.bits == 16) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false); } throw new IIOException( - String.format("Unsupported channel count/bit depth for RGB PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits) + String.format("Unsupported channel count/bit depth for RGB PSD: %d channels/%d bits", header.channels, header.bits) ); case PSD.COLOR_MODE_CMYK: cs = getEmbeddedColorSpace(); if (cs == null) { - cs = CMYKColorSpace.getInstance(); + cs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); } - if (mHeader.mChannels == 4 && mHeader.mBits == 8) { + if (header.channels == 4 && header.bits == 8) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, false, false); } - else if (mHeader.mChannels == 5 && mHeader.mBits == 8) { + else if (header.channels == 5 && header.bits == 8) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false); } - else if (mHeader.mChannels == 4 && mHeader.mBits == 16) { + else if (header.channels == 4 && header.bits == 16) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, false, false); } - else if (mHeader.mChannels == 5 && mHeader.mBits == 16) { + else if (header.channels == 5 && header.bits == 16) { return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false); } throw new IIOException( - String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits) + String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", header.channels, header.bits) ); case PSD.COLOR_MODE_MULTICHANNEL: // TODO: Implement case PSD.COLOR_MODE_LAB: // TODO: Implement + // TODO: If there's a color profile embedded, it should be easy, otherwise we're out of luck... default: throw new IIOException( - String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", mHeader.mMode, mHeader.mChannels, mHeader.mBits) + String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", header.mode, header.channels, header.bits) ); } } - public Iterator getImageTypes(final int pIndex) throws IOException { + public Iterator getImageTypes(final int imageIndex) throws IOException { // TODO: Check out the custom ImageTypeIterator and ImageTypeProducer used in the Sun provided JPEGImageReader // Could use similar concept to create lazily-created ImageTypeSpecifiers (util candidate, based on FilterIterator?) // Get the raw type. Will fail for unsupported types - ImageTypeSpecifier rawType = getRawImageTypeInternal(pIndex); + ImageTypeSpecifier rawType = getRawImageTypeInternal(imageIndex); ColorSpace cs = rawType.getColorModel().getColorSpace(); List types = new ArrayList(); - switch (mHeader.mMode) { + switch (header.mode) { case PSD.COLOR_MODE_RGB: // Prefer interleaved versions as they are much faster to display - if (mHeader.mChannels == 3 && mHeader.mBits == 8) { - // Basically same as BufferedImage.TYPE_3BYTE_BGR - types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); +// if (header.channels == 3 && header.bits == 8) { + if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) { + // TODO: ColorConvertOp to CS_sRGB + // TODO: Integer raster + // types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_RGB)); + types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); + + if (!cs.isCS_sRGB()) { + // Basically BufferedImage.TYPE_3BYTE_BGR, with corrected ColorSpace. Possibly slow. + types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); + } } - else if (mHeader.mChannels >= 4 && mHeader.mBits == 8) { - // Basically same as BufferedImage.TYPE_4BYTE_ABGR - types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false)); +// else if (header.channels >= 4 && header.bits == 8) { + else if (rawType.getNumBands() >= 4 && rawType.getBitsPerBand(0) == 8) { + // TODO: ColorConvertOp to CS_sRGB + // TODO: Integer raster + // types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_ARGB)); + types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR)); +// + if (!cs.isCS_sRGB()) { + // Basically BufferedImage.TYPE_4BYTE_ABGR, with corrected ColorSpace. Possibly slow. + types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false)); + } } - else if (mHeader.mChannels == 3 && mHeader.mBits == 16) { +// else if (header.channels == 3 && header.bits == 16) { + else if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 16) { types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_USHORT, false, false)); } - else if (mHeader.mChannels >= 4 && mHeader.mBits == 16) { +// else if (header.channels >= 4 && header.bits == 16) { + else if (rawType.getNumBands() >= 4 && rawType.getBitsPerBand(0) == 16) { types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false)); } break; case PSD.COLOR_MODE_CMYK: // Prefer interleaved versions as they are much faster to display + // TODO: ColorConvertOp to CS_sRGB // TODO: We should convert these to their RGB equivalents while reading for the common-case, // as Java2D is extremely slow displaying custom images. // Converting to RGB is also correct behaviour, according to the docs. - if (mHeader.mChannels == 4 && mHeader.mBits == 8) { + // Doing this, will require rewriting the image reading, as the raw image data is channelled, not interleaved :-/ +// if (header.channels == 4 && header.bits == 8) { + if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 8) { types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); } - else if (mHeader.mChannels == 5 && mHeader.mBits == 8) { +// else if (header.channels == 5 && header.bits == 8) { + else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 8) { types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false)); } - else if (mHeader.mChannels == 4 && mHeader.mBits == 16) { +// else if (header.channels == 4 && header.bits == 16) { + else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 16) { types.add(ImageTypeSpecifier.createInterleaved(cs, new int[]{3, 2, 1, 0}, DataBuffer.TYPE_USHORT, false, false)); } - else if (mHeader.mChannels == 5 && mHeader.mBits == 16) { +// else if (header.channels == 5 && header.bits == 16) { + else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 16) { types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false)); } break; @@ -255,7 +322,7 @@ public class PSDImageReader extends ImageReaderBase { // Just stick to the raw type } - // Finally add the + // Finally add the raw type types.add(rawType); return types.iterator(); @@ -266,36 +333,55 @@ public class PSDImageReader extends ImageReaderBase { // TODO: Skip this, requires storing some stream offsets readLayerAndMaskInfo(false); - if (mColorSpace == null) { + if (colorSpace == null) { ICC_Profile profile = null; - for (PSDImageResource resource : mMetadata.mImageResources) { + for (PSDImageResource resource : metadata.imageResources) { if (resource instanceof ICCProfile) { profile = ((ICCProfile) resource).getProfile(); break; } } - mColorSpace = profile == null ? null : new ICC_ColorSpace(profile); + colorSpace = profile == null ? null : ColorSpaces.createColorSpace(profile); } - return mColorSpace; + return colorSpace; } - public BufferedImage read(final int pIndex, final ImageReadParam pParam) throws IOException { - checkBounds(pIndex); + public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + checkBounds(imageIndex); readHeader(); readImageResources(false); - readLayerAndMaskInfo(false); +// readLayerAndMaskInfo(false); + readLayerAndMaskInfo(imageIndex > 0); - BufferedImage image = getDestination(pParam, getImageTypes(pIndex), mHeader.mWidth, mHeader.mHeight); - ImageTypeSpecifier rawType = getRawImageType(pIndex); - checkReadParamBandSettings(pParam, rawType.getNumBands(), image.getSampleModel().getNumBands()); + // TODO: What about the extra alpha channels possibly present? Read as gray scale as extra images? + + // Layer hacks... For now, any index above 0 is considered to be a layer... + // TODO: Support layer in index 0, if "has real merged data" flag is false? + // TODO: Param support in layer code (more duping/cleanup..) + if (imageIndex > 0) { +// ImageTypeSpecifier compositeType = getRawImageTypeForCompositeLayer(); +// ImageTypeSpecifier imageType = getImageTypes(0).next(); + +// int layerIndex = imageIndex - 1; + +// PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex); +// +// imageInput.seek(findLayerStartPos(layerIndex)); +// return readLayerData(layerIndex, layerInfo, compositeType, imageType, param); + return readLayerData(imageIndex - 1, param); + } + + BufferedImage image = getDestination(param, getImageTypes(imageIndex), header.width, header.height); + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + checkReadParamBandSettings(param, rawType.getNumBands(), image.getSampleModel().getNumBands()); final Rectangle source = new Rectangle(); final Rectangle dest = new Rectangle(); - computeRegions(pParam, mHeader.mWidth, mHeader.mHeight, image, source, dest); + computeRegions(param, header.width, header.height, image, source, dest); /* NOTE: It seems safe to just leave this out for now. The only thing we need is to support sub sampling. @@ -307,14 +393,16 @@ public class PSDImageReader extends ImageReaderBase { // Otherwise, copy "through" ColorModel? // Copy pixels from temp raster // If possible, leave the destination image "untouched" (accelerated) + // See Jim Grahams comments: + // http://forums.java.net/jive/message.jspa?messageID=295758#295758 // TODO: Doing a per line color convert will be expensive, as data is channelled... // Will need to either convert entire image, or skip back/forth between channels... // TODO: Banding... - ImageTypeSpecifier spec = getRawImageType(pIndex); - BufferedImage temp = spec.createBufferedImage(getWidth(pIndex), 1); + ImageTypeSpecifier spec = getRawImageType(imageIndex); + BufferedImage temp = spec.createBufferedImage(getWidth(imageIndex), 1); temp.getRaster(); if (...) @@ -325,37 +413,37 @@ public class PSDImageReader extends ImageReaderBase { final int xSub; final int ySub; - if (pParam == null) { + if (param == null) { xSub = ySub = 1; } else { - xSub = pParam.getSourceXSubsampling(); - ySub = pParam.getSourceYSubsampling(); + xSub = param.getSourceXSubsampling(); + ySub = param.getSourceYSubsampling(); } - processImageStarted(pIndex); + processImageStarted(imageIndex); int[] byteCounts = null; - int compression = mImageInput.readShort(); + int compression = imageInput.readShort(); // TODO: Need to make sure compression is set in metadata, even without reading the image data! - mMetadata.mCompression = compression; + metadata.compression = compression; switch (compression) { case PSD.COMPRESSION_NONE: break; case PSD.COMPRESSION_RLE: // NOTE: Byte counts will allow us to easily skip rows before AOI - byteCounts = new int[mHeader.mChannels * mHeader.mHeight]; + byteCounts = new int[header.channels * header.height]; for (int i = 0; i < byteCounts.length; i++) { - byteCounts[i] = mImageInput.readUnsignedShort(); + byteCounts[i] = imageInput.readUnsignedShort(); } break; case PSD.COMPRESSION_ZIP: // TODO: Could probably use the ZIPDecoder (DeflateDecoder) here.. case PSD.COMPRESSION_ZIP_PREDICTION: - // TODO: Need to find out if the normal java.util.zip can handle this... + // TODO: Look at TIFF prediction reading // Could be same as PNG prediction? Read up... - throw new IIOException("ZIP compression not supported yet"); + throw new IIOException("PSD with ZIP compression not supported"); default: throw new IIOException( String.format( @@ -378,6 +466,20 @@ public class PSDImageReader extends ImageReaderBase { return image; } + private long findLayerStartPos(int layerIndex) { + long layersStart = metadata.layersStart; + + for (int i = 0; i < layerIndex; i++) { + PSDLayerInfo layerInfo = metadata.layerInfo.get(i); + + for (PSDChannelInfo channelInfo : layerInfo.channelInfo) { + layersStart += channelInfo.length; + } + } + + return layersStart; + } + private void readImageData(final BufferedImage pImage, final ColorModel pSourceCM, final Rectangle pSource, final Rectangle pDest, final int pXSub, final int pYSub, @@ -388,7 +490,7 @@ public class PSDImageReader extends ImageReaderBase { final ColorModel destCM = pImage.getColorModel(); // TODO: This raster is 3-5 times longer than needed, depending on number of channels... - final WritableRaster rowRaster = pSourceCM.createCompatibleWritableRaster(mHeader.mWidth, 1); + final WritableRaster rowRaster = pSourceCM.createCompatibleWritableRaster(header.width, 1); final int channels = rowRaster.getNumBands(); final boolean banded = raster.getDataBuffer().getNumBanks() > 1; @@ -397,30 +499,21 @@ public class PSDImageReader extends ImageReaderBase { for (int c = 0; c < channels; c++) { int bandOffset = banded ? 0 : interleavedBands - 1 - c; - switch (mHeader.mBits) { + switch (header.bits) { case 1: byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - DataBufferByte buffer1 = (DataBufferByte) raster.getDataBuffer(); - byte[] data1 = banded ? buffer1.getData(c) : buffer1.getData(); - - read1bitChannel(c, mHeader.mChannels, data1, interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, pCompression == PSD.COMPRESSION_RLE); + read1bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, pCompression == PSD.COMPRESSION_RLE); break; case 8: byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - DataBufferByte buffer8 = (DataBufferByte) raster.getDataBuffer(); - byte[] data8 = banded ? buffer8.getData(c) : buffer8.getData(); - - read8bitChannel(c, mHeader.mChannels, data8, interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, c * mHeader.mHeight, pCompression == PSD.COMPRESSION_RLE); + read8bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); break; case 16: short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); - DataBufferUShort buffer16 = (DataBufferUShort) raster.getDataBuffer(); - short[] data16 = banded ? buffer16.getData(c) : buffer16.getData(); - - read16bitChannel(c, mHeader.mChannels, data16, interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, c * mHeader.mHeight, pCompression == PSD.COMPRESSION_RLE); + read16bitChannel(c, header.channels, raster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE); break; default: - throw new IIOException(String.format("Unknown PSD bit depth: %s", mHeader.mBits)); + throw new IIOException(String.format("Unknown PSD bit depth: %s", header.bits)); } if (abortRequested()) { @@ -428,14 +521,18 @@ public class PSDImageReader extends ImageReaderBase { } } - if (mHeader.mBits == 8) { + if (header.bits == 8) { // Compose out the background of the semi-transparent pixels, as PS somehow has the background composed in - decomposeAlpha(destCM, (DataBufferByte) raster.getDataBuffer(), pDest.width, pDest.height, raster.getNumBands()); + decomposeAlpha(destCM, raster.getDataBuffer(), pDest.width, pDest.height, raster.getNumBands()); } } + private void processImageProgressForChannel(int channel, int channelCount, int y, int height) { + processImageProgress(100f * channel / channelCount + 100f * y / (height * channelCount)); + } + private void read16bitChannel(final int pChannel, final int pChannelCount, - final short[] pData, final int pBands, final int pBandOffset, + final DataBuffer pData, final int pBands, final int pBandOffset, final ColorModel pSourceColorModel, final short[] pRow, final Rectangle pSource, final Rectangle pDest, @@ -446,6 +543,7 @@ public class PSDImageReader extends ImageReaderBase { final boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK; final int colorComponents = pSourceColorModel.getColorSpace().getNumComponents(); + final boolean banded = pData.getNumBanks() > 1; for (int y = 0; y < pChannelHeight; y++) { // NOTE: Length is in *16 bit values* (shorts) @@ -455,7 +553,7 @@ public class PSDImageReader extends ImageReaderBase { // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(mImageInput, length); + DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); try { for (int x = 0; x < pChannelWidth; x++) { pRow[x] = input.readShort(); @@ -466,7 +564,7 @@ public class PSDImageReader extends ImageReaderBase { } } else { - mImageInput.readFully(pRow, 0, pChannelWidth); + imageInput.readFully(pRow, 0, pChannelWidth); } // TODO: Destination offset...?? @@ -480,22 +578,22 @@ public class PSDImageReader extends ImageReaderBase { value = (short) (65535 - value & 0xffff); } - pData[offset + x * pBands] = value; + pData.setElem(banded ? pChannel : 0, offset + x * pBands, value); } } else { - mImageInput.skipBytes(length); + imageInput.skipBytes(length); } if (abortRequested()) { break; } - processImageProgress((pChannel * y * 100) / pChannelCount * pChannelHeight); + processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight); } } private void read8bitChannel(final int pChannel, final int pChannelCount, - final byte[] pData, final int pBands, final int pBandOffset, + final DataBuffer pData, final int pBands, final int pBandOffset, final ColorModel pSourceColorModel, final byte[] pRow, final Rectangle pSource, final Rectangle pDest, @@ -506,6 +604,7 @@ public class PSDImageReader extends ImageReaderBase { final boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK; final int colorComponents = pSourceColorModel.getColorSpace().getNumComponents(); + final boolean banded = pData.getNumBanks() > 1; for (int y = 0; y < pChannelHeight; y++) { int length = pRLECompressed ? pRowByteCounts[pRowOffset + y] : pChannelWidth; @@ -514,7 +613,7 @@ public class PSDImageReader extends ImageReaderBase { // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(mImageInput, length); + DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); try { input.readFully(pRow, 0, pChannelWidth); } @@ -523,10 +622,9 @@ public class PSDImageReader extends ImageReaderBase { } } else { - mImageInput.readFully(pRow, 0, pChannelWidth); + imageInput.readFully(pRow, 0, pChannelWidth); } - // TODO: If banded and not sub sampling/cmyk, we could just copy using System.arraycopy // TODO: Destination offset...?? // Copy line sub sampled into real data int offset = (y - pSource.y) / pYSub * pDest.width * pBands + pBandOffset; @@ -538,23 +636,23 @@ public class PSDImageReader extends ImageReaderBase { value = (byte) (255 - value & 0xff); } - pData[offset + x * pBands] = value; + pData.setElem(banded ? pChannel : 0, offset + x * pBands, value); } } else { - mImageInput.skipBytes(length); + imageInput.skipBytes(length); } if (abortRequested()) { break; } - processImageProgress((pChannel * y * 100) / pChannelCount * pChannelHeight); + processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight); } } @SuppressWarnings({"UnusedDeclaration"}) private void read1bitChannel(final int pChannel, final int pChannelCount, - final byte[] pData, final int pBands, final int pBandOffset, + final DataBuffer pData, final int pBands, final int pBandOffset, final ColorModel pSourceColorModel, final byte[] pRow, final Rectangle pSource, final Rectangle pDest, @@ -564,6 +662,7 @@ public class PSDImageReader extends ImageReaderBase { // NOTE: 1 bit channels only occurs once final int destWidth = (pDest.width + 7) / 8; + final boolean banded = pData.getNumBanks() > 1; for (int y = 0; y < pChannelHeight; y++) { int length = pRLECompressed ? pRowByteCounts[y] : pChannelWidth; @@ -572,7 +671,7 @@ public class PSDImageReader extends ImageReaderBase { // Read entire line, if within source region and sampling if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) { if (pRLECompressed) { - DataInputStream input = PSDUtil.createPackBitsStream(mImageInput, length); + DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length); try { input.readFully(pRow, 0, pRow.length); } @@ -581,7 +680,7 @@ public class PSDImageReader extends ImageReaderBase { } } else { - mImageInput.readFully(pRow, 0, pRow.length); + imageInput.readFully(pRow, 0, pRow.length); } // TODO: Destination offset...?? @@ -591,7 +690,7 @@ public class PSDImageReader extends ImageReaderBase { for (int i = 0; i < destWidth; i++) { byte value = pRow[pSource.x / 8 + i * pXSub]; // NOTE: Invert bits to match Java's default monochrome - pData[offset + i] = (byte) (~value & 0xff); + pData.setElem(banded ? pChannel : 0, offset + i, (byte) (~value & 0xff)); } } else { @@ -615,22 +714,22 @@ public class PSDImageReader extends ImageReaderBase { } // NOTE: Invert bits to match Java's default monochrome - pData[offset + i] = (byte) (~result & 0xff); + pData.setElem(banded ? pChannel : 0, offset + i, (byte) (~result & 0xff)); } } } else { - mImageInput.skipBytes(length); + imageInput.skipBytes(length); } if (abortRequested()) { break; } - processImageProgress((pChannel * y * 100) / pChannelCount * pChannelHeight); + processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight); } } - private void decomposeAlpha(final ColorModel pModel, final DataBufferByte pBuffer, + private void decomposeAlpha(final ColorModel pModel, final DataBuffer pBuffer, final int pWidth, final int pHeight, final int pChannels) { // TODO: Is the document background always white!? // TODO: What about CMYK + alpha? @@ -638,48 +737,44 @@ public class PSDImageReader extends ImageReaderBase { // TODO: Probably faster to do this in line.. if (pBuffer.getNumBanks() > 1) { - byte[][] data = pBuffer.getBankData(); - for (int y = 0; y < pHeight; y++) { for (int x = 0; x < pWidth; x++) { int offset = (x + y * pWidth); // ARGB format - int alpha = data[pChannels - 1][offset] & 0xff; + int alpha = pBuffer.getElem(pChannels - 1, offset) & 0xff; if (alpha != 0) { double normalizedAlpha = alpha / 255.0; for (int i = 0; i < pChannels - 1; i++) { - data[i][offset] = decompose(data[i][offset] & 0xff, normalizedAlpha); + pBuffer.setElem(i, offset, decompose(pBuffer.getElem(i, offset) & 0xff, normalizedAlpha)); } } else { for (int i = 0; i < pChannels - 1; i++) { - data[i][offset] = 0; + pBuffer.setElem(i, offset, 0); } } } } } else { - byte[] data = pBuffer.getData(); - for (int y = 0; y < pHeight; y++) { for (int x = 0; x < pWidth; x++) { int offset = (x + y * pWidth) * pChannels; // ABGR format - int alpha = data[offset] & 0xff; + int alpha = pBuffer.getElem(offset) & 0xff; if (alpha != 0) { double normalizedAlpha = alpha / 255.0; for (int i = 1; i < pChannels; i++) { - data[offset + i] = decompose(data[offset + i] & 0xff, normalizedAlpha); + pBuffer.setElem(offset + i, decompose(pBuffer.getElem(offset + i) & 0xff, normalizedAlpha)); } } else { for (int i = 1; i < pChannels; i++) { - data[offset + i] = 0; + pBuffer.setElem(offset + i, 0); } } } @@ -696,11 +791,11 @@ public class PSDImageReader extends ImageReaderBase { private void readHeader() throws IOException { assertInput(); - if (mHeader == null) { - mHeader = new PSDHeader(mImageInput); + if (header == null) { + header = new PSDHeader(imageInput); - mMetadata = new PSDMetadata(); - mMetadata.mHeader = mHeader; + metadata = new PSDMetadata(); + metadata.header = header; /* Contains the required data to define the color mode. @@ -713,18 +808,18 @@ public class PSDImageReader extends ImageReaderBase { the duotone image as a grayscale image, and keep the duotone specification around as a black box for use when saving the file. */ - if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) { - mMetadata.mColorData = new PSDColorData(mImageInput); + if (header.mode == PSD.COLOR_MODE_INDEXED) { + metadata.colorData = new PSDColorData(imageInput); } else { // TODO: We need to store the duotone spec if we decide to create a writer... // Skip color mode data for other modes - long length = mImageInput.readUnsignedInt(); - mImageInput.skipBytes(length); + long length = imageInput.readUnsignedInt(); + imageInput.skipBytes(length); } // Don't need the header again - mImageInput.flushBefore(mImageInput.getStreamPosition()); + imageInput.flushBefore(imageInput.getStreamPosition()); } } @@ -732,107 +827,109 @@ public class PSDImageReader extends ImageReaderBase { // TODO: Obey ignoreMetadata private void readImageResources(final boolean pParseData) throws IOException { // TODO: Avoid unnecessary stream repositioning - long pos = mImageInput.getFlushedPosition(); - mImageInput.seek(pos); + long pos = imageInput.getFlushedPosition(); + imageInput.seek(pos); - long length = mImageInput.readUnsignedInt(); + long length = imageInput.readUnsignedInt(); if (pParseData && length > 0) { - if (mMetadata.mImageResources == null) { - mMetadata.mImageResources = new ArrayList(); - long expectedEnd = mImageInput.getStreamPosition() + length; + if (metadata.imageResources == null) { + metadata.imageResources = new ArrayList(); + long expectedEnd = imageInput.getStreamPosition() + length; - while (mImageInput.getStreamPosition() < expectedEnd) { + while (imageInput.getStreamPosition() < expectedEnd) { // TODO: Have PSDImageResources defer actual parsing? (Just store stream offsets) - PSDImageResource resource = PSDImageResource.read(mImageInput); - mMetadata.mImageResources.add(resource); + PSDImageResource resource = PSDImageResource.read(imageInput); + metadata.imageResources.add(resource); } - if (mImageInput.getStreamPosition() != expectedEnd) { + if (imageInput.getStreamPosition() != expectedEnd) { throw new IIOException("Corrupt PSD document"); // ..or maybe just a bug in the reader.. ;-) } } } - mImageInput.seek(pos + length + 4); + imageInput.seek(pos + length + 4); } // TODO: Flags or list of interesting resources to parse // TODO: Obey ignoreMetadata private void readLayerAndMaskInfo(final boolean pParseData) throws IOException { // TODO: Make sure we are positioned correctly - long length = mImageInput.readUnsignedInt(); + // TODO: Avoid unnecessary stream repositioning + long length = imageInput.readUnsignedInt(); if (pParseData && length > 0) { - long pos = mImageInput.getStreamPosition(); + long pos = imageInput.getStreamPosition(); - long layerInfoLength = mImageInput.readUnsignedInt(); + long read; + if (metadata.layerInfo == null) { + long layerInfoLength = imageInput.readUnsignedInt(); - /* - "Layer count. If it is a negative number, its absolute value is the number of - layers and the first alpha channel contains the transparency data for the - merged result." - */ - // TODO: Figure out what the last part of that sentence means in practice... - int layers = mImageInput.readShort(); + /* + "Layer count. If it is a negative number, its absolute value is the number of + layers and the first alpha channel contains the transparency data for the + merged result." + */ + // TODO: Figure out what the last part of that sentence means in practice... + int layers = imageInput.readShort(); - PSDLayerInfo[] layerInfos = new PSDLayerInfo[Math.abs(layers)]; - for (int i = 0; i < layerInfos.length; i++) { - layerInfos[i] = new PSDLayerInfo(mImageInput); - } - mMetadata.mLayerInfo = Arrays.asList(layerInfos); + PSDLayerInfo[] layerInfos = new PSDLayerInfo[Math.abs(layers)]; + for (int i = 0; i < layerInfos.length; i++) { + layerInfos[i] = new PSDLayerInfo(imageInput); + } + metadata.layerInfo = Arrays.asList(layerInfos); + metadata.layersStart = imageInput.getStreamPosition(); - // TODO: Clean-up - mImageInput.mark(); - ImageTypeSpecifier raw = getRawImageTypeInternal(0); - ImageTypeSpecifier imageType = getImageTypes(0).next(); - mImageInput.reset(); + read = imageInput.getStreamPosition() - pos; - for (PSDLayerInfo layerInfo : layerInfos) { - // TODO: If not explicitly needed, skip layers... - BufferedImage layer = readLayerData(layerInfo, raw, imageType); + long diff = layerInfoLength - (read - 4); // - 4 for the layerInfoLength field itself + // System.out.println("diff: " + diff); + imageInput.skipBytes(diff); - // TODO: Don't show! Store in meta data somehow... -// if (layer != null) { -// showIt(layer, layerInfo.mLayerName + " " + layerInfo.mBlendMode.toString()); -// } + // TODO: Global LayerMaskInfo (18 bytes or more..?) + // 4 (length), 2 (colorSpace), 8 (4 * 2 byte color components), 2 (opacity %), 1 (kind), variable (pad) + long layerMaskInfoLength = imageInput.readUnsignedInt(); + // System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength); + if (layerMaskInfoLength > 0) { + metadata.globalLayerMask = new PSDGlobalLayerMask(imageInput); +// System.err.println("globalLayerMask: " + metadata.globalLayerMask); + } } - long read = mImageInput.getStreamPosition() - pos; - - long diff = layerInfoLength - (read - 4); // - 4 for the layerInfoLength field itself -// System.out.println("diff: " + diff); - mImageInput.skipBytes(diff); - - // TODO: Global LayerMaskInfo (18 bytes or more..?) - // 4 (length), 2 (colorSpace), 8 (4 * 2 byte color components), 2 (opacity %), 1 (kind), variable (pad) - long layerMaskInfoLength = mImageInput.readUnsignedInt(); -// System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength); - if (layerMaskInfoLength > 0) { - mMetadata.mGlobalLayerMask = new PSDGlobalLayerMask(mImageInput); -// System.out.println("mGlobalLayerMask: " + mGlobalLayerMask); - } - - read = mImageInput.getStreamPosition() - pos; + read = imageInput.getStreamPosition() - pos; long toSkip = length - read; // System.out.println("toSkip: " + toSkip); - mImageInput.skipBytes(toSkip); + imageInput.skipBytes(toSkip); } else { // Skip entire layer and mask section - mImageInput.skipBytes(length); + imageInput.skipBytes(length); } } - private BufferedImage readLayerData(final PSDLayerInfo pLayerInfo, final ImageTypeSpecifier pRawType, final ImageTypeSpecifier pImageType) throws IOException { - final int width = pLayerInfo.mRight - pLayerInfo.mLeft; - final int height = pLayerInfo.mBottom - pLayerInfo.mTop; +// private BufferedImage readLayerData(int layerIndex, final PSDLayerInfo pLayerInfo, final ImageTypeSpecifier pRawType, final ImageTypeSpecifier pImageType, ImageReadParam param) throws IOException { + private BufferedImage readLayerData(final int layerIndex, final ImageReadParam param) throws IOException { + final int width = getLayerWidth(layerIndex); + final int height = getLayerHeight(layerIndex); + + PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex); +// final int width = layerInfo.right - layerInfo.left; +// final int height = layerInfo.bottom - layerInfo.top; // Even if raw/imageType has no alpha, the layers may still have alpha... - ImageTypeSpecifier imageType = getImageTypeForLayer(pImageType, pLayerInfo); + ImageTypeSpecifier imageType = getRawImageTypeForLayer(layerIndex); + // TODO: Find a better way of handling layers of size 0 + // - Return null? Return a BufferedImage subclass that has no data (0 x 0)? // Create image (or dummy, if h/w are <= 0) - BufferedImage layer = imageType.createBufferedImage(Math.max(1, width), Math.max(1, height)); +// BufferedImage layer = imageType.createBufferedImage(Math.max(1, width), Math.max(1, height)); + if (width <= 0 || height <= 0) { + return null; + } + BufferedImage layer = getDestination(param, getImageTypes(layerIndex + 1), Math.max(1, width), Math.max(1, height)); + + imageInput.seek(findLayerStartPos(layerIndex)); // Source/destination area Rectangle area = new Rectangle(width, height); @@ -845,26 +942,26 @@ public class PSDImageReader extends ImageReaderBase { final ColorModel destCM = layer.getColorModel(); // TODO: This raster is 3-5 times longer than needed, depending on number of channels... - ColorModel sourceCM = pRawType.getColorModel(); + ColorModel sourceCM = imageType.getColorModel(); final WritableRaster rowRaster = width > 0 ? sourceCM.createCompatibleWritableRaster(width, 1) : null; // final int channels = rowRaster.getNumBands(); final boolean banded = raster.getDataBuffer().getNumBanks() > 1; final int interleavedBands = banded ? 1 : raster.getNumBands(); - for (PSDChannelInfo channelInfo : pLayerInfo.mChannelInfo) { - int compression = mImageInput.readUnsignedShort(); + for (PSDChannelInfo channelInfo : layerInfo.channelInfo) { + int compression = imageInput.readUnsignedShort(); // Skip layer if we can't read it // channelId == -2 means "user supplied layer mask", whatever that is... - if (width <= 0 || height <= 0 || channelInfo.mChannelId == -2 || + if (width <= 0 || height <= 0 || channelInfo.channelId == -2 || (compression != PSD.COMPRESSION_NONE && compression != PSD.COMPRESSION_RLE)) { - mImageInput.skipBytes(channelInfo.mLength - 2); + imageInput.skipBytes(channelInfo.length - 2); } else { // 0 = red, 1 = green, etc // -1 = transparency mask; -2 = user supplied layer mask - int c = channelInfo.mChannelId == -1 ? pLayerInfo.mChannelInfo.length - 1 : channelInfo.mChannelId; + int c = channelInfo.channelId == -1 ? layerInfo.channelInfo.length - 1 : channelInfo.channelId; // NOTE: For layers, byte counts are written per channel, while for the composite data // byte counts are written for all channels before the image data. @@ -879,9 +976,9 @@ public class PSDImageReader extends ImageReaderBase { // If RLE, the the image data starts with the byte counts // for all the scan lines in the channel (LayerBottom-LayerTop), with // each count stored as a two*byte value. - byteCounts = new int[pLayerInfo.mBottom - pLayerInfo.mTop]; + byteCounts = new int[layerInfo.bottom - layerInfo.top]; for (int i = 0; i < byteCounts.length; i++) { - byteCounts[i] = mImageInput.readUnsignedShort(); + byteCounts[i] = imageInput.readUnsignedShort(); } break; @@ -894,30 +991,21 @@ public class PSDImageReader extends ImageReaderBase { int bandOffset = banded ? 0 : interleavedBands - 1 - c; - switch (mHeader.mBits) { + switch (header.bits) { case 1: byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - DataBufferByte buffer1 = (DataBufferByte) raster.getDataBuffer(); - byte[] data1 = banded ? buffer1.getData(c) : buffer1.getData(); - - read1bitChannel(c, imageType.getNumBands(), data1, interleavedBands, bandOffset, sourceCM, row1, area, area, xsub, ysub, width, height, byteCounts, compression == PSD.COMPRESSION_RLE); + read1bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row1, area, area, xsub, ysub, width, height, byteCounts, compression == PSD.COMPRESSION_RLE); break; case 8: byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); - DataBufferByte buffer8 = (DataBufferByte) raster.getDataBuffer(); - byte[] data8 = banded ? buffer8.getData(c) : buffer8.getData(); - - read8bitChannel(c, imageType.getNumBands(), data8, interleavedBands, bandOffset, sourceCM, row8, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE); + read8bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row8, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE); break; case 16: short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); - DataBufferUShort buffer16 = (DataBufferUShort) raster.getDataBuffer(); - short[] data16 = banded ? buffer16.getData(c) : buffer16.getData(); - - read16bitChannel(c, imageType.getNumBands(), data16, interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE); + read16bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE); break; default: - throw new IIOException(String.format("Unknown PSD bit depth: %s", mHeader.mBits)); + throw new IIOException(String.format("Unknown PSD bit depth: %s", header.bits)); } if (abortRequested()) { @@ -929,49 +1017,64 @@ public class PSDImageReader extends ImageReaderBase { return layer; } - private ImageTypeSpecifier getImageTypeForLayer(final ImageTypeSpecifier pOriginal, final PSDLayerInfo pLayerInfo) { + private ImageTypeSpecifier getRawImageTypeForLayer(final int layerIndex) throws IOException { + ImageTypeSpecifier compositeType = getRawImageTypeForCompositeLayer(); + + PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex); + // If layer has more channels than composite data, it's normally extra alpha... - if (pLayerInfo.mChannelInfo.length > pOriginal.getNumBands()) { + if (layerInfo.channelInfo.length > compositeType.getNumBands()) { // ...but, it could also be just the user mask... boolean userMask = false; - for (PSDChannelInfo channelInfo : pLayerInfo.mChannelInfo) { - if (channelInfo.mChannelId == -2) { + for (PSDChannelInfo channelInfo : layerInfo.channelInfo) { + if (channelInfo.channelId == -2) { userMask = true; break; } } - int newBandNum = pLayerInfo.mChannelInfo.length - (userMask ? 1 : 0); + int newBandNum = layerInfo.channelInfo.length - (userMask ? 1 : 0); // If there really is more channels, then create new imageTypeSpec - if (newBandNum > pOriginal.getNumBands()) { - int[] offs = new int[newBandNum]; - for (int i = 0, offsLength = offs.length; i < offsLength; i++) { - offs[i] = offsLength - i; + if (newBandNum > compositeType.getNumBands()) { + int[] indices = new int[newBandNum]; + for (int i = 0, indicesLength = indices.length; i < indicesLength; i++) { + indices[i] = indicesLength - i; } - return ImageTypeSpecifier.createInterleaved(pOriginal.getColorModel().getColorSpace(), offs, pOriginal.getSampleModel().getDataType(), true, false); + int[] offs = new int[newBandNum]; + for (int i = 0, offsLength = offs.length; i < offsLength; i++) { + offs[i] = 0; + } + + return ImageTypeSpecifier.createBanded(compositeType.getColorModel().getColorSpace(), indices, offs, compositeType.getSampleModel().getDataType(), true, false); } } - return pOriginal; + + return compositeType; } /// Layer support + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + // NOTE: Spec says this method should throw IllegalStateException if allowSearch && isSeekForwardOnly() + // But that makes no sense for a format (like PSD) that does not need to search, right? + + readHeader(); + readImageResources(false); + readLayerAndMaskInfo(true); // TODO: Consider quicker reading of just the number of layers. + + return metadata.layerInfo != null ? metadata.layerInfo.size() + 1 : 1; // TODO: Only plus one, if "has real merged data"? + } + // TODO: For now, leave as Metadata /* - int getNumLayers(int pImageIndex) throws IOException; - - boolean hasLayers(int pImageIndex) throws IOException; - - BufferedImage readLayer(int pImageIndex, int pLayerIndex, ImageReadParam pParam) throws IOException; - - int getLayerWidth(int pImageIndex, int pLayerIndex) throws IOException; - - int getLayerHeight(int pImageIndex, int pLayerIndex) throws IOException; // ? - Point getLayerOffset(int pImageIndex, int pLayerIndex) throws IOException; + Point getOffset(int pImageIndex) throws IOException; + // Return 0, 0 for index 0, otherwise use layer offset */ @@ -986,22 +1089,22 @@ public class PSDImageReader extends ImageReaderBase { } @Override - public IIOMetadata getImageMetadata(final int pImageIndex) throws IOException { + public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { // TODO: Implement - checkBounds(pImageIndex); + checkBounds(imageIndex); readHeader(); readImageResources(true); readLayerAndMaskInfo(true); // TODO: Need to make sure compression is set in metadata, even without reading the image data! - mMetadata.mCompression = mImageInput.readShort(); + metadata.compression = imageInput.readShort(); -// mMetadata.mHeader = mHeader; -// mMetadata.mColorData = mColorData; -// mMetadata.mImageResources = mImageResources; +// metadata.header = header; +// metadata.colorData = colorData; +// metadata.imageResources = imageResources; - return mMetadata; // TODO: clone if we change to mutable metadata + return metadata; // TODO: clone if we change to mutable metadata } @Override @@ -1016,21 +1119,21 @@ public class PSDImageReader extends ImageReaderBase { return true; } - private List getThumbnailResources(final int pIndex) throws IOException { - checkBounds(pIndex); + private List getThumbnailResources(final int imageIndex) throws IOException { + checkBounds(imageIndex); readHeader(); List thumbnails = null; - if (mMetadata.mImageResources == null) { + if (metadata.imageResources == null) { // TODO: Need flag here, to specify what resources to read... readImageResources(true); // TODO: Skip this, requires storing some stream offsets readLayerAndMaskInfo(false); } - for (PSDImageResource resource : mMetadata.mImageResources) { + for (PSDImageResource resource : metadata.imageResources) { if (resource instanceof PSDThumbnail) { if (thumbnails == null) { thumbnails = new ArrayList(); @@ -1044,57 +1147,60 @@ public class PSDImageReader extends ImageReaderBase { } @Override - public int getNumThumbnails(final int pIndex) throws IOException { - List thumbnails = getThumbnailResources(pIndex); + public int getNumThumbnails(final int imageIndex) throws IOException { + List thumbnails = getThumbnailResources(imageIndex); return thumbnails == null ? 0 : thumbnails.size(); } - private PSDThumbnail getThumbnailResource(final int pImageIndex, final int pThumbnailIndex) throws IOException { - List thumbnails = getThumbnailResources(pImageIndex); + private PSDThumbnail getThumbnailResource(final int imageIndex, final int thumbnailIndex) throws IOException { + List thumbnails = getThumbnailResources(imageIndex); if (thumbnails == null) { - throw new IndexOutOfBoundsException(String.format("thumbnail index %d > 0", pThumbnailIndex)); + throw new IndexOutOfBoundsException(String.format("thumbnail index %d > 0", thumbnailIndex)); } - return thumbnails.get(pThumbnailIndex); + return thumbnails.get(thumbnailIndex); } @Override - public int getThumbnailWidth(final int pImageIndex, final int pThumbnailIndex) throws IOException { - return getThumbnailResource(pImageIndex, pThumbnailIndex).getWidth(); + public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException { + return getThumbnailResource(imageIndex, thumbnailIndex).getWidth(); } @Override - public int getThumbnailHeight(final int pImageIndex, final int pThumbnailIndex) throws IOException { - return getThumbnailResource(pImageIndex, pThumbnailIndex).getHeight(); + public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException { + return getThumbnailResource(imageIndex, thumbnailIndex).getHeight(); } @Override - public BufferedImage readThumbnail(final int pImageIndex, final int pThumbnailIndex) throws IOException { + public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException { // TODO: Thumbnail progress listeners... - PSDThumbnail thumbnail = getThumbnailResource(pImageIndex, pThumbnailIndex); + PSDThumbnail thumbnail = getThumbnailResource(imageIndex, thumbnailIndex); - // TODO: Defer decoding // TODO: It's possible to attach listeners to the ImageIO reader delegate... But do we really care? - processThumbnailStarted(pImageIndex, pThumbnailIndex); + processThumbnailStarted(imageIndex, thumbnailIndex); + processThumbnailProgress(0); + BufferedImage image = thumbnail.getThumbnail(); + processThumbnailProgress(100); processThumbnailComplete(); - // TODO: Returning a cached mutable thumbnail is not really safe... - return thumbnail.getThumbnail(); + return image; } /// Functional testing public static void main(final String[] pArgs) throws IOException { int subsampleFactor = 1; Rectangle sourceRegion = null; + boolean readLayers = false; + boolean readThumbnails = false; int idx = 0; while (pArgs[idx].charAt(0) == '-') { - if (pArgs[idx].equals("-s")) { + if (pArgs[idx].equals("-s") || pArgs[idx].equals("--subsampling")) { subsampleFactor = Integer.parseInt(pArgs[++idx]); } - else if (pArgs[idx].equals("-r")) { + else if (pArgs[idx].equals("-r") || pArgs[idx].equals("--sourceregion")) { int xw = Integer.parseInt(pArgs[++idx]); int yh = Integer.parseInt(pArgs[++idx]); @@ -1114,6 +1220,12 @@ public class PSDImageReader extends ImageReaderBase { System.out.println("sourceRegion: " + sourceRegion); } + else if (pArgs[idx].equals("-l") || pArgs[idx].equals("--layers")) { + readLayers = true; + } + else if (pArgs[idx].equals("-t") || pArgs[idx].equals("--thumbnails")) { + readThumbnails = true; + } else { System.err.println("Usage: java PSDImageReader [-s ] [-r [] ] "); System.exit(1); @@ -1129,15 +1241,15 @@ public class PSDImageReader extends ImageReaderBase { imageReader.setInput(stream); imageReader.readHeader(); -// System.out.println("imageReader.mHeader: " + imageReader.mHeader); +// System.out.println("imageReader.header: " + imageReader.header); imageReader.readImageResources(true); - System.out.println("imageReader.mImageResources: " + imageReader.mMetadata.mImageResources); + System.out.println("imageReader.imageResources: " + imageReader.metadata.imageResources); System.out.println(); imageReader.readLayerAndMaskInfo(true); - System.out.println("imageReader.mLayerInfo: " + imageReader.mMetadata.mLayerInfo); -// System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask); + System.out.println("imageReader.layerInfo: " + imageReader.metadata.layerInfo); +// System.out.println("imageReader.globalLayerMask: " + imageReader.globalLayerMask); System.out.println(); IIOMetadata metadata = imageReader.getImageMetadata(0); @@ -1146,7 +1258,6 @@ public class PSDImageReader extends ImageReaderBase { node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); serializer = new XMLSerializer(System.out, System.getProperty("file.encoding")); - serializer.setIndentation(" "); serializer.serialize(node, true); System.out.println(); @@ -1154,7 +1265,7 @@ public class PSDImageReader extends ImageReaderBase { // serializer = new XMLSerializer(System.out, System.getProperty("file.encoding")); serializer.serialize(node, true); - if (imageReader.hasThumbnails(0)) { + if (readThumbnails && imageReader.hasThumbnails(0)) { int thumbnails = imageReader.getNumThumbnails(0); for (int i = 0; i < thumbnails; i++) { showIt(imageReader.readThumbnail(0, i), String.format("Thumbnail %d", i)); @@ -1176,22 +1287,34 @@ public class PSDImageReader extends ImageReaderBase { // param.setDestinationType(imageReader.getRawImageType(0)); BufferedImage image = imageReader.read(0, param); - System.out.println("time: " + (System.currentTimeMillis() - start)); + System.out.println("read time: " + (System.currentTimeMillis() - start)); System.out.println("image: " + image); if (image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) { try { ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null); - image = op.filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR_PRE)); + GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + image = op.filter(image, gc.createCompatibleImage(image.getWidth(), image.getHeight(), image.getTransparency())); } catch (Exception e) { e.printStackTrace(); image = ImageUtil.accelerate(image); } - System.out.println("time: " + (System.currentTimeMillis() - start)); + System.out.println("conversion time: " + (System.currentTimeMillis() - start)); System.out.println("image: " + image); } showIt(image, file.getName()); + + if (readLayers) { + int images = imageReader.getNumImages(true); + for (int i = 1; i < images; i++) { + start = System.currentTimeMillis(); + BufferedImage layer = imageReader.read(i); + System.out.println("layer read time: " + (System.currentTimeMillis() - start)); + System.err.println("layer: " + layer); + showIt(layer, "layer " + i); + } + } } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java index 0b63af8d..754b355e 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java @@ -53,10 +53,10 @@ public class PSDImageReaderSpi extends ImageReaderSpi { this(IIOUtil.getProviderInfo(PSDImageReaderSpi.class)); } - private PSDImageReaderSpi(final ProviderInfo pProviderInfo) { + private PSDImageReaderSpi(final ProviderInfo providerInfo) { super( - pProviderInfo.getVendorName(), - pProviderInfo.getVersion(), + providerInfo.getVendorName(), + providerInfo.getVersion(), new String[]{"psd", "PSD"}, new String[]{"psd"}, new String[]{ diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java index 7aa60dc4..3ea653f1 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java @@ -43,38 +43,38 @@ import java.lang.reflect.Field; * @author last modified by $Author: haraldk$ * @version $Id: PSDImageResource.java,v 1.0 Apr 29, 2008 5:49:06 PM haraldk Exp$ */ -class PSDImageResource { +public class PSDImageResource { // TODO: Refactor image resources to separate package // TODO: Change constructor to store stream offset and length only (+ possibly the name), defer reading - final short mId; - final String mName; - final long mSize; + final short id; + final String name; + final long size; - PSDImageResource(final short pId, final ImageInputStream pInput) throws IOException { - mId = pId; + PSDImageResource(final short resourceId, final ImageInputStream input) throws IOException { + id = resourceId; - mName = PSDUtil.readPascalString(pInput); + name = PSDUtil.readPascalString(input); // Skip pad - int nameSize = mName.length() + 1; + int nameSize = name.length() + 1; if (nameSize % 2 != 0) { - pInput.readByte(); + input.readByte(); } - mSize = pInput.readUnsignedInt(); - long startPos = pInput.getStreamPosition(); + size = input.readUnsignedInt(); + long startPos = input.getStreamPosition(); - readData(new SubImageInputStream(pInput, mSize)); + readData(new SubImageInputStream(input, size)); // NOTE: This should never happen, however it's safer to keep it here to - if (pInput.getStreamPosition() != startPos + mSize) { - pInput.seek(startPos + mSize); + if (input.getStreamPosition() != startPos + size) { + input.seek(startPos + size); } // Data is even-padded (word aligned) - if (mSize % 2 != 0) { - pInput.read(); + if (size % 2 != 0) { + input.read(); } } @@ -86,7 +86,7 @@ class PSDImageResource { */ protected void readData(final ImageInputStream pInput) throws IOException { // TODO: This design is ugly, as subclasses readData is invoked BEFORE their respective constructor... - pInput.skipBytes(mSize); + pInput.skipBytes(size); } @Override @@ -94,7 +94,7 @@ class PSDImageResource { StringBuilder builder = toStringBuilder(); builder.append(", data length: "); - builder.append(mSize); + builder.append(size); builder.append("]"); return builder.toString(); @@ -103,16 +103,16 @@ class PSDImageResource { protected StringBuilder toStringBuilder() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); - String fakeType = resourceTypeForId(mId); + String fakeType = resourceTypeForId(id); if (fakeType != null) { builder.append("(").append(fakeType).append(")"); } builder.append("[ID: 0x"); - builder.append(Integer.toHexString(mId)); - if (mName != null && mName.trim().length() != 0) { + builder.append(Integer.toHexString(id)); + if (name != null && name.trim().length() != 0) { builder.append(", name: \""); - builder.append(mName); + builder.append(name); builder.append("\""); } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerBlendMode.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerBlendMode.java index fb0df440..f18b6071 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerBlendMode.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerBlendMode.java @@ -40,10 +40,10 @@ import java.io.IOException; * @version $Id: PSDLayerBlendMode.java,v 1.0 May 8, 2008 4:34:35 PM haraldk Exp$ */ class PSDLayerBlendMode { - final int mBlendMode; - final int mOpacity; // 0-255 - final int mClipping; // 0: base, 1: non-base - final int mFlags; + final int blendMode; + final int opacity; // 0-255 + final int clipping; // 0: base, 1: non-base + final int flags; public PSDLayerBlendMode(final ImageInputStream pInput) throws IOException { int blendModeSig = pInput.readInt(); @@ -51,11 +51,11 @@ class PSDLayerBlendMode { throw new IIOException("Illegal PSD Blend Mode signature, expected 8BIM: " + PSDUtil.intToStr(blendModeSig)); } - mBlendMode = pInput.readInt(); + blendMode = pInput.readInt(); - mOpacity = pInput.readUnsignedByte(); - mClipping = pInput.readUnsignedByte(); - mFlags = pInput.readUnsignedByte(); + opacity = pInput.readUnsignedByte(); + clipping = pInput.readUnsignedByte(); + flags = pInput.readUnsignedByte(); pInput.readByte(); // Pad } @@ -65,10 +65,10 @@ class PSDLayerBlendMode { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("["); - builder.append("mode: \"").append(PSDUtil.intToStr(mBlendMode)); - builder.append("\", opacity: ").append(mOpacity); - builder.append(", clipping: ").append(mClipping); - switch (mClipping) { + builder.append("mode: \"").append(PSDUtil.intToStr(blendMode)); + builder.append("\", opacity: ").append(opacity); + builder.append(", clipping: ").append(clipping); + switch (clipping) { case 0: builder.append(" (base)"); break; @@ -79,40 +79,40 @@ class PSDLayerBlendMode { builder.append(" (unknown)"); break; } - builder.append(", flags: ").append(byteToBinary(mFlags)); + builder.append(", flags: ").append(byteToBinary(flags)); /* bit 0 = transparency protected; bit 1 = visible; bit 2 = obsolete; bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful information; bit 4 = pixel data irrelevant to appearance of document */ builder.append(" ("); - if ((mFlags & 0x01) != 0) { + if ((flags & 0x01) != 0) { builder.append("Transp. protected, "); } - if ((mFlags & 0x02) != 0) { + if ((flags & 0x02) != 0) { builder.append("Hidden, "); } - if ((mFlags & 0x04) != 0) { + if ((flags & 0x04) != 0) { builder.append("Obsolete bit, "); } - if ((mFlags & 0x08) != 0) { + if ((flags & 0x08) != 0) { builder.append("PS 5.0 data present, "); // "tells if next bit has useful information"... } - if ((mFlags & 0x10) != 0) { + if ((flags & 0x10) != 0) { builder.append("Pixel data irrelevant, "); } - if ((mFlags & 0x20) != 0) { + if ((flags & 0x20) != 0) { builder.append("Unknown bit 5, "); } - if ((mFlags & 0x40) != 0) { + if ((flags & 0x40) != 0) { builder.append("Unknown bit 6, "); } - if ((mFlags & 0x80) != 0) { + if ((flags & 0x80) != 0) { builder.append("Unknown bit 7, "); } // Stupidity... - if (mFlags != 0) { + if (flags != 0) { builder.delete(builder.length() - 2, builder.length()); } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java index 56a62f26..b9e2f07e 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerInfo.java @@ -41,34 +41,34 @@ import java.util.Arrays; * @version $Id: PSDLayerInfo.java,v 1.0 Apr 29, 2008 6:01:12 PM haraldk Exp$ */ class PSDLayerInfo { - final int mTop; - final int mLeft; - final int mBottom; - final int mRight; + final int top; + final int left; + final int bottom; + final int right; - final PSDChannelInfo[] mChannelInfo; - final PSDLayerBlendMode mBlendMode; - final PSDLayerMaskData mLayerMaskData; - final PSDChannelSourceDestinationRange[] mRanges; - final String mLayerName; + final PSDChannelInfo[] channelInfo; + final PSDLayerBlendMode blendMode; + final PSDLayerMaskData layerMaskData; + final PSDChannelSourceDestinationRange[] ranges; + final String layerName; PSDLayerInfo(ImageInputStream pInput) throws IOException { - mTop = pInput.readInt(); - mLeft = pInput.readInt(); - mBottom = pInput.readInt(); - mRight = pInput.readInt(); + top = pInput.readInt(); + left = pInput.readInt(); + bottom = pInput.readInt(); + right = pInput.readInt(); int channels = pInput.readUnsignedShort(); - mChannelInfo = new PSDChannelInfo[channels]; + channelInfo = new PSDChannelInfo[channels]; for (int i = 0; i < channels; i++) { short channelId = pInput.readShort(); long length = pInput.readUnsignedInt(); - mChannelInfo[i] = new PSDChannelInfo(channelId, length); + channelInfo[i] = new PSDChannelInfo(channelId, length); } - mBlendMode = new PSDLayerBlendMode(pInput); + blendMode = new PSDLayerBlendMode(pInput); // Lenght of layer mask data long extraDataSize = pInput.readUnsignedInt(); @@ -78,10 +78,10 @@ class PSDLayerInfo { // Layer mask/adjustment layer data int layerMaskDataSize = pInput.readInt(); // May be 0, 20 or 36 bytes... if (layerMaskDataSize != 0) { - mLayerMaskData = new PSDLayerMaskData(pInput, layerMaskDataSize); + layerMaskData = new PSDLayerMaskData(pInput, layerMaskDataSize); } else { - mLayerMaskData = null; + layerMaskData = null; } int layerBlendingDataSize = pInput.readInt(); @@ -89,15 +89,15 @@ class PSDLayerInfo { throw new IIOException("Illegal PSD Layer Blending Data size: " + layerBlendingDataSize + ", expected multiple of 8"); } - mRanges = new PSDChannelSourceDestinationRange[layerBlendingDataSize / 8]; - for (int i = 0; i < mRanges.length; i++) { - mRanges[i] = new PSDChannelSourceDestinationRange(pInput, (i == 0 ? "Gray" : "Channel " + (i - 1))); + ranges = new PSDChannelSourceDestinationRange[layerBlendingDataSize / 8]; + for (int i = 0; i < ranges.length; i++) { + ranges[i] = new PSDChannelSourceDestinationRange(pInput, (i == 0 ? "Gray" : "Channel " + (i - 1))); } - mLayerName = PSDUtil.readPascalString(pInput); + layerName = PSDUtil.readPascalString(pInput); - int layerNameSize = mLayerName.length() + 1; + int layerNameSize = layerName.length() + 1; // Skip pad bytes for long word alignment if (layerNameSize % 4 != 0) { @@ -115,18 +115,18 @@ class PSDLayerInfo { public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("["); - builder.append("top: ").append(mTop); - builder.append(", left: ").append(mLeft); - builder.append(", bottom: ").append(mBottom); - builder.append(", right: ").append(mRight); + builder.append("top: ").append(top); + builder.append(", left: ").append(left); + builder.append(", bottom: ").append(bottom); + builder.append(", right: ").append(right); - builder.append(", channels: ").append(Arrays.toString(mChannelInfo)); - builder.append(", blend mode: ").append(mBlendMode); - if (mLayerMaskData != null) { - builder.append(", layer mask data: ").append(mLayerMaskData); + builder.append(", channels: ").append(Arrays.toString(channelInfo)); + builder.append(", blend mode: ").append(blendMode); + if (layerMaskData != null) { + builder.append(", layer mask data: ").append(layerMaskData); } - builder.append(", ranges: ").append(Arrays.toString(mRanges)); - builder.append(", layer name: \"").append(mLayerName).append("\""); + builder.append(", ranges: ").append(Arrays.toString(ranges)); + builder.append(", layer name: \"").append(layerName).append("\""); builder.append("]"); return builder.toString(); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java index 277938b6..16a4bd83 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDLayerMaskData.java @@ -40,48 +40,48 @@ import java.io.IOException; * @version $Id: PSDLayerMaskData.java,v 1.0 May 6, 2008 5:15:05 PM haraldk Exp$ */ class PSDLayerMaskData { - private int mTop; - private int mLeft; - private int mBottom; - private int mRight; - private int mDefaultColor; - private int mFlags; + private int top; + private int left; + private int bottom; + private int right; + private int defaultColor; + private int flags; - private boolean mLarge; - private int mRealFlags; - private int mRealUserBackground; - private int mRealTop; - private int mRealLeft; - private int mRealBottom; - private int mRealRight; + private boolean large; + private int realFlags; + private int realUserBackground; + private int realTop; + private int realLeft; + private int realBottom; + private int realRight; PSDLayerMaskData(ImageInputStream pInput, int pSize) throws IOException { if (pSize != 20 && pSize != 36) { throw new IIOException("Illegal PSD Layer Mask data size: " + pSize + " (expeced 20 or 36)"); } - mTop = pInput.readInt(); - mLeft = pInput.readInt(); - mBottom = pInput.readInt(); - mRight = pInput.readInt(); + top = pInput.readInt(); + left = pInput.readInt(); + bottom = pInput.readInt(); + right = pInput.readInt(); - mDefaultColor = pInput.readUnsignedByte(); + defaultColor = pInput.readUnsignedByte(); - mFlags = pInput.readUnsignedByte(); + flags = pInput.readUnsignedByte(); if (pSize == 20) { pInput.readShort(); // Pad } else { // TODO: What to make out of this? - mLarge = true; + large = true; - mRealFlags = pInput.readUnsignedByte(); - mRealUserBackground = pInput.readUnsignedByte(); + realFlags = pInput.readUnsignedByte(); + realUserBackground = pInput.readUnsignedByte(); - mRealTop = pInput.readInt(); - mRealLeft = pInput.readInt(); - mRealBottom = pInput.readInt(); - mRealRight = pInput.readInt(); + realTop = pInput.readInt(); + realLeft = pInput.readInt(); + realBottom = pInput.readInt(); + realRight = pInput.readInt(); } } @@ -89,43 +89,43 @@ class PSDLayerMaskData { public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("["); - builder.append("top: ").append(mTop); - builder.append(", left: ").append(mLeft); - builder.append(", bottom: ").append(mBottom); - builder.append(", right: ").append(mRight); - builder.append(", default color: ").append(mDefaultColor); - builder.append(", flags: ").append(Integer.toBinaryString(mFlags)); + builder.append("top: ").append(top); + builder.append(", left: ").append(left); + builder.append(", bottom: ").append(bottom); + builder.append(", right: ").append(right); + builder.append(", default color: ").append(defaultColor); + builder.append(", flags: ").append(Integer.toBinaryString(flags)); // TODO: Maybe the flag bits have oposite order? builder.append(" ("); - if ((mFlags & 0x01) != 0) { + if ((flags & 0x01) != 0) { builder.append("Pos. rel. to layer"); } else { builder.append("Pos. abs."); } - if ((mFlags & 0x02) != 0) { + if ((flags & 0x02) != 0) { builder.append(", Mask disabled"); } else { builder.append(", Mask enabled"); } - if ((mFlags & 0x04) != 0) { + if ((flags & 0x04) != 0) { builder.append(", Invert mask"); } - if ((mFlags & 0x08) != 0) { + if ((flags & 0x08) != 0) { builder.append(", Unknown bit 3"); } - if ((mFlags & 0x10) != 0) { + if ((flags & 0x10) != 0) { builder.append(", Unknown bit 4"); } - if ((mFlags & 0x20) != 0) { + if ((flags & 0x20) != 0) { builder.append(", Unknown bit 5"); } - if ((mFlags & 0x40) != 0) { + if ((flags & 0x40) != 0) { builder.append(", Unknown bit 6"); } - if ((mFlags & 0x80) != 0) { + if ((flags & 0x80) != 0) { builder.append(", Unknown bit 7"); } builder.append(")"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java index d18c80ea..5f0b59fc 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java @@ -10,6 +10,7 @@ import org.w3c.dom.Node; import javax.imageio.metadata.IIOMetadataNode; import java.awt.image.IndexColorModel; +import java.io.IOException; import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -27,12 +28,13 @@ public final class PSDMetadata extends AbstractMetadata { static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0"; static final String NATIVE_METADATA_FORMAT_CLASS_NAME = "com.twelvemonkeys.imageio.plugins.psd.PSDMetadataFormat"; - PSDHeader mHeader; - PSDColorData mColorData; - int mCompression = -1; - List mImageResources; - PSDGlobalLayerMask mGlobalLayerMask; - List mLayerInfo; + PSDHeader header; + PSDColorData colorData; + int compression = -1; + List imageResources; + PSDGlobalLayerMask globalLayerMask; + List layerInfo; + long layersStart; static final String[] COLOR_MODES = { "MONOCHROME", "GRAYSCALE", "INDEXED", "RGB", "CMYK", null, null, "MULTICHANNEL", "DUOTONE", "LAB" @@ -70,11 +72,11 @@ public final class PSDMetadata extends AbstractMetadata { root.appendChild(createHeaderNode()); - if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) { + if (header.mode == PSD.COLOR_MODE_INDEXED) { root.appendChild(createPaletteNode()); } - if (mImageResources != null && !mImageResources.isEmpty()) { + if (imageResources != null && !imageResources.isEmpty()) { root.appendChild(createImageResourcesNode()); } @@ -86,11 +88,11 @@ public final class PSDMetadata extends AbstractMetadata { header.setAttribute("type", "PSD"); header.setAttribute("version", "1"); - header.setAttribute("channels", Integer.toString(mHeader.mChannels)); - header.setAttribute("height", Integer.toString(mHeader.mHeight)); - header.setAttribute("width", Integer.toString(mHeader.mWidth)); - header.setAttribute("bits", Integer.toString(mHeader.mBits)); - header.setAttribute("mode", COLOR_MODES[mHeader.mMode]); + header.setAttribute("channels", Integer.toString(this.header.channels)); + header.setAttribute("height", Integer.toString(this.header.height)); + header.setAttribute("width", Integer.toString(this.header.width)); + header.setAttribute("bits", Integer.toString(this.header.bits)); + header.setAttribute("mode", COLOR_MODES[this.header.mode]); return header; } @@ -99,7 +101,7 @@ public final class PSDMetadata extends AbstractMetadata { IIOMetadataNode resource = new IIOMetadataNode("ImageResources"); IIOMetadataNode node; - for (PSDImageResource imageResource : mImageResources) { + for (PSDImageResource imageResource : imageResources) { // TODO: Always add name (if set) and id (as resourceId) to all nodes? // Resource Id is useful for people with access to the PSD spec.. @@ -128,7 +130,7 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("AlphaChannelInfo"); - for (String name : alphaChannelInfo.mNames) { + for (String name : alphaChannelInfo.names) { IIOMetadataNode nameNode = new IIOMetadataNode("Name"); nameNode.setAttribute("value", name); node.appendChild(nameNode); @@ -138,11 +140,11 @@ public final class PSDMetadata extends AbstractMetadata { PSDDisplayInfo displayInfo = (PSDDisplayInfo) imageResource; node = new IIOMetadataNode("DisplayInfo"); - node.setAttribute("colorSpace", DISPLAY_INFO_CS[displayInfo.mColorSpace]); + node.setAttribute("colorSpace", DISPLAY_INFO_CS[displayInfo.colorSpace]); StringBuilder builder = new StringBuilder(); - for (short color : displayInfo.mColors) { + for (short color : displayInfo.colors) { if (builder.length() > 0) { builder.append(" "); } @@ -151,79 +153,79 @@ public final class PSDMetadata extends AbstractMetadata { } node.setAttribute("colors", builder.toString()); - node.setAttribute("opacity", Integer.toString(displayInfo.mOpacity)); - node.setAttribute("kind", DISPLAY_INFO_KINDS[displayInfo.mKind]); + node.setAttribute("opacity", Integer.toString(displayInfo.opacity)); + node.setAttribute("kind", DISPLAY_INFO_KINDS[displayInfo.kind]); } else if (imageResource instanceof PSDGridAndGuideInfo) { PSDGridAndGuideInfo info = (PSDGridAndGuideInfo) imageResource; node = new IIOMetadataNode("GridAndGuideInfo"); - node.setAttribute("version", String.valueOf(info.mVersion)); - node.setAttribute("verticalGridCycle", String.valueOf(info.mGridCycleVertical)); - node.setAttribute("horizontalGridCycle", String.valueOf(info.mGridCycleHorizontal)); + node.setAttribute("version", String.valueOf(info.version)); + node.setAttribute("verticalGridCycle", String.valueOf(info.gridCycleVertical)); + node.setAttribute("horizontalGridCycle", String.valueOf(info.gridCycleHorizontal)); - for (PSDGridAndGuideInfo.GuideResource guide : info.mGuides) { + for (PSDGridAndGuideInfo.GuideResource guide : info.guides) { IIOMetadataNode guideNode = new IIOMetadataNode("Guide"); - guideNode.setAttribute("location", Integer.toString(guide.mLocation)); - guideNode.setAttribute("orientation", GUIDE_ORIENTATIONS[guide.mDirection]); + guideNode.setAttribute("location", Integer.toString(guide.location)); + guideNode.setAttribute("orientation", GUIDE_ORIENTATIONS[guide.direction]); } } else if (imageResource instanceof PSDPixelAspectRatio) { PSDPixelAspectRatio aspectRatio = (PSDPixelAspectRatio) imageResource; node = new IIOMetadataNode("PixelAspectRatio"); - node.setAttribute("version", String.valueOf(aspectRatio.mVersion)); - node.setAttribute("aspectRatio", String.valueOf(aspectRatio.mAspect)); + node.setAttribute("version", String.valueOf(aspectRatio.version)); + node.setAttribute("aspectRatio", String.valueOf(aspectRatio.aspect)); } else if (imageResource instanceof PSDPrintFlags) { PSDPrintFlags flags = (PSDPrintFlags) imageResource; node = new IIOMetadataNode("PrintFlags"); - node.setAttribute("labels", String.valueOf(flags.mLabels)); - node.setAttribute("cropMarks", String.valueOf(flags.mCropMasks)); - node.setAttribute("colorBars", String.valueOf(flags.mColorBars)); - node.setAttribute("registrationMarks", String.valueOf(flags.mRegistrationMarks)); - node.setAttribute("negative", String.valueOf(flags.mNegative)); - node.setAttribute("flip", String.valueOf(flags.mFlip)); - node.setAttribute("interpolate", String.valueOf(flags.mInterpolate)); - node.setAttribute("caption", String.valueOf(flags.mCaption)); + node.setAttribute("labels", String.valueOf(flags.labels)); + node.setAttribute("cropMarks", String.valueOf(flags.cropMasks)); + node.setAttribute("colorBars", String.valueOf(flags.colorBars)); + node.setAttribute("registrationMarks", String.valueOf(flags.registrationMarks)); + node.setAttribute("negative", String.valueOf(flags.negative)); + node.setAttribute("flip", String.valueOf(flags.flip)); + node.setAttribute("interpolate", String.valueOf(flags.interpolate)); + node.setAttribute("caption", String.valueOf(flags.caption)); } else if (imageResource instanceof PSDPrintFlagsInformation) { PSDPrintFlagsInformation information = (PSDPrintFlagsInformation) imageResource; node = new IIOMetadataNode("PrintFlagsInformation"); - node.setAttribute("version", String.valueOf(information.mVersion)); - node.setAttribute("cropMarks", String.valueOf(information.mCropMasks)); - node.setAttribute("field", String.valueOf(information.mField)); - node.setAttribute("bleedWidth", String.valueOf(information.mBleedWidth)); - node.setAttribute("bleedScale", String.valueOf(information.mBleedScale)); + node.setAttribute("version", String.valueOf(information.version)); + node.setAttribute("cropMarks", String.valueOf(information.cropMasks)); + node.setAttribute("field", String.valueOf(information.field)); + node.setAttribute("bleedWidth", String.valueOf(information.bleedWidth)); + node.setAttribute("bleedScale", String.valueOf(information.bleedScale)); } else if (imageResource instanceof PSDPrintScale) { PSDPrintScale printScale = (PSDPrintScale) imageResource; node = new IIOMetadataNode("PrintScale"); - node.setAttribute("style", PRINT_SCALE_STYLES[printScale.mStyle]); - node.setAttribute("xLocation", String.valueOf(printScale.mXLocation)); - node.setAttribute("yLocation", String.valueOf(printScale.mYlocation)); - node.setAttribute("scale", String.valueOf(printScale.mScale)); + node.setAttribute("style", PRINT_SCALE_STYLES[printScale.style]); + node.setAttribute("xLocation", String.valueOf(printScale.xLocation)); + node.setAttribute("yLocation", String.valueOf(printScale.ylocation)); + node.setAttribute("scale", String.valueOf(printScale.scale)); } else if (imageResource instanceof PSDResolutionInfo) { PSDResolutionInfo information = (PSDResolutionInfo) imageResource; node = new IIOMetadataNode("ResolutionInfo"); - node.setAttribute("horizontalResolution", String.valueOf(information.mHRes)); - node.setAttribute("horizontalResolutionUnit", RESOLUTION_UNITS[information.mHResUnit]); - node.setAttribute("widthUnit", DIMENSION_UNITS[information.mWidthUnit]); - node.setAttribute("verticalResolution", String.valueOf(information.mVRes)); - node.setAttribute("verticalResolutionUnit", RESOLUTION_UNITS[information.mVResUnit]); - node.setAttribute("heightUnit", DIMENSION_UNITS[information.mHeightUnit]); + node.setAttribute("horizontalResolution", String.valueOf(information.hRes)); + node.setAttribute("horizontalResolutionUnit", RESOLUTION_UNITS[information.hResUnit]); + node.setAttribute("widthUnit", DIMENSION_UNITS[information.widthUnit]); + node.setAttribute("verticalResolution", String.valueOf(information.vRes)); + node.setAttribute("verticalResolutionUnit", RESOLUTION_UNITS[information.vResUnit]); + node.setAttribute("heightUnit", DIMENSION_UNITS[information.heightUnit]); } else if (imageResource instanceof PSDUnicodeAlphaNames) { PSDUnicodeAlphaNames alphaNames = (PSDUnicodeAlphaNames) imageResource; node = new IIOMetadataNode("UnicodeAlphaNames"); - for (String name : alphaNames.mNames) { + for (String name : alphaNames.names) { IIOMetadataNode nameNode = new IIOMetadataNode("Name"); nameNode.setAttribute("value", name); node.appendChild(nameNode); @@ -233,19 +235,25 @@ public final class PSDMetadata extends AbstractMetadata { PSDVersionInfo information = (PSDVersionInfo) imageResource; node = new IIOMetadataNode("VersionInfo"); - node.setAttribute("version", String.valueOf(information.mVersion)); - node.setAttribute("hasRealMergedData", String.valueOf(information.mHasRealMergedData)); - node.setAttribute("writer", information.mWriter); - node.setAttribute("reader", information.mReader); - node.setAttribute("fileVersion", String.valueOf(information.mFileVersion)); + node.setAttribute("version", String.valueOf(information.version)); + node.setAttribute("hasRealMergedData", String.valueOf(information.hasRealMergedData)); + node.setAttribute("writer", information.writer); + node.setAttribute("reader", information.reader); + node.setAttribute("fileVersion", String.valueOf(information.fileVersion)); } else if (imageResource instanceof PSDThumbnail) { - // TODO: Revise/rethink this... - PSDThumbnail thumbnail = (PSDThumbnail) imageResource; + try { + // TODO: Revise/rethink this... + PSDThumbnail thumbnail = (PSDThumbnail) imageResource; - node = new IIOMetadataNode("Thumbnail"); - // TODO: Thumbnail attributes + access to data, to avoid JPEG re-compression problems - node.setUserObject(thumbnail.getThumbnail()); + node = new IIOMetadataNode("Thumbnail"); + // TODO: Thumbnail attributes + access to data, to avoid JPEG re-compression problems + node.setUserObject(thumbnail.getThumbnail()); + } + catch (IOException e) { + // TODO: Warning + continue; + } } else if (imageResource instanceof PSDIPTCData) { // TODO: Revise/rethink this... @@ -253,9 +261,9 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("DirectoryResource"); node.setAttribute("type", "IPTC"); - node.setUserObject(iptc.mDirectory); + node.setUserObject(iptc.directory); - appendEntries(node, "IPTC", iptc.mDirectory); + appendEntries(node, "IPTC", iptc.directory); } else if (imageResource instanceof PSDEXIF1Data) { // TODO: Revise/rethink this... @@ -264,9 +272,9 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("DirectoryResource"); node.setAttribute("type", "EXIF"); // TODO: Set byte[] data instead - node.setUserObject(exif.mDirectory); + node.setUserObject(exif.directory); - appendEntries(node, "EXIF", exif.mDirectory); + appendEntries(node, "EXIF", exif.directory); } else if (imageResource instanceof PSDXMPData) { // TODO: Revise/rethink this... Would it be possible to parse XMP as IIOMetadataNodes? Or is that just stupid... @@ -275,25 +283,25 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("DirectoryResource"); node.setAttribute("type", "XMP"); - appendEntries(node, "XMP", xmp.mDirectory); + appendEntries(node, "XMP", xmp.directory); // Set the entire XMP document as user data - node.setUserObject(xmp.mData); + node.setUserObject(xmp.data); } else { // Generic resource.. node = new IIOMetadataNode("ImageResource"); - String value = PSDImageResource.resourceTypeForId(imageResource.mId); + String value = PSDImageResource.resourceTypeForId(imageResource.id); if (!"UnknownResource".equals(value)) { node.setAttribute("name", value); } - node.setAttribute("length", String.valueOf(imageResource.mSize)); + node.setAttribute("length", String.valueOf(imageResource.size)); // TODO: Set user object: byte array } // TODO: More resources - node.setAttribute("resourceId", String.format("0x%04x", imageResource.mId)); + node.setAttribute("resourceId", String.format("0x%04x", imageResource.id)); resource.appendChild(node); } @@ -304,8 +312,8 @@ public final class PSDMetadata extends AbstractMetadata { return resource; } - private void appendEntries(final IIOMetadataNode pNode, final String pType, final Directory pDirectory) { - for (Entry entry : pDirectory) { + private void appendEntries(final IIOMetadataNode node, final String type, final Directory directory) { + for (Entry entry : directory) { Object tagId = entry.getIdentifier(); IIOMetadataNode tag = new IIOMetadataNode("Entry"); @@ -316,13 +324,13 @@ public final class PSDMetadata extends AbstractMetadata { tag.setAttribute("field", String.format("%s", field)); } else { - if ("IPTC".equals(pType)) { + if ("IPTC".equals(type)) { tag.setAttribute("field", String.format("%s:%s", (Integer) tagId >> 8, (Integer) tagId & 0xff)); } } if (entry.getValue() instanceof Directory) { - appendEntries(tag, pType, (Directory) entry.getValue()); + appendEntries(tag, type, (Directory) entry.getValue()); tag.setAttribute("type", "Directory"); } else { @@ -330,7 +338,7 @@ public final class PSDMetadata extends AbstractMetadata { tag.setAttribute("type", entry.getTypeName()); } - pNode.appendChild(tag); + node.appendChild(tag); } } @@ -343,7 +351,7 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("ColorSpaceType"); String cs; - switch (mHeader.mMode) { + switch (header.mode) { case PSD.COLOR_MODE_MONOCHROME: case PSD.COLOR_MODE_GRAYSCALE: case PSD.COLOR_MODE_DUOTONE: // Rationale: Spec says treat as gray... @@ -357,7 +365,7 @@ public final class PSDMetadata extends AbstractMetadata { cs = "CMYK"; break; case PSD.COLOR_MODE_MULTICHANNEL: - cs = getMultiChannelCS(mHeader.mChannels); + cs = getMultiChannelCS(header.channels); break; case PSD.COLOR_MODE_LAB: cs = "Lab"; @@ -370,7 +378,7 @@ public final class PSDMetadata extends AbstractMetadata { // TODO: Channels might be 5 for RGB + A + Mask... Probably not correct node = new IIOMetadataNode("NumChannels"); - node.setAttribute("value", Integer.toString(mHeader.mChannels)); + node.setAttribute("value", Integer.toString(header.channels)); chroma_node.appendChild(node); // TODO: Check if this is correct with bitmap (monchrome) @@ -378,7 +386,7 @@ public final class PSDMetadata extends AbstractMetadata { node.setAttribute("value", "true"); chroma_node.appendChild(node); - if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) { + if (header.mode == PSD.COLOR_MODE_INDEXED) { node = createPaletteNode(); chroma_node.appendChild(node); } @@ -411,7 +419,7 @@ public final class PSDMetadata extends AbstractMetadata { private IIOMetadataNode createPaletteNode() { IIOMetadataNode node = new IIOMetadataNode("Palette"); - IndexColorModel cm = mColorData.getIndexColorModel(); + IndexColorModel cm = colorData.getIndexColorModel(); for (int i = 0; i < cm.getMapSize(); i++) { IIOMetadataNode entry = new IIOMetadataNode("PaletteEntry"); @@ -426,9 +434,9 @@ public final class PSDMetadata extends AbstractMetadata { return node; } - private String getMultiChannelCS(short pChannels) { - if (pChannels < 16) { - return String.format("%xCLR", pChannels); + private String getMultiChannelCS(short channels) { + if (channels < 16) { + return String.format("%xCLR", channels); } throw new UnsupportedOperationException("Standard meta data format does not support more than 15 channels"); @@ -442,7 +450,7 @@ public final class PSDMetadata extends AbstractMetadata { node = new IIOMetadataNode("CompressionTypeName"); String compression; - switch (mCompression) { + switch (this.compression) { case PSD.COMPRESSION_NONE: compression = "none"; break; @@ -478,13 +486,13 @@ public final class PSDMetadata extends AbstractMetadata { dataNode.appendChild(node); node = new IIOMetadataNode("SampleFormat"); - node.setAttribute("value", mHeader.mMode == PSD.COLOR_MODE_INDEXED ? "Index" : "UnsignedIntegral"); + node.setAttribute("value", header.mode == PSD.COLOR_MODE_INDEXED ? "Index" : "UnsignedIntegral"); dataNode.appendChild(node); - String bitDepth = Integer.toString(mHeader.mBits); // bits per plane + String bitDepth = Integer.toString(header.bits); // bits per plane // TODO: Channels might be 5 for RGB + A + Mask... - String[] bps = new String[mHeader.mChannels]; + String[] bps = new String[header.channels]; Arrays.fill(bps, bitDepth); node = new IIOMetadataNode("BitsPerSample"); @@ -509,7 +517,7 @@ public final class PSDMetadata extends AbstractMetadata { Iterator ratios = getResources(PSDPixelAspectRatio.class); if (ratios.hasNext()) { PSDPixelAspectRatio ratio = ratios.next(); - aspect = (float) ratio.mAspect; + aspect = (float) ratio.aspect; } node.setAttribute("value", Float.toString(aspect)); @@ -525,11 +533,11 @@ public final class PSDMetadata extends AbstractMetadata { PSDResolutionInfo resolutionInfo = resolutionInfos.next(); node = new IIOMetadataNode("HorizontalPixelSize"); - node.setAttribute("value", Float.toString(asMM(resolutionInfo.mHResUnit, resolutionInfo.mHRes))); + node.setAttribute("value", Float.toString(asMM(resolutionInfo.hResUnit, resolutionInfo.hRes))); dimensionNode.appendChild(node); node = new IIOMetadataNode("VerticalPixelSize"); - node.setAttribute("value", Float.toString(asMM(resolutionInfo.mVResUnit, resolutionInfo.mVRes))); + node.setAttribute("value", Float.toString(asMM(resolutionInfo.vResUnit, resolutionInfo.vRes))); dimensionNode.appendChild(node); } @@ -563,9 +571,9 @@ public final class PSDMetadata extends AbstractMetadata { return dimensionNode; } - private static float asMM(final short pUnit, final float pResolution) { + private static float asMM(final short unit, final float resolution) { // Unit: 1 -> pixels per inch, 2 -> pixels pr cm - return (pUnit == 1 ? 25.4f : 10) / pResolution; + return (unit == 1 ? 25.4f : 10) / resolution; } @Override @@ -583,7 +591,7 @@ public final class PSDMetadata extends AbstractMetadata { PSDEXIF1Data data = exif.next(); // Get the EXIF DateTime (aka ModifyDate) tag if present - Entry dateTime = data.mDirectory.getEntryById(TIFF.TAG_DATE_TIME); + Entry dateTime = data.directory.getEntryById(TIFF.TAG_DATE_TIME); if (dateTime != null) { node = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime // Format: "YYYY:MM:DD hh:mm:ss" @@ -629,7 +637,7 @@ public final class PSDMetadata extends AbstractMetadata { if (textResource instanceof PSDIPTCData) { PSDIPTCData iptc = (PSDIPTCData) textResource; - appendTextEntriesFlat(text, iptc.mDirectory, new FilterIterator.Filter() { + appendTextEntriesFlat(text, iptc.directory, new FilterIterator.Filter() { public boolean accept(final Entry pEntry) { Integer tagId = (Integer) pEntry.getIdentifier(); @@ -645,7 +653,7 @@ public final class PSDMetadata extends AbstractMetadata { else if (textResource instanceof PSDEXIF1Data) { PSDEXIF1Data exif = (PSDEXIF1Data) textResource; - appendTextEntriesFlat(text, exif.mDirectory, new FilterIterator.Filter() { + appendTextEntriesFlat(text, exif.directory, new FilterIterator.Filter() { public boolean accept(final Entry pEntry) { Integer tagId = (Integer) pEntry.getIdentifier(); @@ -662,7 +670,7 @@ public final class PSDMetadata extends AbstractMetadata { } else if (textResource instanceof PSDXMPData) { // TODO: Parse XMP (heavy) ONLY if we don't have required fields from IPTC/EXIF? - // TODO: Use XMP IPTC/EXIF/TIFFF NativeDigest field to validate if the values are in sync... + // TODO: Use XMP IPTC/EXIF/TIFF NativeDigest field to validate if the values are in sync..? PSDXMPData xmp = (PSDXMPData) textResource; } } @@ -670,13 +678,13 @@ public final class PSDMetadata extends AbstractMetadata { return text; } - private void appendTextEntriesFlat(final IIOMetadataNode pNode, final Directory pDirectory, final FilterIterator.Filter pFilter) { - FilterIterator pEntries = new FilterIterator(pDirectory.iterator(), pFilter); + private void appendTextEntriesFlat(final IIOMetadataNode node, final Directory directory, final FilterIterator.Filter filter) { + FilterIterator pEntries = new FilterIterator(directory.iterator(), filter); while (pEntries.hasNext()) { Entry entry = pEntries.next(); if (entry.getValue() instanceof Directory) { - appendTextEntriesFlat(pNode, (Directory) entry.getValue(), pFilter); + appendTextEntriesFlat(node, (Directory) entry.getValue(), filter); } else if (entry.getValue() instanceof String) { IIOMetadataNode tag = new IIOMetadataNode("TextEntry"); @@ -691,7 +699,7 @@ public final class PSDMetadata extends AbstractMetadata { } tag.setAttribute("value", entry.getValueAsString()); - pNode.appendChild(tag); + node.appendChild(tag); } } } @@ -714,29 +722,29 @@ public final class PSDMetadata extends AbstractMetadata { } private boolean hasAlpha() { - return mHeader.mMode == PSD.COLOR_MODE_RGB && mHeader.mChannels >= 4 || - mHeader.mMode == PSD.COLOR_MODE_CMYK & mHeader.mChannels >= 5; + return header.mode == PSD.COLOR_MODE_RGB && header.channels >= 4 || + header.mode == PSD.COLOR_MODE_CMYK & header.channels >= 5; } - Iterator getResources(final Class pResourceType) { + Iterator getResources(final Class resourceType) { // NOTE: The cast here is wrong, strictly speaking, but it does not matter... @SuppressWarnings({"unchecked"}) - Iterator iterator = (Iterator) mImageResources.iterator(); + Iterator iterator = (Iterator) imageResources.iterator(); return new FilterIterator(iterator, new FilterIterator.Filter() { public boolean accept(final T pElement) { - return pResourceType.isInstance(pElement); + return resourceType.isInstance(pElement); } }); } - Iterator getResources(final int... pResourceTypes) { - Iterator iterator = mImageResources.iterator(); + Iterator getResources(final int... resourceTypes) { + Iterator iterator = imageResources.iterator(); return new FilterIterator(iterator, new FilterIterator.Filter() { public boolean accept(final PSDImageResource pResource) { - for (int type : pResourceTypes) { - if (type == pResource.mId) { + for (int type : resourceTypes) { + if (type == pResource.id) { return true; } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java index b6a78cb6..4d19eec9 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java @@ -195,7 +195,7 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { @Override - public boolean canNodeAppear(final String pElementName, final ImageTypeSpecifier pImageType) { + public boolean canNodeAppear(final String elementName, final ImageTypeSpecifier imageType) { // TODO: PSDColorData and PaletteEntry only for indexed color model throw new UnsupportedOperationException("Method canNodeAppear not implemented"); // TODO: Implement } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPixelAspectRatio.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPixelAspectRatio.java index 612e309a..fd09b4bf 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPixelAspectRatio.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPixelAspectRatio.java @@ -12,8 +12,8 @@ import java.io.IOException; */ final class PSDPixelAspectRatio extends PSDImageResource { // 4 bytes (version = 1), 8 bytes double, x / y of a pixel - int mVersion; - double mAspect; + int version; + double aspect; PSDPixelAspectRatio(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -21,7 +21,7 @@ final class PSDPixelAspectRatio extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mVersion = pInput.readInt(); - mAspect = pInput.readDouble(); + version = pInput.readInt(); + aspect = pInput.readDouble(); } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlags.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlags.java index 606fd156..9d054b52 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlags.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlags.java @@ -11,14 +11,14 @@ import java.io.IOException; * @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$ */ final class PSDPrintFlags extends PSDImageResource { - boolean mLabels; - boolean mCropMasks; - boolean mColorBars; - boolean mRegistrationMarks; - boolean mNegative; - boolean mFlip; - boolean mInterpolate; - boolean mCaption; + boolean labels; + boolean cropMasks; + boolean colorBars; + boolean registrationMarks; + boolean negative; + boolean flip; + boolean interpolate; + boolean caption; PSDPrintFlags(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -26,30 +26,30 @@ final class PSDPrintFlags extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mLabels = pInput.readBoolean(); - mCropMasks = pInput.readBoolean(); - mColorBars = pInput.readBoolean(); - mRegistrationMarks = pInput.readBoolean(); - mNegative = pInput.readBoolean(); - mFlip = pInput.readBoolean(); - mInterpolate = pInput.readBoolean(); - mCaption = pInput.readBoolean(); + labels = pInput.readBoolean(); + cropMasks = pInput.readBoolean(); + colorBars = pInput.readBoolean(); + registrationMarks = pInput.readBoolean(); + negative = pInput.readBoolean(); + flip = pInput.readBoolean(); + interpolate = pInput.readBoolean(); + caption = pInput.readBoolean(); - pInput.skipBytes(mSize - 8); + pInput.skipBytes(size - 8); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", labels: ").append(mLabels); - builder.append(", crop masks: ").append(mCropMasks); - builder.append(", color bars: ").append(mColorBars); - builder.append(", registration marks: ").append(mRegistrationMarks); - builder.append(", negative: ").append(mNegative); - builder.append(", flip: ").append(mFlip); - builder.append(", interpolate: ").append(mInterpolate); - builder.append(", caption: ").append(mCaption); + builder.append(", labels: ").append(labels); + builder.append(", crop masks: ").append(cropMasks); + builder.append(", color bars: ").append(colorBars); + builder.append(", registration marks: ").append(registrationMarks); + builder.append(", negative: ").append(negative); + builder.append(", flip: ").append(flip); + builder.append(", interpolate: ").append(interpolate); + builder.append(", caption: ").append(caption); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlagsInformation.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlagsInformation.java index d5de5eb2..c63c8811 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlagsInformation.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintFlagsInformation.java @@ -11,11 +11,11 @@ import java.io.IOException; * @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$ */ final class PSDPrintFlagsInformation extends PSDImageResource { - int mVersion; - boolean mCropMasks; - int mField; - long mBleedWidth; - int mBleedScale; + int version; + boolean cropMasks; + int field; + long bleedWidth; + int bleedScale; PSDPrintFlagsInformation(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -23,24 +23,24 @@ final class PSDPrintFlagsInformation extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mVersion = pInput.readUnsignedShort(); - mCropMasks = pInput.readBoolean(); - mField = pInput.readUnsignedByte(); // TODO: Is this really pad? - mBleedWidth = pInput.readUnsignedInt(); - mBleedScale = pInput.readUnsignedShort(); + version = pInput.readUnsignedShort(); + cropMasks = pInput.readBoolean(); + field = pInput.readUnsignedByte(); // TODO: Is this really pad? + bleedWidth = pInput.readUnsignedInt(); + bleedScale = pInput.readUnsignedShort(); - pInput.skipBytes(mSize - 10); + pInput.skipBytes(size - 10); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", version: ").append(mVersion); - builder.append(", crop masks: ").append(mCropMasks); - builder.append(", field: ").append(mField); - builder.append(", bleed width: ").append(mBleedWidth); - builder.append(", bleed scale: ").append(mBleedScale); + builder.append(", version: ").append(version); + builder.append(", crop masks: ").append(cropMasks); + builder.append(", field: ").append(field); + builder.append(", bleed width: ").append(bleedWidth); + builder.append(", bleed scale: ").append(bleedScale); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintScale.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintScale.java index 35d14b1f..e89e5613 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintScale.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDPrintScale.java @@ -16,10 +16,10 @@ final class PSDPrintScale extends PSDImageResource { // 4 bytes y location (floating point). // 4 bytes scale (floating point) - short mStyle; - float mXLocation; - float mYlocation; - float mScale; + short style; + float xLocation; + float ylocation; + float scale; PSDPrintScale(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -27,9 +27,9 @@ final class PSDPrintScale extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mStyle = pInput.readShort(); - mXLocation = pInput.readFloat(); - mYlocation = pInput.readFloat(); - mScale = pInput.readFloat(); + style = pInput.readShort(); + xLocation = pInput.readFloat(); + ylocation = pInput.readFloat(); + scale = pInput.readFloat(); } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDResolutionInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDResolutionInfo.java index 629f3a9c..f3943f73 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDResolutionInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDResolutionInfo.java @@ -50,12 +50,12 @@ class PSDResolutionInfo extends PSDImageResource { // WORD HeightUnit; /* 1=in, 2=cm, 3=pt, 4=picas, 5=columns */ // } RESOLUTIONINFO; - float mHRes; - short mHResUnit; - short mWidthUnit; - float mVRes; - short mVResUnit; - short mHeightUnit; + float hRes; + short hResUnit; + short widthUnit; + float vRes; + short vResUnit; + short heightUnit; PSDResolutionInfo(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -63,32 +63,32 @@ class PSDResolutionInfo extends PSDImageResource { @Override protected void readData(ImageInputStream pInput) throws IOException { - if (mSize != 16) { - throw new IIOException("Resolution info length expected to be 16: " + mSize); + if (size != 16) { + throw new IIOException("Resolution info length expected to be 16: " + size); } - mHRes = PSDUtil.fixedPointToFloat(pInput.readInt()); - mHResUnit = pInput.readShort(); - mWidthUnit = pInput.readShort(); - mVRes = PSDUtil.fixedPointToFloat(pInput.readInt()); - mVResUnit = pInput.readShort(); - mHeightUnit = pInput.readShort(); + hRes = PSDUtil.fixedPointToFloat(pInput.readInt()); + hResUnit = pInput.readShort(); + widthUnit = pInput.readShort(); + vRes = PSDUtil.fixedPointToFloat(pInput.readInt()); + vResUnit = pInput.readShort(); + heightUnit = pInput.readShort(); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", hRes: ").append(mHRes); + builder.append(", hRes: ").append(hRes); builder.append(" "); - builder.append(resUnit(mHResUnit)); + builder.append(resUnit(hResUnit)); builder.append(", width unit: "); - builder.append(dimUnit(mWidthUnit)); - builder.append(", vRes: ").append(mVRes); + builder.append(dimUnit(widthUnit)); + builder.append(", vRes: ").append(vRes); builder.append(" "); - builder.append(resUnit(mVResUnit)); + builder.append(resUnit(vResUnit)); builder.append(", height unit: "); - builder.append(dimUnit(mHeightUnit)); + builder.append(dimUnit(heightUnit)); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDThumbnail.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDThumbnail.java index be49a19f..31c2352f 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDThumbnail.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDThumbnail.java @@ -1,11 +1,12 @@ package com.twelvemonkeys.imageio.plugins.psd; -import com.twelvemonkeys.imageio.util.IIOUtil; - import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; -import java.awt.image.BufferedImage; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.io.ByteArrayInputStream; import java.io.IOException; /** @@ -16,9 +17,11 @@ import java.io.IOException; * @version $Id: PSDThumbnail.java,v 1.0 Jul 29, 2009 4:41:06 PM haraldk Exp$ */ class PSDThumbnail extends PSDImageResource { - private BufferedImage mThumbnail; - private int mWidth; - private int mHeight; + private int format; + private int width; + private int height; + private int widthBytes; + private byte[] data; public PSDThumbnail(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -37,29 +40,18 @@ class PSDThumbnail extends PSDImageResource { */ @Override protected void readData(final ImageInputStream pInput) throws IOException { - // TODO: Support for RAW RGB (format == 0): Extract RAW reader from PICT RAW QuickTime decompressor - int format = pInput.readInt(); - switch (format) { - case 0: - // RAW RGB - throw new IIOException("RAW RGB format thumbnail not supported yet"); - case 1: - // JPEG - break; - default: - throw new IIOException(String.format("Unsupported thumbnail format (%s) in PSD document", format)); - } + format = pInput.readInt(); - mWidth = pInput.readInt(); - mHeight = pInput.readInt(); + width = pInput.readInt(); + height = pInput.readInt(); // This data isn't really useful, unless we're dealing with raw bytes - int widthBytes = pInput.readInt(); - int totalSize = pInput.readInt(); + widthBytes = pInput.readInt(); + int totalSize = pInput.readInt(); // Hmm.. Is this really useful at all? // Consistency check int sizeCompressed = pInput.readInt(); - if (sizeCompressed != (mSize - 28)) { + if (sizeCompressed != (size - 28)) { throw new IIOException("Corrupt thumbnail in PSD document"); } @@ -70,28 +62,65 @@ class PSDThumbnail extends PSDImageResource { // TODO: Warning/Exception } - // TODO: Defer decoding until getThumbnail? - // TODO: Support BGR if id == RES_THUMBNAIL_PS4? Or is that already supported in the JPEG? - mThumbnail = ImageIO.read(IIOUtil.createStreamAdapter(pInput, sizeCompressed)); + data = new byte[sizeCompressed]; + pInput.readFully(data); + } + + BufferedImage imageFromRawData(int width, int height, int scanLine, byte[] data) { + DataBuffer buffer = new DataBufferByte(data, data.length); + WritableRaster raster = Raster.createInterleavedRaster( + buffer, width, height, + scanLine, 3, + new int[]{0, 1, 2}, + null + ); + ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + + return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); } public final int getWidth() { - return mWidth; + return width; } public final int getHeight() { - return mHeight; + return height; } - public final BufferedImage getThumbnail() { - return mThumbnail; + public final BufferedImage getThumbnail() throws IOException { + switch (format) { + case 0: + // RAW RGB + return imageFromRawData(width, height, widthBytes, data.clone()); // Clone data, as image is mutable + case 1: + // JPEG + // TODO: Support BGR if id == RES_THUMBNAIL_PS4? Or is that already supported in the JPEG reader? + return ImageIO.read(new ByteArrayInputStream(data)); + default: + throw new IIOException(String.format("Unsupported thumbnail format (%s) in PSD document", format)); + } } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", ").append(mThumbnail); + builder.append(", format: "); + switch (format) { + case 0: + // RAW RGB + builder.append("RAW RGB"); + break; + case 1: + // JPEG + builder.append("JPEG"); + break; + default: + builder.append("Unknown"); + break; + } + + builder.append(", size: ").append(data != null ? data.length : -1); builder.append("]"); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java index 6901a8a3..21f752c1 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java @@ -13,7 +13,7 @@ import java.util.List; * @version $Id: PSDUnicodeAlphaNames.java,v 1.0 Nov 7, 2009 9:16:56 PM haraldk Exp$ */ final class PSDUnicodeAlphaNames extends PSDImageResource { - List mNames; + List names; PSDUnicodeAlphaNames(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -21,12 +21,12 @@ final class PSDUnicodeAlphaNames extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mNames = new ArrayList(); + names = new ArrayList(); - long left = mSize; + long left = size; while (left > 0) { String name = PSDUtil.readUnicodeString(pInput); - mNames.add(name); + names.add(name); left -= name.length() * 2 + 4; } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java index 5e1e7ea2..10210ce8 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java @@ -12,11 +12,11 @@ import java.io.IOException; */ final class PSDVersionInfo extends PSDImageResource { - int mVersion; - boolean mHasRealMergedData; - String mWriter; - String mReader; - int mFileVersion; + int version; + boolean hasRealMergedData; + String writer; + String reader; + int fileVersion; PSDVersionInfo(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -32,24 +32,24 @@ final class PSDVersionInfo extends PSDImageResource { 4 bytes file version. */ - mVersion = pInput.readInt(); - mHasRealMergedData = pInput.readBoolean(); + version = pInput.readInt(); + hasRealMergedData = pInput.readBoolean(); - mWriter = PSDUtil.readUnicodeString(pInput); - mReader = PSDUtil.readUnicodeString(pInput); + writer = PSDUtil.readUnicodeString(pInput); + reader = PSDUtil.readUnicodeString(pInput); - mFileVersion = pInput.readInt(); + fileVersion = pInput.readInt(); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - builder.append(", version: ").append(mVersion); - builder.append(", hasRealMergedData: ").append(mHasRealMergedData); - builder.append(", writer: ").append(mWriter); - builder.append(", reader: ").append(mReader); - builder.append(", file version: ").append(mFileVersion); + builder.append(", version: ").append(version); + builder.append(", hasRealMergedData: ").append(hasRealMergedData); + builder.append(", writer: ").append(writer); + builder.append(", reader: ").append(reader); + builder.append(", file version: ").append(fileVersion); builder.append("]"); return builder.toString(); diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java index 605e78ef..2e14bc31 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDXMPData.java @@ -2,13 +2,11 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.lang.StringUtil; import javax.imageio.stream.ImageInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.*; import java.nio.charset.Charset; /** @@ -22,8 +20,8 @@ import java.nio.charset.Charset; * @see Adobe XMP Developer Center */ final class PSDXMPData extends PSDImageResource { - protected byte[] mData; - Directory mDirectory; + protected byte[] data; + Directory directory; PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -31,21 +29,29 @@ final class PSDXMPData extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mData = new byte[(int) mSize]; // TODO: Fix potential overflow, or document why that can't happen (read spec) - //pInput.readFully(mData); + data = new byte[(int) size]; // TODO: Fix potential overflow, or document why that can't happen (read spec) + pInput.readFully(data); - mDirectory = new XMPReader().read(pInput); + // Chop off potential trailing null-termination/padding that SAX parsers don't like... + int len = data.length; + for (; len > 0; len--) { + if (data[len - 1] != 0) { + break; + } + } + + directory = new XMPReader().read(new ByteArrayImageInputStream(data, 0, len)); } @Override public String toString() { StringBuilder builder = toStringBuilder(); - int length = Math.min(256, mData.length); - String data = StringUtil.decode(mData, 0, length, "UTF-8").replace('\n', ' ').replaceAll("\\s+", " "); + int length = Math.min(256, data.length); + String data = StringUtil.decode(this.data, 0, length, "UTF-8").replace('\n', ' ').replaceAll("\\s+", " "); builder.append(", data: \"").append(data); - if (length < mData.length) { + if (length < this.data.length) { builder.append("..."); } @@ -60,6 +66,6 @@ final class PSDXMPData extends PSDImageResource { * @return the XMP metadata. */ public Reader getData() { - return new InputStreamReader(new ByteArrayInputStream(mData), Charset.forName("UTF-8")); + return new InputStreamReader(new ByteArrayInputStream(data), Charset.forName("UTF-8")); } } diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/YCbCrColorSpace.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/YCbCrColorSpace.java deleted file mode 100755 index d0d020f1..00000000 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/YCbCrColorSpace.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.imageio.plugins.psd; - -import java.awt.color.ColorSpace; - -/** - * YCbCrColorSpace - * - * @author Harald Kuhr - * @author last modified by $Author: haraldk$ - * @version $Id: YCbCrColorSpace.java,v 1.0 Jun 28, 2008 3:30:50 PM haraldk Exp$ - */ -// TODO: Move to com.twlevemonkeys.image? -// TODO: Read an ICC YCbCr profile from classpath resource? Is there such a thing? -final class YCbCrColorSpace extends ColorSpace { - - static final ColorSpace INSTANCE = new CMYKColorSpace(); - final ColorSpace sRGB = getInstance(CS_sRGB); - - YCbCrColorSpace() { - super(ColorSpace.TYPE_YCbCr, 3); - } - - public static ColorSpace getInstance() { - return INSTANCE; - } - - // http://www.w3.org/Graphics/JPEG/jfif.txt - /* - Conversion to and from RGB - - Y, Cb, and Cr are converted from R, G, and B as defined in CCIR Recommendation 601 - but are normalized so as to occupy the full 256 levels of a 8-bit binary encoding. More - precisely: - - Y = 256 * E'y - Cb = 256 * [ E'Cb ] + 128 - Cr = 256 * [ E'Cr ] + 128 - - where the E'y, E'Cb and E'Cb are defined as in CCIR 601. Since values of E'y have a - range of 0 to 1.0 and those for E'Cb and E'Cr have a range of -0.5 to +0.5, Y, Cb, and Cr - must be clamped to 255 when they are maximum value. - - RGB to YCbCr Conversion - - YCbCr (256 levels) can be computed directly from 8-bit RGB as follows: - - Y = 0.299 R + 0.587 G + 0.114 B - Cb = - 0.1687 R - 0.3313 G + 0.5 B + 128 - Cr = 0.5 R - 0.4187 G - 0.0813 B + 128 - - NOTE - Not all image file formats store image samples in the order R0, G0, - B0, ... Rn, Gn, Bn. Be sure to verify the sample order before converting an - RGB file to JFIF. - - YCbCr to RGB Conversion - - RGB can be computed directly from YCbCr (256 levels) as follows: - - R = Y + 1.402 (Cr-128) - G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) - B = Y + 1.772 (Cb-128) - */ - public float[] toRGB(float[] colorvalue) { -// R = Y + 1.402 (Cr-128) -// G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) -// B = Y + 1.772 (Cb-128) - return new float[] { - colorvalue[0] + 1.402f * (colorvalue[2] - .5f), - colorvalue[0] - 0.34414f * (colorvalue[1] - .5f) - 0.71414f * (colorvalue[2] - .5f), - colorvalue[0] + 1.772f * (colorvalue[1] - .5f), - }; - // TODO: Convert via CIEXYZ space using sRGB space, as suggested in docs - // return sRGB.fromCIEXYZ(toCIEXYZ(colorvalue)); - } - - public float[] fromRGB(float[] rgbvalue) { -// Y = 0.299 R + 0.587 G + 0.114 B -// Cb = - 0.1687 R - 0.3313 G + 0.5 B + 128 -// Cr = 0.5 R - 0.4187 G - 0.0813 B + 128 - return new float[] { - 0.299f * rgbvalue[0] + 0.587f * rgbvalue[1] + 0.114f * rgbvalue[2], - -0.1687f * rgbvalue[0] - 0.3313f * rgbvalue[1] + 0.5f * rgbvalue[2] + .5f, - 0.5f * rgbvalue[0] - 0.4187f * rgbvalue[1] - 0.0813f * rgbvalue[2] + .5f - }; - } - - public float[] toCIEXYZ(float[] colorvalue) { - throw new UnsupportedOperationException("Method toCIEXYZ not implemented"); // TODO: Implement - } - - public float[] fromCIEXYZ(float[] colorvalue) { - throw new UnsupportedOperationException("Method fromCIEXYZ not implemented"); // TODO: Implement - } -} diff --git a/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi index 21a911af..002f3d14 100755 --- a/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi +++ b/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -1 +1 @@ -com.twelvemonkeys.imageio.plugins.psd.PSDImageReaderSpi \ No newline at end of file +com.twelvemonkeys.imageio.plugins.psd.PSDImageReaderSpi diff --git a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java index 8d65764a..6414ef5c 100644 --- a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java +++ b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTestCase.java @@ -2,16 +2,22 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; import com.twelvemonkeys.imageio.util.ProgressListenerBase; +import org.junit.Test; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.ImageReader; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.ArrayList; import java.io.IOException; +import static org.junit.Assert.*; + /** * PSDImageReaderTestCase * @@ -21,7 +27,7 @@ import java.io.IOException; */ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase { - static ImageReaderSpi sProvider = new PSDImageReaderSpi(); + static ImageReaderSpi provider = new PSDImageReaderSpi(); protected List getTestData() { return Arrays.asList( @@ -49,12 +55,12 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase getReaderClass() { @@ -133,8 +139,10 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase types = imageReader.getImageTypes(i); + + assertNotNull(types); + assertTrue(types.hasNext()); + + boolean found = false; + + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); +// System.err.println("type: " + type); + + if (!found && (rawType == type || rawType.equals(type))) { + found = true; + } + } + + assertTrue("RAW image type not in type iterator", found); + } + } + + @Test + public void testReadLayersExplicitType() throws IOException { + PSDImageReader imageReader = createReader(); + + imageReader.setInput(getTestData().get(3).getInputStream()); + + int numImages = imageReader.getNumImages(true); + for (int i = 0; i < numImages; i++) { + Iterator types = imageReader.getImageTypes(i); + + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + ImageReadParam param = imageReader.getDefaultReadParam(); + param.setDestinationType(type); + BufferedImage image = imageReader.read(i, param); + + assertEquals(type.getBufferedImageType(), image.getType()); + + if (type.getBufferedImageType() == 0) { + // TODO: If type.getBIT == 0, test more + // Compatible color model + assertEquals(type.getNumComponents(), image.getColorModel().getNumComponents()); + + // Same color space + assertEquals(type.getColorModel().getColorSpace(), image.getColorModel().getColorSpace()); + + // Same number of samples + assertEquals(type.getNumBands(), image.getSampleModel().getNumBands()); + + // Same number of bits/sample + for (int j = 0; j < type.getNumBands(); j++) { + assertEquals(type.getBitsPerBand(j), image.getSampleModel().getSampleSize(j)); + } + } + } + } + } + + @Test + public void testReadLayersExplicitDestination() throws IOException { + PSDImageReader imageReader = createReader(); + + imageReader.setInput(getTestData().get(3).getInputStream()); + + int numImages = imageReader.getNumImages(true); + for (int i = 0; i < numImages; i++) { + Iterator types = imageReader.getImageTypes(i); + int width = imageReader.getWidth(i); + int height = imageReader.getHeight(i); + + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + ImageReadParam param = imageReader.getDefaultReadParam(); + BufferedImage destination = type.createBufferedImage(width, height); + param.setDestination(destination); + + BufferedImage image = imageReader.read(i, param); + + assertSame(destination, image); + } + } + } } \ No newline at end of file diff --git a/imageio/imageio-psd/todo.txt b/imageio/imageio-psd/todo.txt index c7d6d53c..7d731b62 100755 --- a/imageio/imageio-psd/todo.txt +++ b/imageio/imageio-psd/todo.txt @@ -1,6 +1,7 @@ -Implement source subsampling and region of interest -Separate package for the resources (seems to be a lot)? -Possibility to read only some resources? readResources(int[] resourceKeys)? - - Probably faster when we only need the color space -Support for Photoshop specific TIFF tags (extension for TIFFImageReader)? -PSDImageWriter? \ No newline at end of file +- Implement source subsampling and region of interest +- Separate package for the resources (seems to be a lot)? + - Move to metadata package, or implement metadata interface, as this is (similar|equivalent) to TIFF/JPEG APP13 segment tag? +- Possibility to read only some resources? readResources(int[] resourceKeys)? + - Probably faster when we only need the color space +- Support for Photoshop specific TIFF tags (extension for TIFFImageReader)? +- PSDImageWriter? diff --git a/imageio/imageio-reference/pom.xml b/imageio/imageio-reference/pom.xml index 4b96331d..db316d00 100644 --- a/imageio/imageio-reference/pom.xml +++ b/imageio/imageio-reference/pom.xml @@ -11,7 +11,7 @@ imageio-reference TwelveMonkeys :: ImageIO :: reference test cases - Test cases for the JDK provided ImageReader implementations for reference. + Test cases for the JRE provided ImageReader implementations for reference. diff --git a/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java b/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java index 8b552a9c..69da521b 100644 --- a/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java +++ b/imageio/imageio-reference/src/test/java/com/twelvemonkeys/imageio/reference/JPEGImageReaderTestCase.java @@ -4,13 +4,15 @@ import com.sun.imageio.plugins.jpeg.JPEGImageReader; import com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; import com.twelvemonkeys.lang.SystemUtil; +import org.junit.Ignore; +import org.junit.Test; import javax.imageio.IIOException; import javax.imageio.spi.ImageReaderSpi; -import java.util.Arrays; -import java.util.List; import java.awt.*; import java.io.IOException; +import java.util.Arrays; +import java.util.List; /** * JPEGImageReaderTestCase @@ -22,7 +24,7 @@ import java.io.IOException; public class JPEGImageReaderTestCase extends ImageReaderAbstractTestCase { private static final boolean IS_JAVA_6 = SystemUtil.isClassAvailable("java.util.Deque"); - protected JPEGImageReaderSpi mProvider = new JPEGImageReaderSpi(); + protected JPEGImageReaderSpi provider = new JPEGImageReaderSpi(); @Override protected List getTestData() { @@ -33,7 +35,7 @@ public class JPEGImageReaderTestCase extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList(mProvider.getFormatNames()); + return Arrays.asList(provider.getFormatNames()); } @Override protected List getSuffixes() { - return Arrays.asList(mProvider.getFileSuffixes()); + return Arrays.asList(provider.getFileSuffixes()); } @Override protected List getMIMETypes() { - return Arrays.asList(mProvider.getMIMETypes()); + return Arrays.asList(provider.getMIMETypes()); } + @Test @Override public void testSetDestination() throws IOException { // Known bug in Sun JPEGImageReader before Java 6 @@ -78,6 +81,7 @@ public class JPEGImageReaderTestCase extends ImageReaderAbstractTestCase { - protected PNGImageReaderSpi mProvider = new PNGImageReaderSpi(); + protected PNGImageReaderSpi provider = new PNGImageReaderSpi(); @Override protected List getTestData() { @@ -30,7 +31,7 @@ public class PNGImageReaderTestCase extends ImageReaderAbstractTestCase getFormatNames() { - return Arrays.asList(mProvider.getFormatNames()); + return Arrays.asList(provider.getFormatNames()); } @Override protected List getSuffixes() { - return Arrays.asList(mProvider.getFileSuffixes()); + return Arrays.asList(provider.getFileSuffixes()); } @Override protected List getMIMETypes() { - return Arrays.asList(mProvider.getMIMETypes()); + return Arrays.asList(provider.getMIMETypes()); } + @Test @Override public void testSetDestinationTypeIllegal() throws IOException { try { diff --git a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/Catalog.java b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/Catalog.java index 51a6f288..c791ac29 100755 --- a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/Catalog.java +++ b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/Catalog.java @@ -50,12 +50,12 @@ import java.util.Date; // TODO: Consider moving this one to io.ole2 public final class Catalog implements Iterable { - private final CatalogHeader mHeader; - private final CatalogItem[] mItems; + private final CatalogHeader header; + private final CatalogItem[] items; Catalog(final CatalogHeader pHeader, final CatalogItem[] pItems) { - mHeader = pHeader; - mItems = pItems; + header = pHeader; + items = pItems; } /** @@ -95,32 +95,32 @@ public final class Catalog implements Iterable { } public final int getThumbnailCount() { - return mHeader.mThumbCount; + return header.mThumbCount; } public final int getMaxThumbnailWidth() { - return mHeader.mThumbWidth; + return header.mThumbWidth; } public final int getMaxThumbnailHeight() { - return mHeader.mThumbHeight; + return header.mThumbHeight; } final CatalogItem getItem(final int pIndex) { - return mItems[pIndex]; + return items[pIndex]; } final CatalogItem getItem(final String pName) { - return mItems[getIndex(pName)]; + return items[getIndex(pName)]; } final int getItemId(final int pIndex) { - return mItems[pIndex].getItemId(); + return items[pIndex].getItemId(); } public final int getIndex(final String pName) { - for (int i = 0; i < mItems.length; i++) { - CatalogItem item = mItems[i]; + for (int i = 0; i < items.length; i++) { + CatalogItem item = items[i]; if (item.getName().equals(pName)) { return i; @@ -139,12 +139,12 @@ public final class Catalog implements Iterable { } final String getName(int pItemId) { - return mItems[pItemId - 1].getName(); + return items[pItemId - 1].getName(); } @Override public String toString() { - return String.format("%s[%s]", getClass().getSimpleName(), mHeader); + return String.format("%s[%s]", getClass().getSimpleName(), header); } public Iterator iterator() { @@ -152,11 +152,11 @@ public final class Catalog implements Iterable { int mCurrentIdx; public boolean hasNext() { - return mCurrentIdx < mItems.length; + return mCurrentIdx < items.length; } public CatalogItem next() { - return mItems[mCurrentIdx++]; + return items[mCurrentIdx++]; } public void remove() { diff --git a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java index 56b80623..13152c22 100644 --- a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java +++ b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReader.java @@ -64,14 +64,14 @@ import java.util.SortedSet; */ public class ThumbsDBImageReader extends ImageReaderBase { private static final int THUMBNAIL_OFFSET = 12; - private Entry mRoot; - private Catalog mCatalog; + private Entry root; + private Catalog catalog; - private BufferedImage[] mThumbnails; - private final ImageReader mReader; - private int mCurrentImage = -1; + private BufferedImage[] thumbnails; + private final ImageReader reader; + private int currentImage = -1; - private boolean mLoadEagerly; + private boolean loadEagerly; public ThumbsDBImageReader() { this(new ThumbsDBImageReaderSpi()); @@ -79,14 +79,14 @@ public class ThumbsDBImageReader extends ImageReaderBase { protected ThumbsDBImageReader(final ThumbsDBImageReaderSpi pProvider) { super(pProvider); - mReader = createJPEGReader(pProvider); + reader = createJPEGReader(pProvider); initReaderListeners(); } protected void resetMembers() { - mRoot = null; - mCatalog = null; - mThumbnails = null; + root = null; + catalog = null; + thumbnails = null; } private static ImageReader createJPEGReader(final ThumbsDBImageReaderSpi pProvider) { @@ -94,12 +94,12 @@ public class ThumbsDBImageReader extends ImageReaderBase { } public void dispose() { - mReader.dispose(); + reader.dispose(); super.dispose(); } public boolean isLoadEagerly() { - return mLoadEagerly; + return loadEagerly; } /** @@ -113,7 +113,7 @@ public class ThumbsDBImageReader extends ImageReaderBase { * @param pLoadEagerly {@code true} if the reader should read all thumbs on first read */ public void setLoadEagerly(final boolean pLoadEagerly) { - mLoadEagerly = pLoadEagerly; + loadEagerly = pLoadEagerly; } /** @@ -135,19 +135,19 @@ public class ThumbsDBImageReader extends ImageReaderBase { // Quick look-up BufferedImage image = null; - if (pIndex < mThumbnails.length) { - image = mThumbnails[pIndex]; + if (pIndex < thumbnails.length) { + image = thumbnails[pIndex]; } if (image == null) { // Read the image, it's a JFIF stream, inside the OLE 2 CompoundDocument init(pIndex); - image = mReader.read(0, pParam); - mReader.reset(); + image = reader.read(0, pParam); + reader.reset(); if (pParam == null) { - mThumbnails[pIndex] = image; // TODO: Caching is not kosher, as images are mutable!! + thumbnails[pIndex] = image; // TODO: Caching is not kosher, as images are mutable!! } } else { @@ -193,7 +193,7 @@ public class ThumbsDBImageReader extends ImageReaderBase { public BufferedImage read(final String pName, final ImageReadParam pParam) throws IOException { initCatalog(); - int index = mCatalog.getIndex(pName); + int index = catalog.getIndex(pName); if (index < 0) { throw new FileNotFoundException("Name not found in \"Catalog\" entry: " + pName); } @@ -203,39 +203,40 @@ public class ThumbsDBImageReader extends ImageReaderBase { public void abort() { super.abort(); - mReader.abort(); + reader.abort(); } @Override - public void setInput(Object pInput, boolean pSeekForwardOnly, boolean pIgnoreMetadata) { - super.setInput(pInput, pSeekForwardOnly, pIgnoreMetadata); - if (mImageInput != null) { - mImageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(input, seekForwardOnly, ignoreMetadata); + if (imageInput != null) { + imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); } } private void init(final int pIndex) throws IOException { - if (mCurrentImage == -1 || pIndex != mCurrentImage || mReader.getInput() == null) { + if (currentImage == -1 || pIndex != currentImage || reader.getInput() == null) { init(); checkBounds(pIndex); - mCurrentImage = pIndex; + currentImage = pIndex; initReader(pIndex); } } private void initReader(final int pIndex) throws IOException { - String name = mCatalog.getStreamName(pIndex); - Entry entry = mRoot.getChildEntry(name); + init(); + String name = catalog.getStreamName(pIndex); + Entry entry = root.getChildEntry(name); // TODO: It might be possible to speed this up, with less wrapping... // Use in-memory input stream for max speed, images are small ImageInputStream input = new MemoryCacheImageInputStream(entry.getInputStream()); input.skipBytes(THUMBNAIL_OFFSET); - mReader.setInput(input); + reader.setInput(input); } private void initReaderListeners() { - mReader.addIIOReadProgressListener(new ProgressListenerBase() { + reader.addIIOReadProgressListener(new ProgressListenerBase() { @Override public void imageComplete(ImageReader pSource) { processImageComplete(); @@ -243,7 +244,7 @@ public class ThumbsDBImageReader extends ImageReaderBase { @Override public void imageStarted(ImageReader pSource, int pImageIndex) { - processImageStarted(mCurrentImage); + processImageStarted(currentImage); } @Override @@ -262,66 +263,66 @@ public class ThumbsDBImageReader extends ImageReaderBase { private void init() throws IOException { assertInput(); - if (mRoot == null) { - mRoot = new CompoundDocument(mImageInput).getRootEntry(); - SortedSet children = mRoot.getChildEntries(); + if (root == null) { + root = new CompoundDocument(imageInput).getRootEntry(); + SortedSet children = root.getChildEntries(); - mThumbnails = new BufferedImage[children.size() - 1]; + thumbnails = new BufferedImage[children.size() - 1]; initCatalog(); // NOTE: This is usually slower, unless you need all images // TODO: Use as many threads as there are CPU cores? :-) - if (mLoadEagerly) { - for (int i = 0; i < mThumbnails.length; i++) { + if (loadEagerly) { + for (int i = 0; i < thumbnails.length; i++) { initReader(i); - ImageReader reader = mReader; + ImageReader reader = this.reader; // TODO: If stream was detached, we could probably create a // new reader, then fire this off in a separate thread... - mThumbnails[i] = reader.read(0, null); + thumbnails[i] = reader.read(0, null); } } } } private void initCatalog() throws IOException { - if (mCatalog == null) { - Entry catalog = mRoot.getChildEntry("Catalog"); + if (catalog == null) { + Entry catalog = root.getChildEntry("Catalog"); if (catalog.length() <= 16L) { // TODO: Throw exception? Return empty catalog? } - mCatalog = Catalog.read(catalog.getInputStream()); + this.catalog = Catalog.read(catalog.getInputStream()); } } - public int getNumImages(boolean pAllowSearch) throws IOException { - if (pAllowSearch) { + public int getNumImages(boolean allowSearch) throws IOException { + if (allowSearch) { init(); } - return mCatalog != null ? mCatalog.getThumbnailCount() : super.getNumImages(false); + return catalog != null ? catalog.getThumbnailCount() : super.getNumImages(false); } public int getWidth(int pIndex) throws IOException { init(pIndex); - BufferedImage image = mThumbnails[pIndex]; - return image != null ? image.getWidth() : mReader.getWidth(0); + BufferedImage image = thumbnails[pIndex]; + return image != null ? image.getWidth() : reader.getWidth(0); } public int getHeight(int pIndex) throws IOException { init(pIndex); - BufferedImage image = mThumbnails[pIndex]; - return image != null ? image.getHeight() : mReader.getHeight(0); + BufferedImage image = thumbnails[pIndex]; + return image != null ? image.getHeight() : reader.getHeight(0); } public Iterator getImageTypes(int pIndex) throws IOException { init(pIndex); initReader(pIndex); - return mReader.getImageTypes(0); + return reader.getImageTypes(0); } public boolean isPresent(final String pFileName) { @@ -344,7 +345,7 @@ public class ThumbsDBImageReader extends ImageReaderBase { } extension = extension.toLowerCase(); - return !"psd".equals(extension) && !"svg".equals(extension) && mCatalog != null && mCatalog.getIndex(pFileName) != -1; + return !"psd".equals(extension) && !"svg".equals(extension) && catalog != null && catalog.getIndex(pFileName) != -1; } /// Test code below @@ -357,7 +358,7 @@ public class ThumbsDBImageReader extends ImageReaderBase { if (pArgs.length > 1) { long start = System.currentTimeMillis(); reader.init(); - for (Catalog.CatalogItem item : reader.mCatalog) { + for (Catalog.CatalogItem item : reader.catalog) { reader.read(item.getName(), null); } long end = System.currentTimeMillis(); @@ -371,8 +372,8 @@ public class ThumbsDBImageReader extends ImageReaderBase { long start = System.currentTimeMillis(); reader.init(); - for (Catalog.CatalogItem item : reader.mCatalog) { - addImage(panel, reader, reader.mCatalog.getIndex(item.getName()), item.getName()); + for (Catalog.CatalogItem item : reader.catalog) { + addImage(panel, reader, reader.catalog.getIndex(item.getName()), item.getName()); } long end = System.currentTimeMillis(); System.out.println("Time: " + (end - start) + " ms"); diff --git a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java index 362c7559..1c704285 100755 --- a/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java +++ b/imageio/imageio-thumbsdb/src/main/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderSpi.java @@ -49,7 +49,7 @@ import java.util.Locale; * @version $Id: ThumbsDBImageReaderSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$ */ public class ThumbsDBImageReaderSpi extends ImageReaderSpi { - private ImageReaderSpi mJPEGProvider; + private ImageReaderSpi jpegProvider; /** * Creates a {@code ThumbsDBImageReaderSpi}. @@ -81,7 +81,7 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi { maybeInitJPEGProvider(); // If this is a OLE 2 CompoundDocument, we could try... // TODO: How do we know it's thumbs.db format (structure), without reading quite a lot? - return mJPEGProvider != null && CompoundDocument.canRead(pInput); + return jpegProvider != null && CompoundDocument.canRead(pInput); } public ImageReader createReaderInstance(Object extension) throws IOException { @@ -98,7 +98,7 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi { // - Class path lookup of properties file with reader? // This way we could deregister immediately - if (mJPEGProvider == null) { + if (jpegProvider == null) { ImageReaderSpi provider = null; try { Iterator providers = getJPEGProviders(); @@ -117,7 +117,7 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi { // In any case, we deregister the provider if there isn't one IIORegistry.getDefaultInstance().deregisterServiceProvider(this, ImageReaderSpi.class); } - mJPEGProvider = provider; + jpegProvider = provider; } } @@ -149,12 +149,12 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi { */ ImageReader createJPEGReader() { maybeInitJPEGProvider(); - if (mJPEGProvider == null) { + if (jpegProvider == null) { throw new IllegalStateException("No suitable JPEG reader provider found"); } try { - return mJPEGProvider.createReaderInstance(); + return jpegProvider.createReaderInstance(); } catch (IOException e) { // NOTE: The default Sun version never throws IOException here @@ -170,7 +170,7 @@ public class ThumbsDBImageReaderSpi extends ImageReaderSpi { // public void onRegistration(ServiceRegistry registry, Class category) { // System.out.println("ThumbsDBImageReaderSpi.onRegistration"); // maybeInitJPEGProvider(); -// if (mJPEGProvider == null) { +// if (jpegProvider == null) { // System.out.println("Deregistering"); // registry.deregisterServiceProvider(this, ImageReaderSpi.class); // } diff --git a/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java b/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java index 868658cc..ee1e7440 100644 --- a/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java +++ b/imageio/imageio-thumbsdb/src/test/java/com/twelvemonkeys/imageio/plugins/thumbsdb/ThumbsDBImageReaderTestCase.java @@ -33,6 +33,8 @@ import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; import com.twelvemonkeys.io.ole2.CompoundDocument; import com.twelvemonkeys.io.ole2.Entry; import com.twelvemonkeys.lang.SystemUtil; +import org.junit.Ignore; +import org.junit.Test; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; @@ -53,7 +55,7 @@ import java.util.List; public class ThumbsDBImageReaderTestCase extends ImageReaderAbstractTestCase { private static final boolean IS_JAVA_6 = SystemUtil.isClassAvailable("java.util.Deque"); - private ThumbsDBImageReaderSpi mProvider = new ThumbsDBImageReaderSpi(); + private ThumbsDBImageReaderSpi provider = new ThumbsDBImageReaderSpi(); protected List getTestData() { return Arrays.asList( @@ -82,12 +84,12 @@ public class ThumbsDBImageReaderTestCase extends ImageReaderAbstractTestCase getReaderClass() { @@ -106,6 +108,7 @@ public class ThumbsDBImageReaderTestCase extends ImageReaderAbstractTestCase + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.0-SNAPSHOT + + imageio-tiff + TwelveMonkeys :: ImageIO :: TIFF plugin + + ImageIO plugin for Aldus/Adobe Tagged Image File Format (TIFF). + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-metadata + + + com.twelvemonkeys.imageio + imageio-core + tests + + + diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/G31DDecoder.java similarity index 75% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/LZWDecoder.java rename to imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/G31DDecoder.java index 7d9958cf..cb02dd9b 100644 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/LZWDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/G31DDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, Harald Kuhr + * Copyright (c) 2012, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,21 +26,22 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.io.enc; +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.io.enc.Decoder; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; /** - * LZWDecoder. - *

    + * CCITT Group 3 One-Dimensional (G31D) "No EOLs" Decoder. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWDecoder.java#2 $ + * @author last modified by $Author: haraldk$ + * @version $Id: G31DDecoder.java,v 1.0 23.05.12 15:55 haraldk Exp$ */ -final class LZWDecoder implements Decoder { - public int decode(InputStream pStream, byte[] pBuffer) throws IOException { - return 0; // TODO: Implement - // TODO: We probably need a GIF specific subclass +final class G31DDecoder implements Decoder { + public int decode(final InputStream stream, final byte[] buffer) throws IOException { + throw new UnsupportedOperationException("Method decode not implemented"); // TODO: Implement } -} \ No newline at end of file +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java new file mode 100644 index 00000000..a806ec11 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/JPEGTables.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGQuality; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; + +import javax.imageio.IIOException; +import javax.imageio.plugins.jpeg.JPEGHuffmanTable; +import javax.imageio.plugins.jpeg.JPEGQTable; +import javax.imageio.stream.ImageInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.*; + +/** + * JPEGTables + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGTables.java,v 1.0 11.05.12 09:13 haraldk Exp$ + */ +class JPEGTables { + private static final int DHT_LENGTH = 16; + private static final Map> SEGMENT_IDS = createSegmentIdsMap(); + + private JPEGQTable[] qTables; + private JPEGHuffmanTable[] dcHTables; + private JPEGHuffmanTable[] acHTables; + + private static Map> createSegmentIdsMap() { + Map> segmentIds = new HashMap>(); + segmentIds.put(JPEG.DQT, null); + segmentIds.put(JPEG.DHT, null); + + return Collections.unmodifiableMap(segmentIds); + } + + private final List segments; + + public JPEGTables(ImageInputStream input) throws IOException { + segments = JPEGSegmentUtil.readSegments(input, SEGMENT_IDS); + } + + public JPEGQTable[] getQTables() throws IOException { + if (qTables == null) { + qTables = JPEGQuality.getQTables(segments); + } + + return qTables; + } + + private void getHuffmanTables() throws IOException { + if (dcHTables == null || acHTables == null) { + List dc = new ArrayList(); + List ac = new ArrayList(); + + // JPEG may contain multiple DHT marker segments + for (JPEGSegment segment : segments) { + if (segment.marker() != JPEG.DHT) { + continue; + } + + DataInputStream data = new DataInputStream(segment.data()); + int read = 0; + + // A single DHT marker segment may contain multiple tables + while (read < segment.length()) { + int htInfo = data.read(); + read++; + + int num = htInfo & 0x0f; // 0-3 + int type = htInfo >> 4; // 0 == DC, 1 == AC + + if (type > 1) { + throw new IIOException("Bad DHT type: " + type); + } + if (num >= 4) { + throw new IIOException("Bad DHT table index: " + num); + } + else if (type == 0 ? dc.size() > num : ac.size() > num) { + throw new IIOException("Duplicate DHT table index: " + num); + } + + // Read lengths as short array + short[] lengths = new short[DHT_LENGTH]; + for (int i = 0, lengthsLength = lengths.length; i < lengthsLength; i++) { + lengths[i] = (short) data.readUnsignedByte(); + } + read += lengths.length; + + int sum = 0; + for (short length : lengths) { + sum += length; + } + + // Expand table to short array + short[] table = new short[sum]; + for (int j = 0; j < sum; j++) { + table[j] = (short) data.readUnsignedByte(); + } + + JPEGHuffmanTable hTable = new JPEGHuffmanTable(lengths, table); + if (type == 0) { + dc.add(num, hTable); + } + else { + ac.add(num, hTable); + } + + read += sum; + } + } + + dcHTables = dc.toArray(new JPEGHuffmanTable[dc.size()]); + acHTables = ac.toArray(new JPEGHuffmanTable[ac.size()]); + } + } + + public JPEGHuffmanTable[] getDCHuffmanTables() throws IOException { + getHuffmanTables(); + return dcHTables; + } + + public JPEGHuffmanTable[] getACHuffmanTables() throws IOException { + getHuffmanTables(); + return acHTables; + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java new file mode 100644 index 00000000..a8cf4617 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.io.enc.DecodeException; +import com.twelvemonkeys.io.enc.Decoder; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +/** + * LZWDecoder + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$ + */ +final class LZWDecoder implements Decoder { + /** Clear: Re-initialize tables. */ + static final int CLEAR_CODE = 256; + /** End of Information. */ + static final int EOI_CODE = 257; + + private static final int MIN_BITS = 9; + private static final int MAX_BITS = 12; + + private final boolean reverseBitOrder; + + private int currentByte = -1; + private int bitPos; + + // TODO: Consider speeding things up with a "string" type (instead of the inner byte[]), + // that uses variable size/dynamic allocation, to avoid the excessive array copying? +// private final byte[][] table = new byte[4096][0]; // libTiff adds another 1024 "for compatibility"... + private final byte[][] table = new byte[4096 + 1024][0]; // libTiff adds another 1024 "for compatibility"... + private int tableLength; + private int bitsPerCode; + private int oldCode = CLEAR_CODE; + private int maxCode; + private int maxString; + private boolean eofReached; + + LZWDecoder(final boolean reverseBitOrder) { + this.reverseBitOrder = reverseBitOrder; + + for (int i = 0; i < 256; i++) { + table[i] = new byte[] {(byte) i}; + } + + init(); + } + + LZWDecoder() { + this(false); + } + + private int maxCodeFor(final int bits) { + return reverseBitOrder ? (1 << bits) - 2 : (1 << bits) - 1; + } + + private void init() { + tableLength = 258; + bitsPerCode = MIN_BITS; + maxCode = maxCodeFor(bitsPerCode); + maxString = 1; + } + + public int decode(final InputStream stream, final byte[] buffer) throws IOException { + // Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992. + // See Section 13: "LZW Compression"/"LZW Decoding", page 61+ + int bufferPos = 0; + int code; + + while ((code = getNextCode(stream)) != EOI_CODE) { + if (code == CLEAR_CODE) { + init(); + code = getNextCode(stream); + + if (code == EOI_CODE) { + break; + } + + bufferPos += writeString(table[code], buffer, bufferPos); + } + else { + if (code > tableLength + 1 || oldCode >= tableLength) { + // TODO: FixMe for old, borked streams + System.err.println("code: " + code); + System.err.println("oldCode: " + oldCode); + System.err.println("tableLength: " + tableLength); + throw new DecodeException("Corrupted LZW table"); + } + + if (isInTable(code)) { + bufferPos += writeString(table[code], buffer, bufferPos); + addStringToTable(concatenate(table[oldCode], table[code][0])); + } + else { + byte[] outString = concatenate(table[oldCode], table[oldCode][0]); + + bufferPos += writeString(outString, buffer, bufferPos); + addStringToTable(outString); + } + } + + oldCode = code; + + if (bufferPos >= buffer.length - maxString - 1) { + // Buffer full, stop decoding for now + break; + } + } + + return bufferPos; + } + + private static byte[] concatenate(final byte[] string, final byte firstChar) { + byte[] result = Arrays.copyOf(string, string.length + 1); + result[string.length] = firstChar; + + return result; + } + + private void addStringToTable(final byte[] string) throws IOException { + table[tableLength++] = string; + + if (tableLength >= maxCode) { + bitsPerCode++; + + if (bitsPerCode > MAX_BITS) { + if (reverseBitOrder) { + bitsPerCode--; + } + else { + throw new DecodeException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS)); + } + } + + maxCode = maxCodeFor(bitsPerCode); + } + + if (string.length > maxString) { + maxString = string.length; + } + } + + private static int writeString(final byte[] string, final byte[] buffer, final int bufferPos) { + if (string.length == 0) { + return 0; + } + else if (string.length == 1) { + buffer[bufferPos] = string[0]; + + return 1; + } + else { + System.arraycopy(string, 0, buffer, bufferPos, string.length); + + return string.length; + } + } + + private boolean isInTable(int code) { + return code < tableLength; + } + + private int getNextCode(final InputStream stream) throws IOException { + if (eofReached) { + return EOI_CODE; + } + + int bitsToFill = bitsPerCode; + int value = 0; + + while (bitsToFill > 0) { + int nextBits; + if (bitPos == 0) { + nextBits = stream.read(); + + if (nextBits == -1) { + // This is really a bad stream, but should be safe to handle this way, rather than throwing an EOFException. + // An EOFException will be thrown by the decoder stream later, if further reading is attempted. + eofReached = true; + return EOI_CODE; + } + } + else { + nextBits = currentByte; + } + + int bitsFromHere = 8 - bitPos; + if (bitsFromHere > bitsToFill) { + bitsFromHere = bitsToFill; + } + + if (reverseBitOrder) { + // NOTE: This is a spec violation. However, libTiff reads such files. + // TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says: + // "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder + // is assumed to be 1. The compressed codes are written as bytes (not words) so that the + // compressed data will be identical whether it is an ‘II’ or ‘MM’ file." + + // Fill bytes from right-to-left + for (int i = 0; i < bitsFromHere; i++) { + int destBitPos = bitsPerCode - bitsToFill + i; + int srcBitPos = bitPos + i; + value |= ((nextBits & (1 << srcBitPos)) >> srcBitPos) << destBitPos; + } + } + else { + value |= (nextBits >> 8 - bitPos - bitsFromHere & 0xff >> 8 - bitsFromHere) << bitsToFill - bitsFromHere; + } + + bitsToFill -= bitsFromHere; + bitPos += bitsFromHere; + + if (bitPos >= 8) { + bitPos = 0; + } + + currentByte = nextBits; + } + + if (value == EOI_CODE) { + eofReached = true; + } + + return value; + } + + static boolean isOldBitReversedStream(final InputStream stream) throws IOException { + stream.mark(2); + try { + int one = stream.read(); + int two = stream.read(); + + return one == 0 && (two & 0x1) == 1; // => (reversed) 1 00000000 == 256 (CLEAR_CODE) + } + finally { + stream.reset(); + } + } +} + diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java new file mode 100644 index 00000000..3cc3e5f6 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +/** + * TIFFBaseline + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFBaseline.java,v 1.0 08.05.12 16:43 haraldk Exp$ + */ +interface TIFFBaseline { + int COMPRESSION_NONE = 1; + int COMPRESSION_CCITT_HUFFMAN = 2; + int COMPRESSION_PACKBITS = 32773; + + int PHOTOMETRIC_WHITE_IS_ZERO = 0; + int PHOTOMETRIC_BLACK_IS_ZERO = 1; + int PHOTOMETRIC_RGB = 2; + int PHOTOMETRIC_PALETTE = 3; + int PHOTOMETRIC_MASK = 4; + + int SAMPLEFORMAT_UINT = 1; // Spec says only UINT required for baseline + + int PLANARCONFIG_CHUNKY = 1; + + int EXTRASAMPLE_UNSPECIFIED = 0; + int EXTRASAMPLE_ASSOCIATED_ALPHA = 1; + int EXTRASAMPLE_UNASSOCIATED_ALPHA = 2; + + int PREDICTOR_NONE = 1; +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java new file mode 100644 index 00000000..5f452b57 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFCustom.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +/** + * TIFFCustom + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFCustom.java,v 1.0 10.05.12 17:35 haraldk Exp$ + */ +interface TIFFCustom { + int COMPRESSION_NEXT = 32766; + int COMPRESSION_CCITTRLEW = 32771; + int COMPRESSION_THUNDERSCAN = 32809; + int COMPRESSION_IT8CTPAD = 32895; + int COMPRESSION_IT8LW = 32896; + int COMPRESSION_IT8MP = 32897; + int COMPRESSION_IT8BL = 32898; + int COMPRESSION_PIXARFILM = 32908; + int COMPRESSION_PIXARLOG = 32909; + int COMPRESSION_DCS = 32947; + int COMPRESSION_JBIG = 34661; + int COMPRESSION_SGILOG = 34676; + int COMPRESSION_SGILOG24 = 34677; + int COMPRESSION_JP2000 = 34712; + + int PHOTOMETRIC_LOGL = 32844; + int PHOTOMETRIC_LOGLUV = 32845; + + /** DNG: CFA (Color Filter Array)*/ + int PHOTOMETRIC_CFA = 32803; + /** DNG: LinearRaw*/ + int PHOTOMETRIC_LINEAR_RAW = 34892; + + int SAMPLEFORMAT_COMPLEX_INT = 5; + int SAMPLEFORMAT_COMPLEX_IEEE_FP = 6; +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java new file mode 100644 index 00000000..4ee2b28e --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +/** + * TIFFExtension + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFExtension.java,v 1.0 08.05.12 16:45 haraldk Exp$ + */ +interface TIFFExtension { + /** CCITT T.4/Group 3 Fax compression. */ + int COMPRESSION_CCITT_T4 = 3; + /** CCITT T.6/Group 4 Fax compression. */ + int COMPRESSION_CCITT_T6 = 4; + /** LZW Compression. Was baseline, but moved to extension due to license issues in the LZW algorithm. */ + int COMPRESSION_LZW = 5; + /** Deprecated. For backwards compatibility only. */ + int COMPRESSION_OLD_JPEG = 6; + /** JPEG Compression (lossy). */ + int COMPRESSION_JPEG = 7; + /** Custom: PKZIP-style Deflate. */ + int COMPRESSION_DEFLATE = 32946; + /** Adobe-style Deflate. */ + int COMPRESSION_ZLIB = 8; + + int PHOTOMETRIC_SEPARATED = 5; + int PHOTOMETRIC_YCBCR = 6; + int PHOTOMETRIC_CIELAB = 8; + int PHOTOMETRIC_ICCLAB = 9; + int PHOTOMETRIC_ITULAB = 10; + + int PLANARCONFIG_PLANAR = 2; + + int PREDICTOR_HORIZONTAL_DIFFERENCING = 2; + int PREDICTOR_HORIZONTAL_FLOATINGPOINT = 3; + + int SAMPLEFORMAT_INT = 2; + int SAMPLEFORMAT_FP = 3; + int SAMPLEFORMAT_UNDEFINED = 4; +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java new file mode 100644 index 00000000..d45d73e5 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -0,0 +1,1076 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.sun.imageio.plugins.jpeg.JPEGImageReader; +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.color.ColorSpaces; +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import com.twelvemonkeys.imageio.stream.SubImageInputStream; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; +import com.twelvemonkeys.imageio.util.ProgressListenerBase; +import com.twelvemonkeys.io.LittleEndianDataInputStream; +import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.io.enc.PackBitsDecoder; + +import javax.imageio.*; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.plugins.jpeg.JPEGImageReadParam; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ServiceRegistry; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.*; +import java.io.*; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * ImageReader implementation for Aldus/Adobe Tagged Image File Format (TIFF). + *

    + * The reader is supposed to be fully "Baseline TIFF" compliant, and supports the following image types: + *

      + *
    • Class B (Bi-level), all relevant compression types, 1 bit per sample
    • + *
    • Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer
    • + *
    • Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer
    • + *
    • Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer
    • + *
    + * In addition, it supports many common TIFF extensions such as: + *
      + *
    • Tiling
    • + *
    • LZW Compression (type 5)
    • + *
    • JPEG Compression (type 7)
    • + *
    • ZLib (aka Adobe-style Deflate) Compression (type 8)
    • + *
    • Deflate Compression (type 32946)
    • + *
    • Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression
    • + *
    • Alpha channel (ExtraSamples type 1/Associated Alpha)
    • + *
    • CMYK data (PhotometricInterpretation type 5/Separated)
    • + *
    • YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG
    • + *
    • Planar data (PlanarConfiguration type 2/Planar)
    • + *
    • ICC profiles (ICCProfile)
    • + *
    • BitsPerSample values up to 16 for most PhotometricInterpretations
    • + *
    + * + * @see Adobe TIFF developer resources + * @see Wikipedia + * @see AWare Systems TIFF pages + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFImageReader.java,v 1.0 08.05.12 15:14 haraldk Exp$ + */ +public class TIFFImageReader extends ImageReaderBase { + // TODOs ImageIO basic functionality: + // TODO: Subsampling (*tests should be failing*) + // TODO: Source region (*tests should be failing*) + // TODO: TIFFImageWriter + Spi + + // TODOs ImageIO advanced functionality: + // TODO: Implement readAsRenderedImage to allow tiled renderImage? + // For some layouts, we could do reads super-fast with a memory mapped buffer. + // TODO: Implement readAsRaster directly + + // TODOs Full BaseLine support: + // TODO: Support ExtraSamples (an array, if multiple extra samples!) + // (0: Unspecified (not alpha), 1: Associated Alpha (pre-multiplied), 2: Unassociated Alpha (non-multiplied) + // TODO: Support Compression 2 (CCITT Modified Huffman) for bi-level images + + // TODOs Extension support + // TODO: Support PlanarConfiguration 2 + // TODO: Support ICCProfile (fully) + // TODO: Support Compression 3 & 4 (CCITT T.4 & T.6) + // TODO: Support Compression 6 ('Old-style' JPEG) + // TODO: Support Compression 34712 (JPEG2000)? Depends on JPEG2000 ImageReader + // TODO: Support Compression 34661 (JBIG)? Depends on JBIG ImageReader + + // DONE: + // Handle SampleFormat (and give up if not == 1) + + private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug")); + + private CompoundDirectory IFDs; + private Directory currentIFD; + + TIFFImageReader(final TIFFImageReaderSpi provider) { + super(provider); + } + + @Override + protected void resetMembers() { + IFDs = null; + currentIFD = null; + } + + private void readMetadata() throws IOException { + if (imageInput == null) { + throw new IllegalStateException("input not set"); + } + + if (IFDs == null) { + IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); // NOTE: Sets byte order as a side effect + + if (DEBUG) { + for (int i = 0; i < IFDs.directoryCount(); i++) { + System.err.printf("ifd[%d]: %s\n", i, IFDs.getDirectory(i)); + } + + System.err.println("Byte order: " + imageInput.getByteOrder()); + System.err.println("numImages: " + IFDs.directoryCount()); + } + } + } + + private void readIFD(final int imageIndex) throws IOException { + readMetadata(); + checkBounds(imageIndex); + currentIFD = IFDs.getDirectory(imageIndex); + } + + @Override + public int getNumImages(final boolean allowSearch) throws IOException { + readMetadata(); + + return IFDs.directoryCount(); + } + + private int getValueAsIntWithDefault(final int tag, String tagName, Integer defaultValue) throws IIOException { + Entry entry = currentIFD.getEntryById(tag); + + if (entry == null) { + if (defaultValue != null) { + return defaultValue; + } + + throw new IIOException("Missing TIFF tag: " + (tagName != null ? tagName : tag)); + } + + return ((Number) entry.getValue()).intValue(); + } + + private int getValueAsIntWithDefault(final int tag, Integer defaultValue) throws IIOException { + return getValueAsIntWithDefault(tag, null, defaultValue); + } + + private int getValueAsInt(final int tag, String tagName) throws IIOException { + return getValueAsIntWithDefault(tag, tagName, null); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + readIFD(imageIndex); + + return getValueAsInt(TIFF.TAG_IMAGE_WIDTH, "ImageWidth"); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + readIFD(imageIndex); + + return getValueAsInt(TIFF.TAG_IMAGE_HEIGHT, "ImageHeight"); + } + + @Override + public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException { + readIFD(imageIndex); + + getSampleFormat(); // We don't support anything but SAMPLEFORMAT_UINT at the moment, just sanity checking input + int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFExtension.PLANARCONFIG_PLANAR); + int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"); + int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXELS, 1); + int bitsPerSample = getBitsPerSample(); + int dataType = bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT; + + // Read embedded cs + ICC_Profile profile = getICCProfile(); + ColorSpace cs; + + switch (interpretation) { + // TIFF 6.0 baseline + case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: + // WhiteIsZero + // NOTE: We handle this by inverting the values when reading, as Java has no ColorModel that easily supports this. + // TODO: Consider returning null? + case TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO: + // BlackIsZero + // Gray scale or B/W + switch (samplesPerPixel) { + case 1: + // TIFF 6.0 Spec says: 1, 4 or 8 for baseline (1 for bi-level, 4/8 for gray) + // ImageTypeSpecifier supports 1, 2, 4, 8 or 16 bits, we'll go with that for now + cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_GRAY) : ColorSpaces.createColorSpace(profile); + + if (cs == ColorSpace.getInstance(ColorSpace.CS_GRAY) && (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 || bitsPerSample == 8 || bitsPerSample == 16)) { + return ImageTypeSpecifier.createGrayscale(bitsPerSample, dataType, false); + } + else if (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 || bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32) { + return ImageTypeSpecifier.createInterleaved(cs, new int[] {0}, dataType, false, false); + } + default: + // TODO: If ExtraSamples is used, PlanarConfiguration must be taken into account also for gray data + + throw new IIOException(String.format("Unsupported SamplesPerPixel/BitsPerSample combination for Bi-level/Gray TIFF (expected 1/1, 1/2, 1/4, 1/8 or 1/16): %d/%d", samplesPerPixel, bitsPerSample)); + } + + case TIFFExtension.PHOTOMETRIC_YCBCR: + // JPEG reader will handle YCbCr to RGB for us, we'll have to do it ourselves if not JPEG... + // TODO: Handle YCbCrSubsampling (up-scaler stream, or read data as-is + up-sample (sub-)raster after read? Apply smoothing?) + case TIFFBaseline.PHOTOMETRIC_RGB: + // RGB + cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpaces.createColorSpace(profile); + + switch (samplesPerPixel) { + case 3: + if (bitsPerSample == 8 || bitsPerSample == 16) { + switch (planarConfiguration) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + if (bitsPerSample == 8 && cs.isCS_sRGB()) { + return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); + } + + return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2}, dataType, false, false); + + case TIFFExtension.PLANARCONFIG_PLANAR: + return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, dataType, false, false); + } + } + case 4: + // TODO: Consult ExtraSamples! + if (bitsPerSample == 8 || bitsPerSample == 16) { + switch (planarConfiguration) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + if (bitsPerSample == 8 && cs.isCS_sRGB()) { + return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); + } + + return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, false, false); + + case TIFFExtension.PLANARCONFIG_PLANAR: + return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, false, false); + } + } + // TODO: More samples might be ok, if multiple alpha or unknown samples + default: + throw new IIOException(String.format("Unsupported SamplesPerPixels/BitsPerSample combination for RGB TIF (expected 3/8, 4/8, 3/16 or 4/16): %d/%d", samplesPerPixel, bitsPerSample)); + } + case TIFFBaseline.PHOTOMETRIC_PALETTE: + // Palette + if (samplesPerPixel != 1) { + throw new IIOException("Bad SamplesPerPixel value for Palette TIFF (expected 1): " + samplesPerPixel); + } + else if (bitsPerSample <= 0 || bitsPerSample > 16) { + throw new IIOException("Bad BitsPerSample value for Palette TIFF (expected <= 16): " + bitsPerSample); + } + // NOTE: If ExtraSamples is used, PlanarConfiguration must be taken into account also for pixel data + + Entry colorMap = currentIFD.getEntryById(TIFF.TAG_COLOR_MAP); + if (colorMap == null) { + throw new IIOException("Missing ColorMap for Palette TIFF"); + } + + int[] cmapShort = (int[]) colorMap.getValue(); + int[] cmap = new int[colorMap.valueCount() / 3]; + + // All reds, then greens, and finally blues + for (int i = 0; i < cmap.length; i++) { + cmap[i] = (cmapShort[i ] / 256) << 16 + | (cmapShort[i + cmap.length] / 256) << 8 + | (cmapShort[i + 2 * cmap.length] / 256); + } + + IndexColorModel icm = new IndexColorModel(bitsPerSample, cmap.length, cmap, 0, false, -1, dataType); + + return IndexedImageTypeSpecifier.createFromIndexColorModel(icm); + + case TIFFExtension.PHOTOMETRIC_SEPARATED: + // Separated (CMYK etc) + // TODO: Consult the 332/InkSet (1=CMYK, 2=Not CMYK; see InkNames), 334/NumberOfInks (def=4) and optionally 333/InkNames + // If "Not CMYK" we'll need an ICC profile to be able to display (in a useful way), readAsRaster should still work. + cs = profile == null ? ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK) : ColorSpaces.createColorSpace(profile); + + switch (samplesPerPixel) { + case 4: + if (bitsPerSample == 8 || bitsPerSample == 16) { + switch (planarConfiguration) { + case TIFFBaseline.PLANARCONFIG_CHUNKY: + return ImageTypeSpecifier.createInterleaved(cs, new int[] {0, 1, 2, 3}, dataType, false, false); + case TIFFExtension.PLANARCONFIG_PLANAR: + return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, dataType, false, false); + } + } + + // TODO: More samples might be ok, if multiple alpha or unknown samples, consult ExtraSamples + + default: + throw new IIOException( + String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8 or 4/16): %d/%s", samplesPerPixel, bitsPerSample) + ); + } + case TIFFBaseline.PHOTOMETRIC_MASK: + // Transparency mask + + // TODO: Known extensions + throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + interpretation); + default: + throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + interpretation); + } + } + + private int getSampleFormat() throws IIOException { + long[] value = getValueAsLongArray(TIFF.TAG_SAMPLE_FORMAT, "SampleFormat", false); + + if (value != null) { + long sampleFormat = value[0]; + + for (int i = 1; i < value.length; i++) { + if (value[i] != sampleFormat) { + throw new IIOException("Variable TIFF SampleFormat not supported: " + Arrays.toString(value)); + } + } + + if (sampleFormat != TIFFBaseline.SAMPLEFORMAT_UINT) { + throw new IIOException("Unsupported TIFF SampleFormat (expected 1/Unsigned Integer): " + sampleFormat); + } + } + + // The default, and the only value we support + return TIFFBaseline.SAMPLEFORMAT_UINT; + } + + private int getBitsPerSample() throws IIOException { + long[] value = getValueAsLongArray(TIFF.TAG_BITS_PER_SAMPLE, "BitsPerSample", false); + + if (value == null || value.length == 0) { + return 1; + } + else { + int bitsPerSample = (int) value[0]; + + for (int i = 1; i < value.length; i++) { + if (value[i] != bitsPerSample) { + throw new IIOException("Variable BitsPerSample not supported: " + Arrays.toString(value)); + } + } + + return bitsPerSample; + } + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + readIFD(imageIndex); + + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + List specs = new ArrayList(); + + // TODO: Based on raw type, we can probably convert to most RGB types at least, maybe gray etc + // TODO: Planar to chunky by default + if (!rawType.getColorModel().getColorSpace().isCS_sRGB() && rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) { + if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) { + specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); + specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)); + specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); + } + else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 8) { + specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR)); + specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB)); + specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE)); + } + } + + specs.add(rawType); + + return specs.iterator(); + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + readIFD(imageIndex); + + int width = getWidth(imageIndex); + int height = getHeight(imageIndex); + + BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height); + ImageTypeSpecifier rawType = getRawImageType(imageIndex); + checkReadParamBandSettings(param, rawType.getNumBands(), destination.getSampleModel().getNumBands()); + + final Rectangle source = new Rectangle(); + final Rectangle dest = new Rectangle(); + computeRegions(param, width, height, destination, source, dest); + + WritableRaster raster = destination.getRaster(); + + final int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"); + final int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE); + final int predictor = getValueAsIntWithDefault(TIFF.TAG_PREDICTOR, 1); + final int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFBaseline.PLANARCONFIG_CHUNKY); + final int numBands = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR ? 1 : raster.getNumBands(); + + // NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip + // Strips are top/down, tiles are left/right, top/down + int stripTileWidth = width; + int stripTileHeight = getValueAsIntWithDefault(TIFF.TAG_ROWS_PER_STRIP, height); + long[] stripTileOffsets = getValueAsLongArray(TIFF.TAG_TILE_OFFSETS, "TileOffsets", false); + long[] stripTileByteCounts; + + if (stripTileOffsets != null) { + stripTileByteCounts = getValueAsLongArray(TIFF.TAG_TILE_BYTE_COUNTS, "TileByteCounts", false); + if (stripTileByteCounts == null) { + processWarningOccurred("Missing TileByteCounts for tiled TIFF with compression: " + compression); + } + + stripTileWidth = getValueAsInt(TIFF.TAG_TILE_WIDTH, "TileWidth"); + stripTileHeight = getValueAsInt(TIFF.TAG_TILE_HEIGTH, "TileHeight"); + } + else { + stripTileOffsets = getValueAsLongArray(TIFF.TAG_STRIP_OFFSETS, "StripOffsets", true); + stripTileByteCounts = getValueAsLongArray(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts", false); + if (stripTileByteCounts == null) { + processWarningOccurred("Missing StripByteCounts for TIFF with compression: " + compression); + } + + // NOTE: This is really against the spec, but libTiff seems to handle it. TIFF 6.0 says: + // "Do not use both strip- oriented and tile-oriented fields in the same TIFF file". + stripTileWidth = getValueAsIntWithDefault(TIFF.TAG_TILE_WIDTH, "TileWidth", stripTileWidth); + stripTileHeight = getValueAsIntWithDefault(TIFF.TAG_TILE_HEIGTH, "TileHeight", stripTileHeight); + } + + int tilesAcross = (width + stripTileWidth - 1) / stripTileWidth; + int tilesDown = (height + stripTileHeight - 1) / stripTileHeight; + WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1); + int row = 0; + + // Read data + processImageStarted(imageIndex); + + switch (compression) { + // TIFF Baseline + case TIFFBaseline.COMPRESSION_NONE: + // No compression + case TIFFExtension.COMPRESSION_DEFLATE: + // 'PKZIP-style' Deflate + case TIFFBaseline.COMPRESSION_PACKBITS: + // PackBits + case TIFFExtension.COMPRESSION_LZW: + // LZW + case TIFFExtension.COMPRESSION_ZLIB: + // 'Adobe-style' Deflate + + // TODO: Read only tiles that lies within region + + // General uncompressed/compressed reading + for (int y = 0; y < tilesDown; y++) { + int col = 0; + int rowsInTile = Math.min(stripTileHeight, height - row); + + for (int x = 0; x < tilesAcross; x++) { + int colsInTile = Math.min(stripTileWidth, width - col); + int i = y * tilesAcross + x; + + imageInput.seek(stripTileOffsets[i]); + + DataInput input; + if (compression == TIFFBaseline.COMPRESSION_NONE) { + input = imageInput; + } + else { + InputStream adapter = stripTileByteCounts != null + ? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i]) + : IIOUtil.createStreamAdapter(imageInput); + + // According to the spec, short/long/etc should follow order of containing stream + input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN + ? new DataInputStream(createDecoderInputStream(compression, adapter)) + : new LittleEndianDataInputStream(createDecoderInputStream(compression, adapter)); + + } + + // Read a full strip/tile + readStripTileData(rowRaster, interpretation, predictor, raster, numBands, col, row, colsInTile, rowsInTile, input); + + if (abortRequested()) { + break; + } + + col += colsInTile; + } + + processImageProgress(100f * row / (float) height); + + if (abortRequested()) { + processReadAborted(); + break; + } + + row += rowsInTile; + } + + break; + + case TIFFExtension.COMPRESSION_JPEG: + // JPEG ('new-style' JPEG) + // TODO: Refactor all JPEG reading out to separate JPEG support class? + + // TIFF is strictly ISO JPEG, so we should probably stick to the standard reader + ImageReader jpegReader = new JPEGImageReader(getOriginatingProvider()); + JPEGImageReadParam jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam(); + + // JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing: + // SOI, DQT, DHT, (optional markers that we ignore)..., EOI + Entry tablesEntry = currentIFD.getEntryById(TIFF.TAG_JPEG_TABLES); + byte[] tablesValue = tablesEntry != null ? (byte[]) tablesEntry.getValue() : null; + if (tablesValue != null) { + // TODO: Work this out... + // Whatever values I pass the reader as the read param, it never gets the same quality as if + // I just invoke jpegReader.getStreamMetadata... + // Might have something to do with subsampling? + // How do we pass the chroma-subsampling parameter from the TIFF structure to the JPEG reader? + + jpegReader.setInput(new ByteArrayImageInputStream(tablesValue)); + + // NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic). + // This is probably a bug, as later setInput calls should clear/override the tables. + // However, it would be extremely convenient, not having to actually fiddle with the stream meta data (as below) + /*IIOMetadata streamMetadata = */jpegReader.getStreamMetadata(); + + /* + IIOMetadataNode root = (IIOMetadataNode) streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName()); + NodeList dqt = root.getElementsByTagName("dqt"); + NodeList dqtables = ((IIOMetadataNode) dqt.item(0)).getElementsByTagName("dqtable"); + JPEGQTable[] qTables = new JPEGQTable[dqtables.getLength()]; + for (int i = 0; i < dqtables.getLength(); i++) { + qTables[i] = (JPEGQTable) ((IIOMetadataNode) dqtables.item(i)).getUserObject(); + System.err.println("qTables: " + qTables[i]); + } + + List acHTables = new ArrayList(); + List dcHTables = new ArrayList(); + + NodeList dht = root.getElementsByTagName("dht"); + for (int i = 0; i < dht.getLength(); i++) { + NodeList dhtables = ((IIOMetadataNode) dht.item(i)).getElementsByTagName("dhtable"); + for (int j = 0; j < dhtables.getLength(); j++) { + System.err.println("dhtables.getLength(): " + dhtables.getLength()); + IIOMetadataNode dhtable = (IIOMetadataNode) dhtables.item(j); + JPEGHuffmanTable userObject = (JPEGHuffmanTable) dhtable.getUserObject(); + if ("0".equals(dhtable.getAttribute("class"))) { + dcHTables.add(userObject); + } + else { + acHTables.add(userObject); + } + } + } + + JPEGHuffmanTable[] dcTables = dcHTables.toArray(new JPEGHuffmanTable[dcHTables.size()]); + JPEGHuffmanTable[] acTables = acHTables.toArray(new JPEGHuffmanTable[acHTables.size()]); +*/ +// JPEGTables tables = new JPEGTables(new ByteArrayImageInputStream(tablesValue)); +// JPEGQTable[] qTables = tables.getQTables(); +// JPEGHuffmanTable[] dcTables = tables.getDCHuffmanTables(); +// JPEGHuffmanTable[] acTables = tables.getACHuffmanTables(); + +// System.err.println("qTables: " + Arrays.toString(qTables)); +// System.err.println("dcTables: " + Arrays.toString(dcTables)); +// System.err.println("acTables: " + Arrays.toString(acTables)); + +// jpegParam.setDecodeTables(qTables, dcTables, acTables); + } + else { + processWarningOccurred("Missing JPEGTables for TIFF with compression: 7 (JPEG)"); + // ...and the JPEG reader will probably choke on missing tables... + } + + for (int y = 0; y < tilesDown; y++) { + int col = 0; + int rowsInTile = Math.min(stripTileHeight, height - row); + + for (int x = 0; x < tilesAcross; x++) { + int i = y * tilesAcross + x; + int colsInTile = Math.min(stripTileWidth, width - col); + + imageInput.seek(stripTileOffsets[i]); + SubImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE); + try { + jpegReader.setInput(subStream); + jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile)); + jpegParam.setDestinationOffset(new Point(col, row)); + jpegParam.setDestination(destination); + // TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc... + // In the latter case we will have to use readAsRaster + jpegReader.read(0, jpegParam); + } + finally { + subStream.close(); + } + + if (abortRequested()) { + break; + } + + col += colsInTile; + } + + processImageProgress(100f * row / (float) height); + + if (abortRequested()) { + processReadAborted(); + break; + } + + row += rowsInTile; + } + + break; + + case TIFFBaseline.COMPRESSION_CCITT_HUFFMAN: + // CCITT modified Huffman + // Additionally, the specification defines these values as part of the TIFF extensions: + case TIFFExtension.COMPRESSION_CCITT_T4: + // CCITT Group 3 fax encoding + case TIFFExtension.COMPRESSION_CCITT_T6: + // CCITT Group 4 fax encoding + case TIFFExtension.COMPRESSION_OLD_JPEG: + // JPEG ('old-style' JPEG, later overridden in Technote2) + + throw new IIOException("Unsupported TIFF Compression value: " + compression); + default: + throw new IIOException("Unknown TIFF Compression value: " + compression); + } + + processImageComplete(); + + return destination; + } + + private void readStripTileData(final WritableRaster rowRaster, final int interpretation, final int predictor, + final WritableRaster raster, final int numBands, final int col, final int startRow, + final int colsInStrip, final int rowsInStrip, final DataInput input) + throws IOException { + switch (rowRaster.getTransferType()) { + case DataBuffer.TYPE_BYTE: + byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + for (int j = 0; j < rowsInStrip; j++) { + int row = startRow + j; + + if (row >= raster.getHeight()) { + break; + } + + input.readFully(rowData); + +// for (int k = 0; k < rowData.length; k++) { +// try { +// rowData[k] = input.readByte(); +// } +// catch (IOException e) { +// Arrays.fill(rowData, k, rowData.length, (byte) -1); +// System.err.printf("Unexpected EOF or bad data at [%d %d]\n", col + k, row); +// break; +// } +// } + + unPredict(predictor, colsInStrip, 1, numBands, rowData); + normalizeBlack(interpretation, rowData); + + if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) { + raster.setDataElements(col, row, rowRaster); + } + else if (col >= raster.getMinX() && col < raster.getWidth()) { + raster.setDataElements(col, row, rowRaster.createChild(0, 0, Math.min(colsInStrip, raster.getWidth() - col), 1, 0, 0, null)); + } + // Else skip data + } + + break; + case DataBuffer.TYPE_USHORT: + short [] rowDataShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(); + for (int j = 0; j < rowsInStrip; j++) { + int row = startRow + j; + + if (row >= raster.getHeight()) { + break; + } + + for (int k = 0; k < rowDataShort.length; k++) { + rowDataShort[k] = input.readShort(); + } + + unPredict(predictor, colsInStrip, 1, numBands, rowDataShort); + normalizeBlack(interpretation, rowDataShort); + + if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) { + raster.setDataElements(col, row, rowRaster); + } + else if (col >= raster.getMinX() && col < raster.getWidth()) { + raster.setDataElements(col, row, rowRaster.createChild(0, 0, Math.min(colsInStrip, raster.getWidth() - col), 1, 0, 0, null)); + } + // Else skip data + } + + break; + case DataBuffer.TYPE_INT: + int [] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData(); + for (int j = 0; j < rowsInStrip; j++) { + int row = startRow + j; + + if (row >= raster.getHeight()) { + break; + } + + for (int k = 0; k < rowDataInt.length; k++) { + rowDataInt[k] = input.readInt(); + } + + unPredict(predictor, colsInStrip, 1, numBands, rowDataInt); + normalizeBlack(interpretation, rowDataInt); + + if (colsInStrip == rowRaster.getWidth() && col + colsInStrip <= raster.getWidth()) { + raster.setDataElements(col, row, rowRaster); + } + else if (col >= raster.getMinX() && col < raster.getWidth()) { + raster.setDataElements(col, row, rowRaster.createChild(0, 0, Math.min(colsInStrip, raster.getWidth() - col), 1, 0, 0, null)); + } + // Else skip data + } + + break; + } + } + + private void normalizeBlack(int photometricInterpretation, short[] data) { + if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] = (short) (0xffff - data[i] & 0xffff); + } + } + } + + private void normalizeBlack(int photometricInterpretation, int[] data) { + if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] = (0xffffffff - data[i]); + } + } + } + + private void normalizeBlack(int photometricInterpretation, byte[] data) { + if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { + // Inverse values + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (0xff - data[i] & 0xff); + } + } + } + + @SuppressWarnings("UnusedParameters") + private void unPredict(final int predictor, int scanLine, int rows, int bands, int[] data) throws IIOException { + // See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. + switch (predictor) { + case TIFFBaseline.PREDICTOR_NONE: + break; + case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING: + // TODO: Implement + case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT: + throw new IIOException("Unsupported TIFF Predictor value: " + predictor); + default: + throw new IIOException("Unknown TIFF Predictor value: " + predictor); + } + } + + @SuppressWarnings("UnusedParameters") + private void unPredict(final int predictor, int scanLine, int rows, int bands, short[] data) throws IIOException { + // See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. + switch (predictor) { + case TIFFBaseline.PREDICTOR_NONE: + break; + case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING: + // TODO: Implement + case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT: + throw new IIOException("Unsupported TIFF Predictor value: " + predictor); + default: + throw new IIOException("Unknown TIFF Predictor value: " + predictor); + } + } + + private void unPredict(final int predictor, int scanLine, int rows, final int bands, byte[] data) throws IIOException { + // See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. + switch (predictor) { + case TIFFBaseline.PREDICTOR_NONE: + break; + case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING: + for (int y = 0; y < rows; y++) { + for (int x = 1; x < scanLine; x++) { + // TODO: For planar data (PlanarConfiguration == 2), treat as bands == 1 + for (int b = 0; b < bands; b++) { + int off = y * scanLine + x; + data[off * bands + b] = (byte) (data[(off - 1) * bands + b] + data[off * bands + b]); + } + } + } + + break; + case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT: + throw new IIOException("Unsupported TIFF Predictor value: " + predictor); + default: + throw new IIOException("Unknown TIFF Predictor value: " + predictor); + } + } + + private InputStream createDecoderInputStream(final int compression, final InputStream stream) throws IOException { + switch (compression) { + case TIFFBaseline.COMPRESSION_PACKBITS: + return new DecoderStream(stream, new PackBitsDecoder(), 1024); + case TIFFExtension.COMPRESSION_LZW: + return new DecoderStream(stream, new LZWDecoder(LZWDecoder.isOldBitReversedStream(stream)), 1024); + case TIFFExtension.COMPRESSION_ZLIB: + case TIFFExtension.COMPRESSION_DEFLATE: + // TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical + return new InflaterInputStream(stream, new Inflater(), 1024); + default: + throw new IllegalArgumentException("Unsupported TIFF compression: " + compression); + } + } + + private long[] getValueAsLongArray(final int tag, final String tagName, boolean required) throws IIOException { + Entry entry = currentIFD.getEntryById(tag); + if (entry == null) { + if (required) { + throw new IIOException("Missing TIFF tag " + tagName); + } + + return null; + } + + long[] value; + + if (entry.valueCount() == 1) { + // For single entries, this will be a boxed type + value = new long[] {((Number) entry.getValue()).longValue()}; + } + else if (entry.getValue() instanceof short[]) { + short[] shorts = (short[]) entry.getValue(); + value = new long[shorts.length]; + + for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) { + value[i] = shorts[i]; + } + } + else if (entry.getValue() instanceof int[]) { + int[] ints = (int[]) entry.getValue(); + value = new long[ints.length]; + + for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) { + value[i] = ints[i]; + } + } + else if (entry.getValue() instanceof long[]) { + value = (long[]) entry.getValue(); + } + else { + throw new IIOException(String.format("Unsupported %s type: %s (%s)", tagName, entry.getTypeName(), entry.getValue().getClass())); + } + + return value; + } + + public ICC_Profile getICCProfile() { + Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE); + if (entry == null) { + return null; + } + + byte[] value = (byte[]) entry.getValue(); + return ICC_Profile.getInstance(value); + } + + public static void main(final String[] args) throws IOException { + for (final String arg : args) { + File file = new File(arg); + + ImageInputStream input = ImageIO.createImageInputStream(file); + if (input == null) { + System.err.println("Could not read file: " + file); + continue; + } + + deregisterOSXTIFFImageReaderSpi(); + + Iterator readers = ImageIO.getImageReaders(input); + + if (!readers.hasNext()) { + System.err.println("No reader for: " + file); + continue; + } + + ImageReader reader = readers.next(); + System.err.println("Reading using: " + reader); + + reader.addIIOReadWarningListener(new IIOReadWarningListener() { + public void warningOccurred(ImageReader source, String warning) { + System.err.println("Warning: " + arg + ": " + warning); + } + }); + reader.addIIOReadProgressListener(new ProgressListenerBase() { + private static final int MAX_W = 78; + int lastProgress = 0; + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + System.out.print("["); + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + int steps = ((int) (percentageDone * MAX_W) / 100); + + for (int i = lastProgress; i < steps; i++) { + System.out.print("."); + } + + System.out.flush(); + lastProgress = steps; + } + + @Override + public void imageComplete(ImageReader source) { + for (int i = lastProgress; i < MAX_W; i++) { + System.out.print("."); + } + + System.out.println("]"); + } + }); + + reader.setInput(input); + + try { + ImageReadParam param = reader.getDefaultReadParam(); + int numImages = reader.getNumImages(true); + for (int imageNo = 0; imageNo < numImages; imageNo++) { + // if (args.length > 1) { + // int sub = Integer.parseInt(args[1]); + // int sub = 4; + // param.setSourceSubsampling(sub, sub, 0, 0); + // } + + long start = System.currentTimeMillis(); +// param.setSourceRegion(new Rectangle(100, 100, 100, 100)); +// param.setDestinationOffset(new Point(50, 150)); +// param.setSourceSubsampling(2, 2, 0, 0); + BufferedImage image = reader.read(imageNo, param); + System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); +// System.err.println("image: " + image); + +// File tempFile = File.createTempFile("lzw-", ".bin"); +// byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); +// FileOutputStream stream = new FileOutputStream(tempFile); +// try { +// FileUtil.copy(new ByteArrayInputStream(data, 45 * image.getWidth() * 3, 5 * image.getWidth() * 3), stream); +// +// showIt(image.getSubimage(0, 45, image.getWidth(), 5), tempFile.getAbsolutePath()); +// } +// finally { +// stream.close(); +// } +// +// System.err.println("tempFile: " + tempFile.getAbsolutePath()); + + // image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null); +// +// int maxW = 800; +// int maxH = 800; +// +// if (image.getWidth() > maxW || image.getHeight() > maxH) { +// start = System.currentTimeMillis(); +// float aspect = reader.getAspectRatio(0); +// if (aspect >= 1f) { +// image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT); +// } +// else { +// image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_DEFAULT); +// } +// // System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms"); +// } + + showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo))); + + try { + int numThumbnails = reader.getNumThumbnails(0); + for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) { + BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo); + // System.err.println("thumbnail: " + thumbnail); + showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); + } + } + catch (IIOException e) { + System.err.println("Could not read thumbnails: " + e.getMessage()); + e.printStackTrace(); + } + } + } + catch (Throwable t) { + System.err.println(file); + t.printStackTrace(); + } + finally { + input.close(); + } + } + } + + private static void deregisterOSXTIFFImageReaderSpi() { + IIORegistry registry = IIORegistry.getDefaultInstance(); + Iterator providers = registry.getServiceProviders(ImageReaderSpi.class, new ServiceRegistry.Filter() { + public boolean filter(Object provider) { + return provider.getClass().getName().equals("com.sun.imageio.plugins.tiff.TIFFImageReaderSpi"); + } + }, false); + + while (providers.hasNext()) { + ImageReaderSpi next = providers.next(); + registry.deregisterServiceProvider(next); + } + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java new file mode 100644 index 00000000..c4254176 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.spi.ProviderInfo; +import com.twelvemonkeys.imageio.util.IIOUtil; + +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Locale; + +/** + * TIFFImageReaderSpi + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFImageReaderSpi.java,v 1.0 08.05.12 15:14 haraldk Exp$ + */ +public class TIFFImageReaderSpi extends ImageReaderSpi { + // TODO: Should we make sure we register (order) before the com.sun.imageio thing (that isn't what is says) provided by Apple? + /** + * Creates a {@code TIFFImageReaderSpi}. + */ + public TIFFImageReaderSpi() { + this(IIOUtil.getProviderInfo(TIFFImageReaderSpi.class)); + } + + private TIFFImageReaderSpi(final ProviderInfo providerInfo) { + super( + providerInfo.getVendorName(), + providerInfo.getVersion(), + new String[]{"tiff", "TIFF"}, + new String[]{"tif", "tiff"}, + new String[]{ + "image/tiff", "image/x-tiff" + }, + "com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader", + STANDARD_INPUT_TYPE, +// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"}, + null, + true, // supports standard stream metadata + null, null, // native stream format name and class + null, null, // extra stream formats + true, // supports standard image metadata + null, null, + null, null // extra image metadata formats + ); + } + + public boolean canDecodeInput(final Object pSource) throws IOException { + if (!(pSource instanceof ImageInputStream)) { + return false; + } + + ImageInputStream stream = (ImageInputStream) pSource; + + stream.mark(); + try { + byte[] bom = new byte[2]; + stream.readFully(bom); + + ByteOrder originalOrder = stream.getByteOrder(); + + try { + if (bom[0] == 'I' && bom[1] == 'I') { + stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + else if (bom[0] == 'M' && bom[1] == 'M') { + stream.setByteOrder(ByteOrder.BIG_ENDIAN); + } + else { + return false; + } + + // TODO: BigTiff uses version 43 instead of TIFF's 42, and header is slightly different, see + // http://www.awaresystems.be/imaging/tiff/bigtiff.html + int magic = stream.readUnsignedShort(); + + return magic == TIFF.TIFF_MAGIC; + } + finally { + stream.setByteOrder(originalOrder); + } + } + finally { + stream.reset(); + } + } + + public TIFFImageReader createReaderInstance(final Object pExtension) { + return new TIFFImageReader(this); + } + + public String getDescription(final Locale pLocale) { + return "Aldus/Adobe Tagged Image File Format (TIFF) image reader"; + } +} diff --git a/imageio/imageio-tiff/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-tiff/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100755 index 00000000..be0208a5 --- /dev/null +++ b/imageio/imageio-tiff/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java new file mode 100644 index 00000000..e2d8dc62 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java @@ -0,0 +1,122 @@ +package com.twelvemonkeys.imageio.plugins.tiff;/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DecoderAbstractTestCase; +import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.io.enc.Encoder; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; + +/** + * LZWDecoderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LZWDecoderTest.java,v 1.0 08.05.12 23:44 haraldk Exp$ + */ +public class LZWDecoderTest extends DecoderAbstractTestCase { + + @Test + public void testIsOldBitReversedStreamTrue() throws IOException { + assertTrue(LZWDecoder.isOldBitReversedStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"))); + } + + @Test + public void testIsOldBitReversedStreamFalse() throws IOException { + assertFalse(LZWDecoder.isOldBitReversedStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"))); + } + + @Test + public void testShortBitReversedStream() throws IOException { + InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), new LZWDecoder(true), 128); + InputStream unpacked = new ByteArrayInputStream(new byte[512 * 3 * 5]); // Should be all 0's + + assertSameStreamContents(unpacked, stream); + } + + @Ignore("Known issue") + @Test + public void testShortBitReversedStreamLine45To49() throws IOException { + InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short-45-49.bin"), new LZWDecoder(true), 128); + InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-short-45-49.bin"); + + assertSameStreamContents(unpacked, stream); + } + + @Test + public void testLongStream() throws IOException { + InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), new LZWDecoder(), 1024); + InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-long.bin"); + + assertSameStreamContents(unpacked, stream); + } + + private void assertSameStreamContents(InputStream expected, InputStream actual) { + int count = 0; + int data; + + try { +// long toSkip = 3800; +// while ((toSkip -= expected.skip(toSkip)) > 0) { +// } +// toSkip = 3800; +// while ((toSkip -= actual.skip(toSkip)) > 0) { +// } + + while ((data = actual.read()) != -1) { + count++; + + assertEquals(String.format("Incorrect data at offset 0x%04x", count), expected.read(), data); + } + + assertEquals(-1, data); + assertEquals(expected.read(), actual.read()); + } + catch (IOException e) { + fail(String.format("Bad/corrupted data or EOF at offset 0x%04x: %s", count, e.getMessage())); + } + } + + @Override + public Decoder createDecoder() { + return new LZWDecoder(); + } + + @Override + public Encoder createCompatibleEncoder() { + // Don't have an encoder yet + return null; + } +} diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTestCase.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java old mode 100755 new mode 100644 similarity index 52% rename from imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTestCase.java rename to imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 5e97748e..ced889d5 --- a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTestCase.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -1,5 +1,5 @@ -/* - * Copyright (c) 2008, Harald Kuhr +package com.twelvemonkeys.imageio.plugins.tiff;/* + * Copyright (c) 2012, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,8 +26,6 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.plugins.iff; - import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; import javax.imageio.spi.ImageReaderSpi; @@ -36,50 +34,58 @@ import java.util.Arrays; import java.util.List; /** - * IFFImageReaderTestCase + * TIFFImageReaderTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: IFFImageReaderTestCase.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$ + * @version $Id: TIFFImageReaderTest.java,v 1.0 08.05.12 15:25 haraldk Exp$ */ -public class IFFImageReaderTestCase extends ImageReaderAbstractTestCase { - // TODO: Need test for IFF PBM +public class TIFFImageReaderTest extends ImageReaderAbstractTestCase { + + private static final TIFFImageReaderSpi SPI = new TIFFImageReaderSpi(); + + @Override protected List getTestData() { return Arrays.asList( - // 32 bit - Ok - new TestData(getClassLoaderResource("/iff/test.iff"), new Dimension(300, 200)), // 32 bit - // 24 bit - Ok - new TestData(getClassLoaderResource("/iff/survivor.iff"), new Dimension(800, 600)), // 24 bit - // HAM6 - Ok (a lot of visual "fringe", would be interesting to see on a real HAM display) - new TestData(getClassLoaderResource("/iff/A4000T_HAM6.IFF"), new Dimension(320, 512)), // ham6 - // HAM8 - Ok (PackBits decoder chokes on padding byte) - new TestData(getClassLoaderResource("/iff/A4000T_HAM8.IFF"), new Dimension(628, 512)), // ham8 - // 8 color indexed - Ok - new TestData(getClassLoaderResource("/iff/AmigaBig.iff"), new Dimension(300, 200)), // 8 color - // 8 color indexed - Ok - new TestData(getClassLoaderResource("/iff/AmigaAmiga.iff"), new Dimension(200, 150)), // 8 color - // Ok (PackBits decoder chokes on padding byte) - new TestData(getClassLoaderResource("/iff/Abyss.iff"), new Dimension(320, 400)) + new TestData(getClassLoaderResource("/tiff/balloons.tif"), new Dimension(640, 480)), // RGB, uncompressed + new TestData(getClassLoaderResource("/tiff/sm_colors_pb.tif"), new Dimension(64, 64)), // RGB, PackBits compressed + new TestData(getClassLoaderResource("/tiff/sm_colors_tile.tif"), new Dimension(64, 64)), // RGB, uncompressed, tiled + new TestData(getClassLoaderResource("/tiff/sm_colors_pb_tile.tif"), new Dimension(64, 64)), // RGB, PackBits compressed, tiled + new TestData(getClassLoaderResource("/tiff/galaxy.tif"), new Dimension(965, 965)), // RGB, LZW compressed + new TestData(getClassLoaderResource("/tiff/bali.tif"), new Dimension(725, 489)), // Palette-based, LZW compressed + new TestData(getClassLoaderResource("/tiff/f14.tif"), new Dimension(640, 480)), // Gray, uncompressed + new TestData(getClassLoaderResource("/tiff/marbles.tif"), new Dimension(1419, 1001)), // RGB, LZW compressed w/predictor + new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)) // CMYK, uncompressed ); } + @Override protected ImageReaderSpi createProvider() { - return new IFFImageReaderSpi(); + return SPI; } - protected Class getReaderClass() { - return IFFImageReader.class; + @Override + protected Class getReaderClass() { + return TIFFImageReader.class; } + @Override + protected TIFFImageReader createReader() { + return SPI.createReaderInstance(null); + } + + @Override protected List getFormatNames() { - return Arrays.asList("iff"); + return Arrays.asList("tiff", "TIFF"); } + @Override protected List getSuffixes() { - return Arrays.asList("iff", "ilbm", "ham", "ham8", "lbm"); + return Arrays.asList("tif", "tiff"); } + @Override protected List getMIMETypes() { - return Arrays.asList("image/iff", "image/x-iff"); + return Arrays.asList("image/tiff"); } } diff --git a/imageio/imageio-tiff/src/test/resources/lzw/lzw-long.bin b/imageio/imageio-tiff/src/test/resources/lzw/lzw-long.bin new file mode 100644 index 00000000..4ac88acf Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/lzw/lzw-long.bin differ diff --git a/imageio/imageio-tiff/src/test/resources/lzw/lzw-short-45-49.bin b/imageio/imageio-tiff/src/test/resources/lzw/lzw-short-45-49.bin new file mode 100755 index 00000000..17bd1fcd Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/lzw/lzw-short-45-49.bin differ diff --git a/imageio/imageio-tiff/src/test/resources/lzw/lzw-short.bin b/imageio/imageio-tiff/src/test/resources/lzw/lzw-short.bin new file mode 100644 index 00000000..c9ee3755 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/lzw/lzw-short.bin differ diff --git a/imageio/imageio-tiff/src/test/resources/lzw/unpacked-long.bin b/imageio/imageio-tiff/src/test/resources/lzw/unpacked-long.bin new file mode 100644 index 00000000..89c494cb Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/lzw/unpacked-long.bin differ diff --git a/imageio/imageio-tiff/src/test/resources/lzw/unpacked-short-45-49.bin b/imageio/imageio-tiff/src/test/resources/lzw/unpacked-short-45-49.bin new file mode 100644 index 00000000..9fb859fe Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/lzw/unpacked-short-45-49.bin differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/bali.tif b/imageio/imageio-tiff/src/test/resources/tiff/bali.tif new file mode 100644 index 00000000..1ae13b79 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/bali.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/balloons.tif b/imageio/imageio-tiff/src/test/resources/tiff/balloons.tif new file mode 100644 index 00000000..833db35d Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/balloons.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/chifley_logo.tif b/imageio/imageio-tiff/src/test/resources/tiff/chifley_logo.tif new file mode 100644 index 00000000..90fb5eb3 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/chifley_logo.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/f14.tif b/imageio/imageio-tiff/src/test/resources/tiff/f14.tif new file mode 100644 index 00000000..813e0c5e Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/f14.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/galaxy.tif b/imageio/imageio-tiff/src/test/resources/tiff/galaxy.tif new file mode 100644 index 00000000..b90ac4b2 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/galaxy.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/marbles.tif b/imageio/imageio-tiff/src/test/resources/tiff/marbles.tif new file mode 100644 index 00000000..06ccd3ec Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/marbles.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_pb.tif b/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_pb.tif new file mode 100644 index 00000000..29999dba Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_pb.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_pb_tile.tif b/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_pb_tile.tif new file mode 100644 index 00000000..c655f41e Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_pb_tile.tif differ diff --git a/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_tile.tif b/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_tile.tif new file mode 100644 index 00000000..a6bc40d5 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/sm_colors_tile.tif differ diff --git a/imageio/pom.xml b/imageio/pom.xml index 7918a451..f91ba124 100644 --- a/imageio/pom.xml +++ b/imageio/pom.xml @@ -10,11 +10,9 @@ 4.0.0 com.twelvemonkeys.imageio imageio - 3.0-SNAPSHOT pom TwelveMonkeys :: ImageIO - Harald Kuhr @@ -33,16 +31,20 @@ imageio-ico + imageio-icns imageio-iff - imageio-pdf + imageio-jpeg + imageio-pdf imageio-pict imageio-psd imageio-thumbsdb + imageio-tiff imageio-batik imageio-jmagick + imageio-reference @@ -77,7 +79,6 @@ test - junit junit @@ -86,10 +87,9 @@ - jmock - jmock-cglib - 1.0.1 - test + org.mockito + mockito-all + 1.8.5 @@ -100,7 +100,8 @@ imageio-core ${project.version} - + + ${project.groupId} imageio-metadata ${project.version} diff --git a/imageio/todo.txt b/imageio/todo.txt index a0334f8f..d3fbf082 100755 --- a/imageio/todo.txt +++ b/imageio/todo.txt @@ -1,18 +1,19 @@ -- Get vendor name/version for SPIs from manifest. - Package pkg = getClass().getPackage(); - version = pkg.getImplementationVersion(); - vendor = pkg.getImplementationVendor(); - specTitle = pkg.getSpecificationTitle(); - - Consider creating a raw ImageReader (or util class?) that can read raw bitmaps: o Interleaved (A)RGB (as in BMP, PICT, IFF PBM etc) -> A1R1G1B1, A2R2G2B2, ..., AnRnGnNn o Channeled (A)RGB (as in Photoshop) -> A1A2...An, R1R2...Rn, G1G2...Gn, B1B2...Bn o Planar RGB (as in IFF ILBM) -> .... Formats that internally have these structures could delegate to an instance of this class. - Could also be interesting to allow for raw reading using a RawImageReader. + Could also be interesting to allow for raw reading using a RawImageReader. o Would need to specify width, height o bit depth o Pixel layout (planar, channeled, interleaved) o Channel order o Compression? RLE/PackBits/LZW/ZIP? o IndexColorModel? + +DONE: +- Get vendor name/version for SPIs from manifest. + Package pkg = getClass().getPackage(); + version = pkg.getImplementationVersion(); + vendor = pkg.getImplementationVendor(); + specTitle = pkg.getSpecificationTitle(); diff --git a/pom.xml b/pom.xml index e8ea26a3..7a805421 100755 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ common servlet imageio + @@ -40,17 +41,15 @@ - twelvemonkeys-${project.artifactId}-${project.version} - - - + twelvemonkeys-${project.artifactId}-${project.version} + + org.apache.maven.plugins maven-jar-plugin - 2.2 - ${project.name} + twelvemonkeys-${project.artifactId} TwelveMonkeys ${project.version} http://github.com/haraldk/TwelveMonkeys @@ -58,14 +57,21 @@ - - maven-resources-plugin - - UTF-8 - - + + + + + org.apache.maven.plugins + maven-resources-plugin + + UTF-8 + + + + org.apache.maven.plugins maven-jar-plugin + 2.2 true @@ -78,8 +84,10 @@ + org.apache.maven.plugins maven-source-plugin - true + true + package @@ -92,6 +100,7 @@ + org.apache.maven.plugins maven-compiler-plugin true @@ -103,7 +112,22 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + + java.awt.headless + true + + + + + + + org.apache.maven.plugins maven-idea-plugin true 2.2 @@ -114,6 +138,7 @@ + @@ -123,7 +148,7 @@ 1.9 - + @@ -169,5 +194,5 @@ java-net:/maven2-repository/trunk/repository/ - + diff --git a/sandbox/pom.xml b/sandbox/pom.xml new file mode 100644 index 00000000..20decb8b --- /dev/null +++ b/sandbox/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + com.twelvemonkeys.sandbox + sandbox + 3.0-SNAPSHOT + TwelveMonkeys :: Sandbox + pom + + + The TwelveMonkeys Sandbox. Experimental stuff, in progress, not for production use. + + + + com.twelvemonkeys + twelvemonkeys + 3.0-SNAPSHOT + + + + sandbox-common + sandbox-imageio + sandbox-servlet + sandbox-swing + + + + + junit + junit + test + + + + + + + + com.twelvemonkeys.common + common-lang + ${project.version} + compile + + + com.twelvemonkeys.common + common-io + ${project.version} + compile + + + com.twelvemonkeys.common + common-image + ${project.version} + compile + + + com.twelvemonkeys.servlet + servlet + ${project.version} + compile + + + com.twelvemonkeys.swing + swing-core + ${project.version} + compile + + + com.twelvemonkeys.swing + swing-application + ${project.version} + compile + + + com.twelvemonkeys.imageio + imageio-core + ${project.version} + provided + + + + com.twelvemonkeys.common + common-io + ${project.version} + test + tests + + + com.twelvemonkeys.common + common-lang + ${project.version} + test + tests + + + + junit + junit + 4.7 + test + + + + + + + + maven-source-plugin + + + + maven-resources-plugin + + UTF-8 + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + + ${project.name} + TwelveMonkeys + ${project.version} + http://github.com/haraldk/TwelveMonkeys + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + true + + true + + + + + + \ No newline at end of file diff --git a/sandbox/sandbox-common/pom.xml b/sandbox/sandbox-common/pom.xml new file mode 100644 index 00000000..4579dfb4 --- /dev/null +++ b/sandbox/sandbox-common/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.twelvemonkeys.sandbox + sandbox + 3.0-SNAPSHOT + + sandbox-common + jar + TwelveMonkeys :: Sandbox :: Common + + The TwelveMonkeys Common Sandbox. Experimental stuff. + + + + + com.twelvemonkeys.common + common-lang + compile + + + + com.twelvemonkeys.common + common-io + compile + + + + com.twelvemonkeys.common + common-image + compile + + + + com.twelvemonkeys.imageio + imageio-core + compile + + + + com.twelvemonkeys.common + common-io + test + tests + + + + com.twelvemonkeys.common + common-lang + test + tests + + + + diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/ConvolveTester.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/ConvolveTester.java similarity index 100% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/ConvolveTester.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/ConvolveTester.java diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/EasyImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/EasyImage.java similarity index 97% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/EasyImage.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/EasyImage.java index dde4cfe6..71427244 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/EasyImage.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/EasyImage.java @@ -1,78 +1,78 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.image; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.*; -import java.awt.image.renderable.RenderableImage; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * EasyImage - *

    - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java#1 $ - */ -public class EasyImage extends BufferedImage { - public EasyImage(InputStream pInput) throws IOException { - this(ImageIO.read(pInput)); - } - - public EasyImage(BufferedImage pImage) { - this(pImage.getColorModel(), pImage.getRaster()); - } - - public EasyImage(RenderableImage pImage) { - this(pImage.createDefaultRendering()); - } - - public EasyImage(RenderedImage pImage) { - this(pImage.getColorModel(), pImage.copyData(pImage.getColorModel().createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()))); - } - - public EasyImage(ImageProducer pImage) { - this(new BufferedImageFactory(pImage).getBufferedImage()); - } - - public EasyImage(Image pImage) { - this(new BufferedImageFactory(pImage).getBufferedImage()); - } - - private EasyImage(ColorModel cm, WritableRaster raster) { - super(cm, raster, cm.isAlphaPremultiplied(), null); - } - - public boolean write(String pFormat, OutputStream pOutput) throws IOException { - return ImageIO.write(this, pFormat, pOutput); - } -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.*; +import java.awt.image.renderable.RenderableImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * EasyImage + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java#1 $ + */ +public class EasyImage extends BufferedImage { + public EasyImage(InputStream pInput) throws IOException { + this(ImageIO.read(pInput)); + } + + public EasyImage(BufferedImage pImage) { + this(pImage.getColorModel(), pImage.getRaster()); + } + + public EasyImage(RenderableImage pImage) { + this(pImage.createDefaultRendering()); + } + + public EasyImage(RenderedImage pImage) { + this(pImage.getColorModel(), pImage.copyData(pImage.getColorModel().createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()))); + } + + public EasyImage(ImageProducer pImage) { + this(new BufferedImageFactory(pImage).getBufferedImage()); + } + + public EasyImage(Image pImage) { + this(new BufferedImageFactory(pImage).getBufferedImage()); + } + + private EasyImage(ColorModel cm, WritableRaster raster) { + super(cm, raster, cm.isAlphaPremultiplied(), null); + } + + public boolean write(String pFormat, OutputStream pOutput) throws IOException { + return ImageIO.write(this, pFormat, pOutput); + } +} diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/ExtendedImageConsumer.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java similarity index 97% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/ExtendedImageConsumer.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java index 7443d3f2..949d6932 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/ExtendedImageConsumer.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java @@ -1,61 +1,61 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.image; - -import java.awt.image.ImageConsumer; -import java.awt.image.ColorModel; - -/** - * ExtendedImageConsumer - *

    - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java#1 $ - */ -public interface ExtendedImageConsumer extends ImageConsumer { - /** - * - * @param pX - * @param pY - * @param pWidth - * @param pHeight - * @param pModel - * @param pPixels - * @param pOffset - * @param pScanSize - */ - public void setPixels(int pX, int pY, int pWidth, int pHeight, - ColorModel pModel, - short[] pPixels, int pOffset, int pScanSize); - - // Allow for packed and interleaved models - public void setPixels(int pX, int pY, int pWidth, int pHeight, - ColorModel pModel, - byte[] pPixels, int pOffset, int pScanSize); -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.ImageConsumer; +import java.awt.image.ColorModel; + +/** + * ExtendedImageConsumer + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java#1 $ + */ +public interface ExtendedImageConsumer extends ImageConsumer { + /** + * + * @param pX + * @param pY + * @param pWidth + * @param pHeight + * @param pModel + * @param pPixels + * @param pOffset + * @param pScanSize + */ + public void setPixels(int pX, int pY, int pWidth, int pHeight, + ColorModel pModel, + short[] pPixels, int pOffset, int pScanSize); + + // Allow for packed and interleaved models + public void setPixels(int pX, int pY, int pWidth, int pHeight, + ColorModel pModel, + byte[] pPixels, int pOffset, int pScanSize); +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/GenericWritableRaster.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/GenericWritableRaster.java new file mode 100644 index 00000000..e05fc6b2 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/GenericWritableRaster.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; + +/** + * A generic writable raster. + * For use when factory methods from {@link java.awt.image.Raster} can't be used, + * typically because of custom data buffers. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: GenericWritableRaster.java,v 1.0 Jun 13, 2010 12:27:45 AM haraldk Exp$ + */ +class GenericWritableRaster extends WritableRaster { + public GenericWritableRaster(final SampleModel model, final DataBuffer buffer, final Point origin) { + super(model, buffer, origin); + } + + @Override + public String toString() { + return String.format( + "%s: %s width = %s height = %s #Bands = %s xOff = %s yOff = %s %s", + getClass().getSimpleName(), + sampleModel, + getWidth(), getHeight(), getNumBands(), + sampleModelTranslateX, sampleModelTranslateY, + dataBuffer + ); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java new file mode 100644 index 00000000..7a31a119 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java @@ -0,0 +1,574 @@ +/* + * Copyright (c) 2010, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import com.twelvemonkeys.imageio.util.ProgressListenerBase; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.stream.ImageInputStream; +import javax.swing.*; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * MappedBufferImage + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: MappedBufferImage.java,v 1.0 Jun 13, 2010 7:33:19 PM haraldk Exp$ + */ +public class MappedBufferImage { + private static int threads = Runtime.getRuntime().availableProcessors(); + private static ExecutorService executorService = Executors.newFixedThreadPool(threads); + + public static void main(String[] args) throws IOException { + int argIndex = 0; + File file = args.length > 0 ? new File(args[argIndex]) : null; + + int w; + int h; + BufferedImage image; + + if (file != null && file.exists()) { + argIndex++; + + // Load image using ImageIO + ImageInputStream input = ImageIO.createImageInputStream(file); + Iterator readers = ImageIO.getImageReaders(input); + + if (!readers.hasNext()) { + System.err.println("No image reader found for input: " + file.getAbsolutePath()); + System.exit(0); + return; + } + + ImageReader reader = readers.next(); + try { + reader.setInput(input); + + Iterator types = reader.getImageTypes(0); + ImageTypeSpecifier type = types.next(); + + // TODO: Negotiate best layout according to the GraphicsConfiguration. + + w = reader.getWidth(0); + h = reader.getHeight(0); + + // GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + // ColorModel cm2 = configuration.getColorModel(cm.getTransparency()); + + // image = MappedImageFactory.createCompatibleMappedImage(w, h, cm2); + // image = MappedImageFactory.createCompatibleMappedImage(w, h, cm); + // image = MappedImageFactory.createCompatibleMappedImage(w, h, BufferedImage.TYPE_4BYTE_ABGR); + // image = MappedImageFactory.createCompatibleMappedImage(w, h, BufferedImage.TYPE_INT_BGR); +// image = MappedImageFactory.createCompatibleMappedImage(w, h, type); +// if (w > 1024 || h > 1024) { + image = MappedImageFactory.createCompatibleMappedImage(w, h, type); +// } +// else { +// image = type.createBufferedImage(w, h); +// } + + System.out.println("image = " + image); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setDestination(image); + + reader.addIIOReadProgressListener(new ConsoleProgressListener()); + reader.read(0, param); + } + finally { + reader.dispose(); + } + } + else { + w = args.length > argIndex && StringUtil.isNumber(args[argIndex]) ? Integer.parseInt(args[argIndex++]) : 6000; + h = args.length > argIndex && StringUtil.isNumber(args[argIndex]) ? Integer.parseInt(args[argIndex++]) : w * 2 / 3; + + GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + image = MappedImageFactory.createCompatibleMappedImage(w, h, configuration, Transparency.TRANSLUCENT); +// image = MappedImageFactory.createCompatibleMappedImage(w, h, configuration, Transparency.OPAQUE); +// image = MappedImageFactory.createCompatibleMappedImage(w, h, BufferedImage.TYPE_4BYTE_ABGR); + + System.out.println("image = " + image); + + DataBuffer buffer = image.getRaster().getDataBuffer(); + final boolean alpha = image.getColorModel().hasAlpha(); + + // Mix in some nice colors + createBackground(w, h, buffer, alpha); + + // Add some random dots (get out the coffee) + paintDots(w, h, image); + } + + // Resample down to some fixed size + if (args.length > argIndex && "-scale".equals(args[argIndex++])) { + image = resampleImage(image, 800); + } + + int bytesPerPixel = image.getColorModel().getPixelSize() / 8; // Calculate first to avoid overflow + String size = toHumanReadableSize(w * h * bytesPerPixel); + showIt(w, h, image, size); + } + + private static void showIt(final int w, final int h, BufferedImage image, final String size) { + JFrame frame = new JFrame(String.format("Test [%s x %s] (%s)", w, h, size)) { + @Override + public Dimension getPreferredSize() { + // TODO: This looks like a useful util method... + DisplayMode displayMode = getGraphicsConfiguration().getDevice().getDisplayMode(); + Dimension size = super.getPreferredSize(); + + size.width = Math.min(size.width, displayMode.getWidth()); + size.height = Math.min(size.height, displayMode.getHeight()); + + return size; + } + }; + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + JScrollPane scroll = new JScrollPane(new ImageComponent(image)); + scroll.setBorder(BorderFactory.createEmptyBorder()); + frame.add(scroll); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private static BufferedImage resampleImage(final BufferedImage image, final int width) { + long start = System.currentTimeMillis(); + + float aspect = image.getHeight() / (float) image.getWidth(); + int height = Math.round(width * aspect); + + // NOTE: The createCompatibleDestImage takes the byte order/layout into account, unlike the cm.createCompatibleWritableRaster + final BufferedImage output = new ResampleOp(width, height).createCompatibleDestImage(image, null); + + final int inStep = (int) Math.ceil(image.getHeight() / (double) threads); + final int outStep = (int) Math.ceil(height / (double) threads); + + final CountDownLatch latch = new CountDownLatch(threads); + + // Resample image in slices + for (int i = 0; i < threads; i++) { + final int inY = i * inStep; + final int outY = i * outStep; + final int inHeight = Math.min(inStep, image.getHeight() - inY); + final int outHeight = Math.min(outStep, output.getHeight() - outY); + executorService.submit(new Runnable() { + public void run() { + try { + BufferedImage in = image.getSubimage(0, inY, image.getWidth(), inHeight); + BufferedImage out = output.getSubimage(0, outY, width, outHeight); + new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, out); +// new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).resample(in, out, ResampleOp.createFilter(ResampleOp.FILTER_LANCZOS)); +// BufferedImage out = new ResampleOp(width, outHeight, ResampleOp.FILTER_LANCZOS).filter(in, null); +// ImageUtil.drawOnto(output.getSubimage(0, outY, width, outHeight), out); + } + catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } + finally { + latch.countDown(); + } + } + }); + } + +// System.out.println("Starting image scale on single thread, waiting for execution to complete..."); +// BufferedImage output = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS).filter(image, null); + System.out.printf("Started image scale on %d threads, waiting for execution to complete...%n", threads); + + Boolean done = null; + try { + done = latch.await(5L, TimeUnit.MINUTES); + } + catch (InterruptedException ignore) { + } + + System.out.printf("%s scaling image in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start); + System.out.println("image = " + output); + return output; + } + + private static void paintDots(int width, int height, final BufferedImage image) { + long start = System.currentTimeMillis(); + + int s = 300; + int ws = width / s; + int hs = height / s; + + Color[] colors = new Color[] { + Color.WHITE, Color.ORANGE, Color.BLUE, Color.MAGENTA, Color.BLACK, Color.RED, Color.CYAN, + Color.GRAY, Color.GREEN, Color.YELLOW, Color.PINK, Color.LIGHT_GRAY, Color.DARK_GRAY + }; + + CountDownLatch latch = new CountDownLatch(threads); + int step = (int) Math.ceil(hs / (double) threads); + Random r = new Random(); + + for (int i = 0; i < threads; i++) { + executorService.submit(new PaintDotsTask(image, s, ws, colors, r, i * step, i * step + step, latch)); + } + + System.err.printf("Started painting in %d threads, waiting for execution to complete...%n", threads); + + Boolean done = null; + try { + done = latch.await(3L, TimeUnit.MINUTES); + } + catch (InterruptedException ignore) { + } + + System.out.printf("%s painting %d dots in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), Math.max(0, hs - 1) * Math.max(0, ws - 1), System.currentTimeMillis() - start); + } + + private static void paintDots0(BufferedImage image, int s, int ws, Color[] colors, Random r, final int first, final int last) { + for (int y = first; y < last; y++) { + for (int x = 0; x < ws - 1; x++) { + BufferedImage tile = image.getSubimage(x * s, y * s, 2 * s, 2 * s); + Graphics2D g; + try { + g = tile.createGraphics(); + } + catch (OutOfMemoryError e) { + System.gc(); + System.err.println("Out of memory: " + e.getMessage()); + g = tile.createGraphics(); // If this fails, give up + } + + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setComposite(AlphaComposite.SrcOver.derive(r.nextFloat())); + g.setColor(colors[r.nextInt(colors.length)]); + int o = r.nextInt(s) + s / 10; + int c = (2 * s - o) / 2; + g.fillOval(c, c, o, o); + } + finally { + g.dispose(); + } + } + } + } + + private static void createBackground(int w, int h, DataBuffer buffer, boolean alpha) { + long start = System.currentTimeMillis(); + + int step = (int) Math.ceil(h / (double) threads); + + CountDownLatch latch = new CountDownLatch(threads); + for (int i = 0; i < threads; i++) { + executorService.submit(new PaintBackgroundTask(w, h, buffer, alpha, i * step, i * step + step, latch)); + } + System.err.printf("Started painting in %d threads, waiting for execution to complete...%n", threads); + + Boolean done = null; + try { + done = latch.await(3L, TimeUnit.MINUTES); + } + catch (InterruptedException ignore) { + } + + System.out.printf("%s creating background in %d ms%n", (done == null ? "Interrupted" : !done ? "Timed out" : "Done"), System.currentTimeMillis() - start); + } + + private static void paintBackground0(int w, int h, DataBuffer buffer, boolean alpha, final int first, final int last) { + for (int y = first; y < last; y++) { + for (int x = 0; x < w; x++) { + int r = (int) ((x * y * 255.0) / (h * w)); + int g = (int) (((w - x) * y * 255.0) / (h * w)); + int b = (int) ((x * (h - y) * 255.0) / (h * w)); + int a = alpha ? (int) (((w - x) * (h - y) * 255.0) / (h * w)) : 0; + + switch (buffer.getDataType()) { + case DataBuffer.TYPE_BYTE: + int off = (y * w + x) * (alpha ? 4 : 3); + if (alpha) { + buffer.setElem(off++, 255 - a); + buffer.setElem(off++, b); + buffer.setElem(off++, g); + buffer.setElem(off, r); + } + else { + // TODO: Why the RGB / ABGR byte order inconsistency?? + buffer.setElem(off++, r); + buffer.setElem(off++, g); + buffer.setElem(off, b); + } + break; + case DataBuffer.TYPE_INT: + buffer.setElem(y * w + x, (255 - a) << 24 | r << 16 | g << 8 | b); + break; + default: + System.err.println("Transfer type not supported: " + buffer.getDataType()); + } + } + } + } + + private static String toHumanReadableSize(long size) { + return String.format("%,d MB", (long) (size / (double) (1024L << 10))); + } + + /** + * A fairly optimized component for displaying a BufferedImage + */ + private static class ImageComponent extends JComponent implements Scrollable { + private final BufferedImage image; + private Paint texture; + double zoom = 1; + + public ImageComponent(final BufferedImage image) { + setOpaque(true); // Very important when subclassing JComponent... + this.image = image; + } + + @Override + public void addNotify() { + super.addNotify(); + + texture = createTexture(); + } + + private Paint createTexture() { + BufferedImage pattern = getGraphicsConfiguration().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())); + } + + @Override + protected void paintComponent(Graphics g) { + // TODO: Figure out why mouse wheel/track pad scroll repaints entire component, + // unlike using the scroll bars of the JScrollPane. + // Consider creating a custom mouse wheel listener as a workaround. + + // We want to paint only the visible part of the image + Rectangle visible = getVisibleRect(); + Rectangle clip = g.getClipBounds(); + Rectangle rect = clip == null ? visible : visible.intersection(clip); + + Graphics2D g2 = (Graphics2D) g; + g2.setPaint(texture); + g2.fillRect(rect.x, rect.y, rect.width, rect.height); + + if (zoom != 1) { + AffineTransform transform = AffineTransform.getScaleInstance(zoom, zoom); + g2.setTransform(transform); + } + + long start = System.currentTimeMillis(); + repaintImage(rect, g2); + System.err.println("repaint: " + (System.currentTimeMillis() - start) + " ms"); + } + + private void repaintImage(Rectangle rect, Graphics2D g2) { + try { + // Paint tiles of the image, to preserve memory + int sliceSize = 200; + + int slicesW = rect.width / sliceSize; + int slicesH = rect.height / sliceSize; + + for (int sliceY = 0; sliceY <= slicesH; sliceY++) { + for (int sliceX = 0; sliceX <= slicesW; sliceX++) { + int x = rect.x + sliceX * sliceSize; + int y = rect.y + sliceY * sliceSize; + + int w = sliceX == slicesW ? Math.min(sliceSize, rect.x + rect.width - x) : sliceSize; + int h = sliceY == slicesH ? Math.min(sliceSize, rect.y + rect.height - y) : sliceSize; + + if (w == 0 || h == 0) { + continue; + } + +// System.err.printf("%04d, %04d, %04d, %04d%n", x, y, w, h); + BufferedImage img = image.getSubimage(x, y, w, h); + g2.drawImage(img, x, y, null); + } + } + +// BufferedImage img = image.getSubimage(rect.x, rect.y, rect.width, rect.height); +// g2.drawImage(img, rect.x, rect.y, null); + } + catch (NullPointerException e) { +// e.printStackTrace(); + // Happens whenever apple.awt.OSXCachingSufraceManager runs out of memory + // TODO: Figure out why repaint(x,y,w,h) doesn't work any more..? + repaint(); // NOTE: Might cause a brief flash while the component is redrawn + } + } + + @Override + public Dimension getPreferredSize() { + return new Dimension((int) (image.getWidth() * zoom), (int) (image.getHeight() * zoom)); + } + + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + return 10; + } + + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + switch (orientation) { + case SwingConstants.HORIZONTAL: + return visibleRect.width * 3 / 4; + case SwingConstants.VERTICAL: + default: + return visibleRect.height * 3 / 4; + } + } + + public boolean getScrollableTracksViewportWidth() { + return false; + } + + public boolean getScrollableTracksViewportHeight() { + return false; + } + } + + private static class PaintDotsTask implements Runnable { + private final BufferedImage image; + private final int s; + private final int wstep; + private final Color[] colors; + private final Random random; + private final int last; + private final int first; + private final CountDownLatch latch; + + public PaintDotsTask(BufferedImage image, int s, int wstep, Color[] colors, Random random, int first, int last, CountDownLatch latch) { + this.image = image; + this.s = s; + this.wstep = wstep; + this.colors = colors; + this.random = random; + this.last = last; + this.first = first; + this.latch = latch; + } + + public void run() { + try { + paintDots0(image, s, wstep, colors, random, first, last); + } + finally { + latch.countDown(); + } + } + } + + private static class PaintBackgroundTask implements Runnable { + private final int w; + private final int h; + private final DataBuffer buffer; + private final boolean alpha; + private final int first; + private final int last; + private final CountDownLatch latch; + + public PaintBackgroundTask(int w, int h, DataBuffer buffer, boolean alpha, int first, int last, CountDownLatch latch) { + this.w = w; + this.h = h; + this.buffer = buffer; + this.alpha = alpha; + this.first = first; + this.last = last; + this.latch = latch; + } + + public void run() { + try { + paintBackground0(w, h, buffer, alpha, first, last); + } + finally { + latch.countDown(); + } + } + } + + private static class ConsoleProgressListener extends ProgressListenerBase { + static final int COLUMNS = System.getenv("COLUMNS") != null ? Integer.parseInt(System.getenv("COLUMNS")) - 2 : 78; + int left = COLUMNS; + + @Override + public void imageComplete(ImageReader source) { + for (; left > 0; left--) { + System.out.print("."); + } + System.out.println("]"); + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + int progress = COLUMNS - Math.round(COLUMNS * percentageDone / 100f); + if (progress < left) { + for (; left > progress; left--) { + System.out.print("."); + } + } + } + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + System.out.print("["); + } + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedFileBuffer.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedFileBuffer.java new file mode 100644 index 00000000..ff1acafc --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedFileBuffer.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2010, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import com.twelvemonkeys.lang.Validate; + +import java.awt.image.DataBuffer; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.*; +import java.nio.channels.FileChannel; + +/** + * A {@code DataBuffer} implementation that is backed by a memory mapped file. + * Memory will be allocated outside the normal JVM heap, allowing more efficient + * memory usage for large buffers. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: MappedFileBuffer.java,v 1.0 Jun 12, 2010 4:56:51 PM haraldk Exp$ + * + * @see java.nio.channels.FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long) + */ +public abstract class MappedFileBuffer extends DataBuffer { + private final Buffer buffer; + + private MappedFileBuffer(final int type, final int size, final int numBanks) throws IOException { + super(type, Validate.isTrue(size >= 0, size, "Integer overflow for size: %d"), Validate.isTrue(numBanks >= 0, numBanks, "Number of banks must be positive")); + + int componentSize = DataBuffer.getDataTypeSize(type) / 8; + + // Create temp file to get a file handle to use for memory mapping + File tempFile = File.createTempFile(String.format("%s-", getClass().getSimpleName().toLowerCase()), ".tmp"); + + try { + RandomAccessFile raf = new RandomAccessFile(tempFile, "rw"); + + long length = ((long) size) * componentSize * numBanks; + + raf.setLength(length); + FileChannel channel = raf.getChannel(); + + // Map entire file into memory, let OS virtual memory/paging do the heavy lifting + MappedByteBuffer byteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length); + + switch (type) { + case DataBuffer.TYPE_BYTE: + buffer = byteBuffer; + break; + case DataBuffer.TYPE_USHORT: + buffer = byteBuffer.asShortBuffer(); + break; + case DataBuffer.TYPE_INT: + buffer = byteBuffer.asIntBuffer(); + break; + default: + throw new IllegalArgumentException("Unsupported data type: " + type); + } + + // According to the docs, we can safely close the channel and delete the file now + channel.close(); + } + finally { + // NOTE: File can't be deleted right now on Windows, as the file is open. Let JVM clean up later + if (!tempFile.delete()) { + tempFile.deleteOnExit(); + } + } + } + + @Override + public String toString() { + return String.format("MappedFileBuffer: %s", buffer); + } + + // TODO: Is throws IOException a good idea? + + public static DataBuffer create(final int type, final int size, final int numBanks) throws IOException { + switch (type) { + case DataBuffer.TYPE_BYTE: + return new DataBufferByte(size, numBanks); + case DataBuffer.TYPE_USHORT: + return new DataBufferUShort(size, numBanks); + case DataBuffer.TYPE_INT: + return new DataBufferInt(size, numBanks); + default: + throw new IllegalArgumentException("Unsupported data type: " + type); + } + } + + final static class DataBufferByte extends MappedFileBuffer { + private final ByteBuffer buffer; + + public DataBufferByte(int size, int numBanks) throws IOException { + super(DataBuffer.TYPE_BYTE, size, numBanks); + buffer = (ByteBuffer) super.buffer; + } + + @Override + public int getElem(int bank, int i) { + return buffer.get(bank * size + i) & 0xff; + } + + @Override + public void setElem(int bank, int i, int val) { + buffer.put(bank * size + i, (byte) val); + } + } + + final static class DataBufferUShort extends MappedFileBuffer { + private final ShortBuffer buffer; + + public DataBufferUShort(int size, int numBanks) throws IOException { + super(DataBuffer.TYPE_USHORT, size, numBanks); + buffer = (ShortBuffer) super.buffer; + } + + @Override + public int getElem(int bank, int i) { + return buffer.get(bank * size + i) & 0xffff; + } + + @Override + public void setElem(int bank, int i, int val) { + buffer.put(bank * size + i, (short) val); + } + } + + final static class DataBufferInt extends MappedFileBuffer { + private final IntBuffer buffer; + + public DataBufferInt(int size, int numBanks) throws IOException { + super(DataBuffer.TYPE_INT, size, numBanks); + buffer = (IntBuffer) super.buffer; + } + + @Override + public int getElem(int bank, int i) { + return buffer.get(bank * size + i); + } + + @Override + public void setElem(int bank, int i, int val) { + buffer.put(bank * size + i, val); + } + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java new file mode 100644 index 00000000..3347dd38 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageTypeSpecifier; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; +import java.io.IOException; + +/** + * A factory for creating {@link BufferedImage}s backed by memory mapped files. + * The data buffers will be allocated outside the normal JVM heap, allowing more efficient + * memory usage for large images. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: MappedImageFactory.java,v 1.0 May 26, 2010 5:07:01 PM haraldk Exp$ + */ +public final class MappedImageFactory { + + // TODO: Create a way to do ColorConvertOp (or other color space conversion) on these images. + // - Current implementation of CCOp delegates to internal sun.awt classes that assumes java.awt.DataBufferByte for type byte buffers :-/ + + private MappedImageFactory() {} + + public static BufferedImage createCompatibleMappedImage(int width, int height, int type) throws IOException { + BufferedImage temp = new BufferedImage(1, 1, type); + return createCompatibleMappedImage(width, height, temp.getSampleModel().createCompatibleSampleModel(width, height), temp.getColorModel()); + } + + public static BufferedImage createCompatibleMappedImage(int width, int height, GraphicsConfiguration configuration, int transparency) throws IOException { + // TODO: Should we also use the sample model? + return createCompatibleMappedImage(width, height, configuration.getColorModel(transparency)); + } + + public static BufferedImage createCompatibleMappedImage(int width, int height, ImageTypeSpecifier type) throws IOException { + return createCompatibleMappedImage(width, height, type.getSampleModel(width, height), type.getColorModel()); + } + + static BufferedImage createCompatibleMappedImage(int width, int height, ColorModel cm) throws IOException { + return createCompatibleMappedImage(width, height, cm.createCompatibleSampleModel(width, height), cm); + } + + static BufferedImage createCompatibleMappedImage(int width, int height, SampleModel sm, ColorModel cm) throws IOException { + DataBuffer buffer = MappedFileBuffer.create(sm.getTransferType(), width * height * sm.getNumDataElements(), 1); + + return new BufferedImage(cm, new GenericWritableRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null); + } +} diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/SubsampleTester.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/SubsampleTester.java similarity index 100% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/image/SubsampleTester.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/SubsampleTester.java diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/inv_cmap.c b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/inv_cmap.c similarity index 100% rename from common/common-image/src/main/java/com/twelvemonkeys/image/inv_cmap.c rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/inv_cmap.c diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/FileLockingTest.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/FileLockingTest.java new file mode 100755 index 00000000..101a1179 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/FileLockingTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2009, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS 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 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, 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. + */ + +package com.twelvemonkeys.io; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; + +/** + * FileLockingTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FileLockingTest.java,v 1.0 May 12, 2009 7:15:38 PM haraldk Exp$ + */ +public class FileLockingTest { + public static void main(final String[] pArgs) throws IOException { + FileChannel channel = new RandomAccessFile(pArgs[0], "rw").getChannel(); + FileLock lock = channel.tryLock(0, Long.MAX_VALUE, pArgs.length <= 1 || !"false".equalsIgnoreCase(pArgs[1])); // Shared lock for entire file + + System.out.println("lock: " + lock); + + if (lock != null) { + System.in.read(); + + InputStream stream = Channels.newInputStream(channel); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } + else { + System.out.println("Already locked"); + } + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/FileMonitor.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/FileMonitor.java new file mode 100755 index 00000000..84584c78 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/FileMonitor.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2009, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS 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 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, 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. + */ + +package com.twelvemonkeys.io; + +import java.io.File; + +/** + * Allows monitoring a file on the file system. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FileMonitor.java,v 1.0 May 12, 2009 6:58:55 PM haraldk Exp$ + */ +public class FileMonitor { + + + + /** + * Listens for changes to a file. + * + */ + public interface FileChangeListener { + + public void fileCreated(File pFile) throws Exception; // TODO: Is this a good thing? + + public void fileUpdated(File pFile) throws Exception; + + public void fileDeleted(File pFile) throws Exception; + } + +} diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/DeflateEncoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java similarity index 88% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/DeflateEncoder.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java index 0b99f958..c457a85d 100644 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/DeflateEncoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java @@ -46,8 +46,8 @@ import java.util.zip.Deflater; */ final class DeflateEncoder implements Encoder { - private final Deflater mDeflater; - private final byte[] mBuffer = new byte[1024]; + private final Deflater deflater; + private final byte[] buffer = new byte[1024]; public DeflateEncoder() { // this(new Deflater()); @@ -59,32 +59,32 @@ final class DeflateEncoder implements Encoder { throw new IllegalArgumentException("deflater == null"); } - mDeflater = pDeflater; + deflater = pDeflater; } public void encode(final OutputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { System.out.println("DeflateEncoder.encode"); - mDeflater.setInput(pBuffer, pOffset, pLength); + deflater.setInput(pBuffer, pOffset, pLength); flushInputToStream(pStream); } private void flushInputToStream(final OutputStream pStream) throws IOException { System.out.println("DeflateEncoder.flushInputToStream"); - if (mDeflater.needsInput()) { + if (deflater.needsInput()) { System.out.println("Foo"); } - while (!mDeflater.needsInput()) { - int deflated = mDeflater.deflate(mBuffer, 0, mBuffer.length); - pStream.write(mBuffer, 0, deflated); + while (!deflater.needsInput()) { + int deflated = deflater.deflate(buffer, 0, buffer.length); + pStream.write(buffer, 0, deflated); System.out.println("flushed " + deflated); } } // public void flush() { -// mDeflater.finish(); +// deflater.finish(); // } } diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/InflateDecoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java similarity index 88% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/InflateDecoder.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java index 57d8259a..438bdbba 100644 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/io/enc/InflateDecoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java @@ -49,9 +49,9 @@ import java.util.zip.Inflater; */ final class InflateDecoder implements Decoder { - private final Inflater mInflater; + private final Inflater inflater; - private final byte[] mBuffer; + private final byte[] buffer; /** * Creates an {@code InflateDecoder} @@ -71,20 +71,20 @@ final class InflateDecoder implements Decoder { throw new IllegalArgumentException("inflater == null"); } - mInflater = pInflater; - mBuffer = new byte[1024]; + inflater = pInflater; + buffer = new byte[1024]; } public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { try { int decoded; - while ((decoded = mInflater.inflate(pBuffer, 0, pBuffer.length)) == 0) { - if (mInflater.finished() || mInflater.needsDictionary()) { + while ((decoded = inflater.inflate(pBuffer, 0, pBuffer.length)) == 0) { + if (inflater.finished() || inflater.needsDictionary()) { return 0; } - if (mInflater.needsInput()) { + if (inflater.needsInput()) { fill(pStream); } } @@ -98,12 +98,12 @@ final class InflateDecoder implements Decoder { } private void fill(final InputStream pStream) throws IOException { - int available = pStream.read(mBuffer, 0, mBuffer.length); + int available = pStream.read(buffer, 0, buffer.length); if (available == -1) { throw new EOFException("Unexpected end of ZLIB stream"); } - mInflater.setInput(mBuffer, 0, available); + inflater.setInput(buffer, 0, available); } } \ No newline at end of file diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/DuckType.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/DuckType.java new file mode 100755 index 00000000..97eb33d1 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/DuckType.java @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +/** + * “If it walks like a duck, looks like a duck, quacks like a duck, it must be…”. + *

    + * Based on an idea found at + * The Visual Editor + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/lang/DuckType.java#1 $ + * + * @see java.lang.reflect.Proxy + */ +public final class DuckType { + /* + EXAMPLE: + public ImageMgr(Object receiver, Image image) { + if (!DuckType.instanceOf(IImageHolder.class, receiver)) { + throw new ClassCastException("Cannot implement IImageHolder"); + } + + this.image = image; + + IImageHolder imageHolder = (IImageHolder) DuckType.implement(IImageHolder.class, receiver); + imageHolder.setImage(image); + imageHolder.addDisposeListener(this); + } + */ + + // TODO: Implement some weak caching of proxy classes and instances + // TODO: Make the proxy classes serializable... + + private DuckType() {} + + public static boolean instanceOf(Class pInterface, Object pObject) { + return instanceOf(new Class[] {pInterface}, new Object[] {pObject}); + } + + public static boolean instanceOf(Class[] pInterfaces, Object pObject) { + return instanceOf(pInterfaces, new Object[] {pObject}); + } + + public static boolean instanceOf(final Class[] pInterfaces, final Object[] pObjects) { + // Get all methods of all Class in pInterfaces, and see if pObjects has + // matching implementations + + // TODO: Possible optimization: If any of the interfaces are implemented + // by one of the objects' classes, we don't need to find every method... + + for (int i = 0; i < pInterfaces.length; i++) { + Class interfce = pInterfaces[i]; + + Method[] methods = interfce.getMethods(); + + for (int j = 0; j < methods.length; j++) { + Method method = methods[j]; + + //if (findMethodImplementation(method, getClasses(pObjects)) < 0) { + //if (findMethodImplementation(method, getClasses(pObjects)) == null) { + if (findMethodImplementation(method, pObjects) == null) { + return false; + } + } + } + + return true; + } + + // TODO: Might be moved to ReflectUtil + private static Class[] getClasses(final Object[] pObjects) { + Class[] classes = new Class[pObjects.length]; + + for (int i = 0; i < pObjects.length; i++) { + classes[i] = pObjects[i].getClass(); + } + + return classes; + } + + /** + * Searches for a class that has a method maching the given signature. + * Returns the index of the class in the {@code pClasses} array that has a + * matching method. + * If there is more than one class that has a matching method the first + * index will be returned. + * If there is no match in any of the classes, {@code -1} is returned. + * + * @param pMethod + * @param pObjects + * + * @return the first index of the object in the {@code pObjects} array that + * has a matching method, or {@code -1} if none was found. + */ + // TODO: Might be moved to ReflectUtil + //static int findMethodImplementation(final Method pMethod, final Class[] pClasses) { + static MethodProxy findMethodImplementation(final Method pMethod, final Object[] pObjects) { + // TODO: Optimization: Each getParameterTypes() invokation creates a + // new clone of the array. If we do it once and store the refs, that + // would be a good idea + + // Optimization, don't test class more than once + Set tested = new HashSet(pObjects.length); + + for (int i = 0; i < pObjects.length; i++) { + Class cls = pObjects[i].getClass(); + + if (tested.contains(cls)) { + continue; + } + else { + tested.add(cls); + } + + try { + // NOTE: This test might be too restrictive + // We could actually go ahead with + // supertype parameters or subtype return types... + // However, we should only do that after we have tried all + // classes for direct mathces. + Method method = cls.getMethod(pMethod.getName(), + pMethod.getParameterTypes()); + + if (matches(pMethod, method)) { + //return i; + // TODO: This is a waste of time if we are only testing if there's a method here... + return new MethodProxy(method, pObjects[i]); + } + } + catch (NoSuchMethodException e) { + // Ingore + } + } + + if (hasSuperTypes(pMethod.getParameterTypes())) { + SortedSet uniqueMethods = new TreeSet(); + for (int i = 0; i < pObjects.length; i++) { + Class cls = pObjects[i].getClass(); + + Method[] methods = cls.getMethods(); + + for (int j = 0; j < methods.length; j++) { + Method method = methods[j]; + + // Now, for each method + // 1 test if the name matches + // 2 test if the parameter types match for superclass + // 3 Test return types for assignability? + if (pMethod.getName().equals(method.getName()) + && isAssignableFrom(method.getParameterTypes(), pMethod.getParameterTypes()) + && pMethod.getReturnType().isAssignableFrom(method.getReturnType())) { + // 4 TODO: How to find the most specific match?! + //return new MethodProxy(method, pObjects[i]); + uniqueMethods.add(new MethodProxy(method, pObjects[i])); + } + } + } + if (uniqueMethods.size() == 1) { + return (MethodProxy) uniqueMethods.first(); + } + else { + // TODO: We need to figure out what method is the best match.. + } + } + + //return -1; + return null; + } + + private static boolean isAssignableFrom(Class[] pTypes, Class[] pSubTypes) { + if (pTypes.length != pSubTypes.length) { + return false; + } + + for (int i = 0; i < pTypes.length; i++) { + if (!pTypes[i].isAssignableFrom(pSubTypes[i])) { + return false; + } + + } + return true; + } + + private static boolean hasSuperTypes(Class[] pParameterTypes) { + for (int i = 0; i < pParameterTypes.length; i++) { + Class type = pParameterTypes[i]; + + if (type != Object.class + && (type.isInterface() || type.getSuperclass() != null)) { + return true; + } + } + return false; + } + + /** + * Tests two {@code Method}s for match. + * That is, they have same name and equal parameters. + * + * @param pLeft + * @param pRight + * + * @return + * + * @see Method#equals(Object) + */ + private static boolean matches(Method pLeft, Method pRight) { + if (pLeft == pRight) { + return true; + } + else if (pLeft.getName().equals(pRight.getName()) + && pLeft.getReturnType().isAssignableFrom(pRight.getReturnType())) { + + // Avoid unnecessary cloning + Class[] params1 = pLeft.getParameterTypes(); + Class[] params2 = pRight.getParameterTypes(); + if (params1.length == params2.length) { + for (int i = 0; i < params1.length; i++) { + if (params1[i] != params2[i]) { + return false; + } + } + return true; + } + } + + return false; + } + + public static Object implement(Class pInterface, Object pObject) throws NoMatchingMethodException { + return implement(new Class[] {pInterface}, new Object[] {pObject}, false); + } + + public static Object implement(Class[] pInterfaces, Object pObject) throws NoMatchingMethodException { + return implement(pInterfaces, new Object[] {pObject}, false); + } + + // TODO: What about the interfaces pObjects allready implements? + // TODO: Use first object as "identity"? Allow user to supply "indentity" + // that is not exposed as part of the implemented interfaces? + public static Object implement(final Class[] pInterfaces, final Object[] pObjects) throws NoMatchingMethodException { + return implement(pInterfaces, pObjects, false); + } + + public static Object implement(final Class[] pInterfaces, final Object[] pObjects, boolean pStubAbstract) throws NoMatchingMethodException { + Map delegates = new HashMap(pObjects.length * 10); + + for (int i = 0; i < pInterfaces.length; i++) { + Class interfce = pInterfaces[i]; + + Method[] methods = interfce.getMethods(); + + for (int j = 0; j < methods.length; j++) { + Method method = methods[j]; + + //int idx = findMethodImplementation(method, getClasses(pObjects)); + //Method impl = findMethodImplementation(method, getClasses(pObjects)); + MethodProxy impl = findMethodImplementation(method, pObjects); + //if (idx < 0) { + if (impl == null) { + // TODO: Maybe optionally create stubs that fails when invoked?! + if (pStubAbstract) { + impl = MethodProxy.createAbstract(method); + } + else { + throw new NoMatchingMethodException(interfce.getName() + "." + + method.getName() + + parameterTypesToString(method.getParameterTypes())); + } + } + + if (!delegates.containsKey(method)) { + // TODO: Must find the correct object... + //delegates.put(method, new MethodProxy(method, pObjects[idx])); + delegates.put(method, impl); + } + } + } + + // TODO: It's probably not good enough to use the current context class loader + // TODO: Either let user specify classloader directly + // TODO: ...or use one of the classloaders from pInterfaces or pObjects + return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), + pInterfaces, new DelegationHandler(delegates)); + } + + private static String parameterTypesToString(Class[] pTypes) { + StringBuilder buf = new StringBuilder(); + buf.append("("); + if (pTypes != null) { + for (int i = 0; i < pTypes.length; i++) { + if (i > 0) { + buf.append(", "); + } + Class c = pTypes[i]; + buf.append((c == null) ? "null" : c.getName()); + } + } + buf.append(")"); + return buf.toString(); + } + + static class MethodProxy { + private final Method mMethod; + private final Object mDelegate; + + private final static Object ABSTRACT_METHOD_DELEGATE = new Object() { + }; + + public static MethodProxy createAbstract(Method pMethod) { + return new MethodProxy(pMethod, ABSTRACT_METHOD_DELEGATE) { + public Object invoke(Object[] pArguments) throws Throwable { + throw abstractMehthodError(); + } + }; + } + + public MethodProxy(Method pMethod, Object pDelegate) { + if (pMethod == null) { + throw new IllegalArgumentException("method == null"); + } + if (pDelegate == null) { + throw new IllegalArgumentException("delegate == null"); + } + + mMethod = pMethod; + mDelegate = pDelegate; + } + + public Object invoke(Object[] pArguments) throws Throwable { + try { + return mMethod.invoke(mDelegate, pArguments); + } + catch (IllegalAccessException e) { + throw new Error(e); // This is an error in the impl + } + catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + Error abstractMehthodError() { + return new AbstractMethodError(mMethod.toString()); + } + + public int hashCode() { + return mMethod.hashCode() ^ mDelegate.hashCode(); + } + + public boolean equals(Object pOther) { + if (pOther == this) { + return true; + } + if (pOther instanceof MethodProxy) { + MethodProxy other = (MethodProxy) pOther; + return mMethod.equals(other.mMethod) && mDelegate.equals(other.mDelegate); + } + return false; + } + + public String toString() { + return mMethod.toString() + mDelegate.toString(); + } + } + + public static class NoMatchingMethodException extends IllegalArgumentException { + public NoMatchingMethodException() { + super(); + } + + public NoMatchingMethodException(String s) { + super(s); + } + + public NoMatchingMethodException(Exception e) { + super(e.getMessage()); + initCause(e); + } + } + + // TODO: Must handle identity... + // TODO: equals/hashCode + // TODO: Allow clients to pass in Identity subclasses? + private static class DelegationHandler implements InvocationHandler { + private final Map mDelegates; + + public DelegationHandler(Map pDelegates) { + mDelegates = pDelegates; + } + + public final Object invoke(Object pProxy, Method pMethod, Object[] pArguments) + throws Throwable + { + if (pMethod.getDeclaringClass() == Object.class) { + // Intercept equals/hashCode/toString + String name = pMethod.getName(); + if (name.equals("equals")) { + return proxyEquals(pProxy, pArguments[0]); + } + else if (name.equals("hashCode")) { + return proxyHashCode(pProxy); + } + else if (name.equals("toString")) { + return proxyToString(pProxy); + } + + // Other methods are handled by their default Object + // implementations + return pMethod.invoke(this, pArguments); + } + + MethodProxy mp = (MethodProxy) mDelegates.get(pMethod); + + return mp.invoke(pArguments); + } + + protected Integer proxyHashCode(Object pProxy) { + //return new Integer(System.identityHashCode(pProxy)); + return new Integer(mDelegates.hashCode()); + } + + protected Boolean proxyEquals(Object pProxy, Object pOther) { + return pProxy == pOther || + (Proxy.isProxyClass(pOther.getClass()) + && Proxy.getInvocationHandler(pOther) instanceof DelegationHandler + && ((DelegationHandler) Proxy.getInvocationHandler(pOther)).mDelegates.equals(mDelegates)) + ? Boolean.TRUE : Boolean.FALSE; + } + + protected String proxyToString(Object pProxy) { + return pProxy.getClass().getName() + '@' + + Integer.toHexString(pProxy.hashCode()); + } + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java new file mode 100755 index 00000000..9d1f89cf --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java @@ -0,0 +1,89 @@ +package com.twelvemonkeys.lang; + +import java.lang.reflect.UndeclaredThrowableException; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * ExceptionUtil + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java#2 $ + */ +public final class ExceptionUtil { + + /** + * Re-throws an exception, either as-is if the exception was already unchecked, otherwise wrapped in + * a {@link RuntimeException} subtype. + * "Expected" exception types are wrapped in {@link RuntimeException}s directly, while + * "unexpected" exception types are wrapped in {@link java.lang.reflect.UndeclaredThrowableException}s. + * + * @param pThrowable the exception to launder + * @param pExpectedTypes the types of exception the code is expected to throw + */ + /*public*/ static void launder(final Throwable pThrowable, Class... pExpectedTypes) { + if (pThrowable instanceof Error) { + throw (Error) pThrowable; + } + if (pThrowable instanceof RuntimeException) { + throw (RuntimeException) pThrowable; + } + + for (Class expectedType : pExpectedTypes) { + if (expectedType.isInstance(pThrowable)) { + throw new RuntimeException(pThrowable); + } + } + + throw new UndeclaredThrowableException(pThrowable); + } + + @SuppressWarnings({"unchecked", "UnusedDeclaration"}) + static void throwAs(final Class pType, final Throwable pThrowable) throws T { + throw (T) pThrowable; + } + + public static void throwUnchecked(final Throwable pThrowable) { + throwAs(RuntimeException.class, pThrowable); + } + + /*@SafeVarargs*/ + @SuppressWarnings({"unchecked", "varargs"}) + /*public*/ static void handle(final Throwable pThrowable, final ThrowableHandler... pHandlers) { + handleImpl(pThrowable, (ThrowableHandler[]) pHandlers); + } + + private static void handleImpl(final Throwable pThrowable, final ThrowableHandler... pHandlers) { + // TODO: Sort more specific throwable handlers before less specific? + for (ThrowableHandler handler : pHandlers) { + if (handler.handles(pThrowable)) { + handler.handle(pThrowable); + return; + } + } + + // Not handled, re-throw + throwUnchecked(pThrowable); + } + + public static abstract class ThrowableHandler { + private final Class[] throwables; + + protected ThrowableHandler(final Class... pThrowables) { + throwables = notNull(pThrowables).clone(); + } + + final public boolean handles(final Throwable pThrowable) { + for (Class throwable : throwables) { + if (throwable.isAssignableFrom(pThrowable.getClass())) { + return true; + } + } + + return false; + } + + public abstract void handle(T pThrowable); + } +} diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/MostUnfortunateException.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java similarity index 97% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/MostUnfortunateException.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java index 1c8814a9..120f1ff1 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/MostUnfortunateException.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java @@ -1,55 +1,55 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.lang; - -/** - * MostUnfortunateException - *

    - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java#1 $ - */ -class MostUnfortunateException extends RuntimeException { - public MostUnfortunateException() { - this("Most unfortunate."); - } - - public MostUnfortunateException(Throwable pCause) { - this(pCause.getMessage(), pCause); - } - - public MostUnfortunateException(String pMessage, Throwable pCause) { - this(pMessage); - initCause(pCause); - } - - public MostUnfortunateException(String pMessage) { - super("A most unfortunate exception has occured: " + pMessage); - } +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +/** + * MostUnfortunateException + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java#1 $ + */ +class MostUnfortunateException extends RuntimeException { + public MostUnfortunateException() { + this("Most unfortunate."); + } + + public MostUnfortunateException(Throwable pCause) { + this(pCause.getMessage(), pCause); + } + + public MostUnfortunateException(String pMessage, Throwable pCause) { + this(pMessage); + initCause(pCause); + } + + public MostUnfortunateException(String pMessage) { + super("A most unfortunate exception has occured: " + pMessage); + } } \ No newline at end of file diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/NativeLoader.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/NativeLoader.java similarity index 96% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/NativeLoader.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/NativeLoader.java index 98d2d589..8c24ed25 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/NativeLoader.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/NativeLoader.java @@ -33,8 +33,8 @@ import com.twelvemonkeys.util.FilterIterator; import com.twelvemonkeys.util.service.ServiceRegistry; import java.io.*; +import java.util.Collections; import java.util.Iterator; -import java.util.Arrays; /** * NativeLoader @@ -182,6 +182,7 @@ final class NativeLoader { Iterator providers = sRegistry.providers(pLibrary); while (providers.hasNext()) { NativeResourceSPI resourceSPI = providers.next(); + try { return resourceSPI.getClassPathResource(Platform.get()); } @@ -372,27 +373,29 @@ final class NativeLoader { private static class NativeResourceRegistry extends ServiceRegistry { public NativeResourceRegistry() { - super(Arrays.asList(NativeResourceSPI.class).iterator()); + super(Collections.singletonList(NativeResourceSPI.class).iterator()); registerApplicationClasspathSPIs(); } - Iterator providers(String pNativeResource) { - return new FilterIterator(providers(NativeResourceSPI.class), - new NameFilter(pNativeResource)); + Iterator providers(final String nativeResource) { + return new FilterIterator( + providers(NativeResourceSPI.class), + new NameFilter(nativeResource) + ); } } private static class NameFilter implements FilterIterator.Filter { - private final String mName; + private final String name; NameFilter(String pName) { if (pName == null) { throw new IllegalArgumentException("name == null"); } - mName = pName; + name = pName; } public boolean accept(NativeResourceSPI pElement) { - return mName.equals(pElement.getResourceName()); + return name.equals(pElement.getResourceName()); } } } \ No newline at end of file diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/NativeResourceSPI.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java similarity index 97% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/NativeResourceSPI.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java index 5c130f23..2290ef2a 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/lang/NativeResourceSPI.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java @@ -1,99 +1,99 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.lang; - -/** - * Abstract base class for native reource providers to iplement. - *

    - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java#1 $ - */ -public abstract class NativeResourceSPI { - - private final String mResourceName; - - /** - * Creates a {@code NativeResourceSPI} with the given name. - * - * The name will typically be a short string, with the common name of the - * library that is provided, like "JMagick", "JOGL" or similar. - * - * @param pResourceName name of the resource (native library) provided by - * this SPI. - * - * @throws IllegalArgumentException if {@code pResourceName == null} - */ - protected NativeResourceSPI(String pResourceName) { - if (pResourceName == null) { - throw new IllegalArgumentException("resourceName == null"); - } - - mResourceName = pResourceName; - } - - /** - * Returns the name of the resource (native library) provided by this SPI. - * - * The name will typically be a short string, with the common name of the - * library that is provided, like "JMagick", "JOGL" or similar. - *

    - * NOTE: This method is intended for the SPI framework, and should not be - * invoked by client code. - * - * @return the name of the resource provided by this SPI - */ - public final String getResourceName() { - return mResourceName; - } - - /** - * Returns the path to the classpath resource that is suited for the given - * runtime configuration. - *

    - * In the common case, the {@code pPlatform} parameter is - * normalized from the values found in - * {@code System.getProperty("os.name")} and - * {@code System.getProperty("os.arch")}. - * For unknown operating systems and architectures, {@code toString()} on - * the platforms's properties will return the the same value as these properties. - *

    - * NOTE: This method is intended for the SPI framework, and should not be - * invoked by client code. - * - * @param pPlatform the current platform - * @return a {@code String} containing the path to a classpath resource or - * {@code null} if no resource is available. - * - * @see com.twelvemonkeys.lang.Platform.OperatingSystem - * @see com.twelvemonkeys.lang.Platform.Architecture - * @see System#getProperties() - */ - public abstract String getClassPathResource(final Platform pPlatform); -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +/** + * Abstract base class for native reource providers to iplement. + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java#1 $ + */ +public abstract class NativeResourceSPI { + + private final String mResourceName; + + /** + * Creates a {@code NativeResourceSPI} with the given name. + * + * The name will typically be a short string, with the common name of the + * library that is provided, like "JMagick", "JOGL" or similar. + * + * @param pResourceName name of the resource (native library) provided by + * this SPI. + * + * @throws IllegalArgumentException if {@code pResourceName == null} + */ + protected NativeResourceSPI(String pResourceName) { + if (pResourceName == null) { + throw new IllegalArgumentException("resourceName == null"); + } + + mResourceName = pResourceName; + } + + /** + * Returns the name of the resource (native library) provided by this SPI. + * + * The name will typically be a short string, with the common name of the + * library that is provided, like "JMagick", "JOGL" or similar. + *

    + * NOTE: This method is intended for the SPI framework, and should not be + * invoked by client code. + * + * @return the name of the resource provided by this SPI + */ + public final String getResourceName() { + return mResourceName; + } + + /** + * Returns the path to the classpath resource that is suited for the given + * runtime configuration. + *

    + * In the common case, the {@code pPlatform} parameter is + * normalized from the values found in + * {@code System.getProperty("os.name")} and + * {@code System.getProperty("os.arch")}. + * For unknown operating systems and architectures, {@code toString()} on + * the platforms's properties will return the the same value as these properties. + *

    + * NOTE: This method is intended for the SPI framework, and should not be + * invoked by client code. + * + * @param pPlatform the current platform + * @return a {@code String} containing the path to a classpath resource or + * {@code null} if no resource is available. + * + * @see com.twelvemonkeys.lang.Platform.OperatingSystem + * @see com.twelvemonkeys.lang.Platform.Architecture + * @see System#getProperties() + */ + public abstract String getClassPathResource(final Platform pPlatform); +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseConnection.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseConnection.java new file mode 100755 index 00000000..efb5b60f --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseConnection.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + +import com.twelvemonkeys.lang.SystemUtil; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +/** + * A class that holds a JDBC Connection. The class can be configured by a + * properties file. However, the approach is rather lame, and only lets you + * configure one connection... + *

    + * Tested with jConnect (Sybase), I-net Sprinta2000 (MS SQL) and Oracle. + *

    + * @todo be able to register more drivers, trough properties and runtime + * @todo be able to register more connections, trough properties and runtime + *

    + * Example properties file
    + * # filename: com.twelvemonkeys.sql.DatabaseConnection.properties + * driver=com.inet.tds.TdsDriver + * url=jdbc:inetdae7:127.0.0.1:1433?database\=mydb + * user=scott + * password=tiger + * # What do you expect, really? + * logDebug=true + * + * @author Philippe Béal (phbe@iconmedialab.no) + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/DatabaseConnection.java#1 $ + * + * @todo Use org.apache.commons.logging instead of proprietary logging. + * + */ + +public class DatabaseConnection { + + // Default driver + public final static String DEFAULT_DRIVER = "NO_DRIVER"; + // Default URL + public final static String DEFAULT_URL = "NO_URL"; + + protected static String mDriver = null; + protected static String mUrl = null; + + // Default debug is true + // private static boolean debug = true; + + protected static Properties mConfig = null; + //protected static Log mLog = null; + protected static Log mLog = null; + + protected static boolean mInitialized = false; + + // Must be like this... + // http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html :-) + private static DatabaseConnection sInstance = new DatabaseConnection(); + + /** + * Creates the DatabaseConnection. + */ + + private DatabaseConnection() { + init(); + } + + /** + * Gets the single DatabaseConnection instance. + */ + + protected static DatabaseConnection getInstance() { + /* + if (sInstance == null) { + sInstance = new DatabaseConnection(); + sInstance.init(); + } + */ + return sInstance; + } + + /** + * Initializes the DatabaseConnection, called from the constructor. + * + * @exception IllegalStateException if an attempt to call init() is made + * after the instance is allready initialized. + */ + + protected synchronized void init() { + // Make sure init is executed only once! + if (mInitialized) { + throw new IllegalStateException("init() may only be called once!"); + } + + mInitialized = true; + + try { + mConfig = SystemUtil.loadProperties(DatabaseConnection.class); + } + catch (FileNotFoundException fnf) { + // Ignore + } + catch (IOException ioe) { + //LogFactory.getLog(getClass()).error("Caught IOException: ", ioe); + new Log(this).logError(ioe); + //ioe.printStackTrace(); + } + finally { + if (mConfig == null) { + mConfig = new Properties(); + } + } + + mLog = new Log(this, mConfig); + //mLog = LogFactory.getLog(getClass()); + // debug = new Boolean(config.getProperty("debug", "true")).booleanValue(); + // config.list(System.out); + + mDriver = mConfig.getProperty("driver", DEFAULT_DRIVER); + mUrl = mConfig.getProperty("url", DEFAULT_URL); + } + + /** + * Gets the default JDBC Connection. The connection is configured through + * the properties file. + * + * @return the default jdbc Connection + */ + + public static Connection getConnection() { + return getConnection(null, null, getInstance().mUrl); + } + + /** + * Gets a JDBC Connection with the given parameters. The connection is + * configured through the properties file. + * + * @param pUser the database user name + * @param pPassword the password of the database user + * @param pURL the url to connect to + * + * @return a jdbc Connection + */ + + public static Connection getConnection(String pUser, + String pPassword, + String pURL) { + return getInstance().getConnectionInstance(pUser, pPassword, pURL); + + } + + /** + * Gets a JDBC Connection with the given parameters. The connection is + * configured through the properties file. + * + * @param pUser the database user name + * @param pPassword the password of the database user + * @param pURL the url to connect to + * + * @return a jdbc Connection + */ + + protected Connection getConnectionInstance(String pUser, + String pPassword, + String pURL) { + Properties props = (Properties) mConfig.clone(); + + if (pUser != null) { + props.put("user", pUser); + } + if (pPassword != null) { + props.put("password", pPassword); + } + + // props.list(System.out); + + try { + // Load & register the JDBC Driver + if (!DEFAULT_DRIVER.equals(mDriver)) { + Class.forName(mDriver).newInstance(); + } + + Connection conn = DriverManager.getConnection(pURL, props); + + if (mLog.getLogDebug()) { + //if (mLog.isDebugEnabled()) { + DatabaseMetaData dma = conn.getMetaData(); + mLog.logDebug("Connected to " + dma.getURL()); + mLog.logDebug("Driver " + dma.getDriverName()); + mLog.logDebug("Version " + dma.getDriverVersion()); + + //mLog.debug("Connected to " + dma.getURL()); + //mLog.debug("Driver " + dma.getDriverName()); + //mLog.debug("Version " + dma.getDriverVersion()); + } + + return conn; + } + catch (Exception e) { + mLog.logError(e.getMessage()); + + // Get chained excpetions + if (e instanceof SQLException) { + SQLException sqle = (SQLException) e; + while ((sqle = sqle.getNextException()) != null) { + mLog.logWarning(sqle); + } + } + } + return null; + } + +} + diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseProduct.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseProduct.java new file mode 100755 index 00000000..63188ded --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseProduct.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + +import java.io.Serializable; + +/** + * DatabaseProduct + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/DatabaseProduct.java#1 $ + */ +public final class DatabaseProduct implements Serializable { + private static final String UNKNOWN_NAME = "Unknown"; + private static final String GENERIC_NAME = "Generic"; + private static final String CACHE_NAME = "Caché"; + private static final String DB2_NAME = "DB2"; + private static final String MSSQL_NAME = "MSSQL"; + private static final String ORACLE_NAME = "Oracle"; + private static final String POSTGRESS_NAME = "PostgreSQL"; + private static final String SYBASE_NAME = "Sybase"; + + /*public*/ static final DatabaseProduct UNKNOWN = new DatabaseProduct(UNKNOWN_NAME); + public static final DatabaseProduct GENERIC = new DatabaseProduct(GENERIC_NAME); + public static final DatabaseProduct CACHE = new DatabaseProduct(CACHE_NAME); + public static final DatabaseProduct DB2 = new DatabaseProduct(DB2_NAME); + public static final DatabaseProduct MSSQL = new DatabaseProduct(MSSQL_NAME); + public static final DatabaseProduct ORACLE = new DatabaseProduct(ORACLE_NAME); + public static final DatabaseProduct POSTGRES = new DatabaseProduct(POSTGRESS_NAME); + public static final DatabaseProduct SYBASE = new DatabaseProduct(SYBASE_NAME); + + private static final DatabaseProduct[] VALUES = { + GENERIC, CACHE, DB2, MSSQL, ORACLE, POSTGRES, SYBASE, + }; + + private static int sNextOrdinal = -1; + private final int mOrdinal = sNextOrdinal++; + + private final String mKey; + + private DatabaseProduct(String pName) { + mKey = pName; + } + + static int enumSize() { + return sNextOrdinal; + } + + final int id() { + return mOrdinal; + } + + final String key() { + return mKey; + } + + public String toString() { + return mKey + " [id=" + mOrdinal+ "]"; + } + + /** + * Gets the {@code DatabaseProduct} known by the given name. + * + * @param pName + * @return the {@code DatabaseProduct} known by the given name + * @throws IllegalArgumentException if there's no such name + */ + public static DatabaseProduct resolve(String pName) { + if ("ANSI".equalsIgnoreCase(pName) || GENERIC_NAME.equalsIgnoreCase(pName)) { + return GENERIC; + } + else if ("Cache".equalsIgnoreCase(pName) || CACHE_NAME.equalsIgnoreCase(pName)) { + return CACHE; + } + else if (DB2_NAME.equalsIgnoreCase(pName)) { + return DB2; + } + else if (MSSQL_NAME.equalsIgnoreCase(pName)) { + return MSSQL; + } + else if (ORACLE_NAME.equalsIgnoreCase(pName)) { + return ORACLE; + } + else if ("Postgres".equalsIgnoreCase(pName) || POSTGRESS_NAME.equalsIgnoreCase(pName)) { + return POSTGRES; + } + else if (SYBASE_NAME.equalsIgnoreCase(pName)) { + return SYBASE; + } + else { + throw new IllegalArgumentException("Unknown database product \"" + pName + + "\", try any of the known products, or \"Generic\""); + } + } + + private Object readResolve() { + return VALUES[mOrdinal]; // Canonicalize + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseReadable.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseReadable.java new file mode 100755 index 00000000..f966dc7b --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/DatabaseReadable.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + +import java.util.Hashtable; + +/** + * Interface for classes that is to be read from a database, using the + * ObjectReader class. + * + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/DatabaseReadable.java#1 $ + * + * @todo Use JDK logging instead of proprietary logging. + * + */ +public interface DatabaseReadable { + + /** + * Gets the unique identifier of this DatabaseReadable object. + * + * @return An object that uniqely identifies this DatabaseReadable. + */ + + public Object getId(); + + /** + * Sets the unique identifier of this DatabaseReadable object. + * + * @param id An object that uniqely identifies this DatabaseReadable. + */ + + public void setId(Object id); + + /** + * Gets the object to database mapping of this DatabaseReadable. + * + * @return A Hashtable cotaining the database mapping for this + * DatabaseReadable. + */ + + public Hashtable getMapping(); +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/JDBCHelper.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/JDBCHelper.java new file mode 100755 index 00000000..e9847d7f --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/JDBCHelper.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + +/** + * AbstractHelper + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/JDBCHelper.java#1 $ + */ +public abstract class JDBCHelper { + + private static JDBCHelper[] sHelpers = new JDBCHelper[DatabaseProduct.enumSize()]; + + static { + DatabaseProduct product = DatabaseProduct.resolve(System.getProperty("com.twelvemonkeys.sql.databaseProduct", "Generic")); + sHelpers[0] = createInstance(product); + } + + private JDBCHelper() { + } + + private static JDBCHelper createInstance(DatabaseProduct pProduct) { + // Get database name + // Instantiate helper + if (pProduct == DatabaseProduct.GENERIC) { + return new GenericHelper(); + } + else if (pProduct == DatabaseProduct.CACHE) { + return new CacheHelper(); + } + else if (pProduct == DatabaseProduct.DB2) { + return new DB2Helper(); + } + else if (pProduct == DatabaseProduct.MSSQL) { + return new MSSQLHelper(); + } + else if (pProduct == DatabaseProduct.ORACLE) { + return new OracleHelper(); + } + else if (pProduct == DatabaseProduct.POSTGRES) { + return new PostgreSQLHelper(); + } + else if (pProduct == DatabaseProduct.SYBASE) { + return new SybaseHelper(); + } + else { + throw new IllegalArgumentException("Unknown database product, try any of the known products, or \"generic\""); + } + } + + public final static JDBCHelper getInstance() { + return sHelpers[0]; + } + + public final static JDBCHelper getInstance(DatabaseProduct pProuct) { + JDBCHelper helper = sHelpers[pProuct.id()]; + if (helper == null) { + // This is ok, iff sHelpers[pProuct] = helper is an atomic op... + synchronized (sHelpers) { + helper = sHelpers[pProuct.id()]; + if (helper == null) { + helper = createInstance(pProuct); + sHelpers[pProuct.id()] = helper; + } + } + } + return helper; + } + + // Abstract or ANSI SQL implementations of different stuff + + public String getDefaultDriverName() { + return ""; + } + + public String getDefaultURL() { + return "jdbc:{$DRIVER}://localhost:{$PORT}/{$DATABASE}"; + } + + // Vendor specific concrete implementations + + static class GenericHelper extends JDBCHelper { + // Nothing here + } + + static class CacheHelper extends JDBCHelper { + public String getDefaultDriverName() { + return "com.intersys.jdbc.CacheDriver"; + } + + public String getDefaultURL() { + return "jdbc:Cache://localhost:1972/{$DATABASE}"; + } + } + + static class DB2Helper extends JDBCHelper { + public String getDefaultDriverName() { + return "COM.ibm.db2.jdbc.net.DB2Driver"; + } + + public String getDefaultURL() { + return "jdbc:db2:{$DATABASE}"; + } + } + + static class MSSQLHelper extends JDBCHelper { + public String getDefaultDriverName() { + return "com.microsoft.jdbc.sqlserver.SQLServerDriver"; + } + + public String getDefaultURL() { + return "jdbc:microsoft:sqlserver://localhost:1433;databasename={$DATABASE};SelectMethod=cursor"; + } + } + + static class OracleHelper extends JDBCHelper { + public String getDefaultDriverName() { + return "oracle.jdbc.driver.OracleDriver"; + } + + public String getDefaultURL() { + return "jdbc:oracle:thin:@localhost:1521:{$DATABASE}"; + } + } + + static class PostgreSQLHelper extends JDBCHelper { + public String getDefaultDriverName() { + return "org.postgresql.Driver"; + } + + public String getDefaultURL() { + return "jdbc:postgresql://localhost/{$DATABASE}"; + } + } + + static class SybaseHelper extends JDBCHelper { + public String getDefaultDriverName() { + return "com.sybase.jdbc2.jdbc.SybDriver"; + } + + public String getDefaultURL() { + return "jdbc:sybase:Tds:localhost:4100/"; + } + } +} \ No newline at end of file diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/Log.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/Log.java new file mode 100755 index 00000000..df7b51d2 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/Log.java @@ -0,0 +1,673 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + + +import com.twelvemonkeys.lang.SystemUtil; + +import java.io.*; +import java.util.Date; +import java.util.Hashtable; +import java.util.Properties; + +/** + * Class used for logging. + * The class currently supports four levels of logging (debug, warning, error + * and info). + *

    + * The class maintains a cahce of OutputStreams, to avoid more than one stream + * logging to a specific file. The class should also be thread safe, in that no + * more than one instance of the class can log to the same OuputStream. + *

    + * + * WARNING: The uniqueness of logfiles is based on filenames alone, meaning + * "info.log" and "./info.log" will probably be treated as different files, + * and have different streams attatched to them. + * + *

    + * + * WARNING: The cached OutputStreams can possibly be in error state or be + * closed without warning. Should be fixed in later versions! + * + * + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/Log.java#1 $ + * + * @deprecated Use the JDK java.util.logging for logging. + * This class is old and outdated, and is here only for compatibility. It will + * be removed from the library in later releases. + *

    + * All new code are strongly encouraged to use the org.apache.commons.logging + * package for logging. + * + * @see java.util.logging.Logger + * + */ + +class Log { + private static Hashtable streamCache = new Hashtable(); + + static { + streamCache.put("System.out", System.out); + streamCache.put("System.err", System.err); + } + + private static Log globalLog = null; + + private String owner = null; + + private boolean logDebug = false; + private boolean logWarning = false; + private boolean logError = true; // Log errors! + private boolean logInfo = false; + + private PrintStream debugLog = null; + private PrintStream warningLog = null; + private PrintStream errorLog = null; + private PrintStream infoLog = null; + + /** + * Init global log + */ + + static { + Properties config = null; + try { + config = SystemUtil.loadProperties(Log.class); + } + catch (FileNotFoundException fnf) { + // That's okay. + } + catch (IOException ioe) { + // Not so good + log(System.err, "ERROR", Log.class.getName(), null, ioe); + } + + globalLog = new Log(new Log(), config); + + // Defaults + if (globalLog.debugLog == null) + globalLog.setDebugLog(System.out); + if (globalLog.warningLog == null) + globalLog.setWarningLog(System.err); + if (globalLog.errorLog == null) + globalLog.setErrorLog(System.err); + if (globalLog.infoLog == null) + globalLog.setInfoLog(System.out); + + // Info + globalLog.logDebug("Logging system started."); + log(globalLog.infoLog, "INFO", Log.class.getName(), + "Logging system started.", null); + } + + /** + * Internal use only + */ + + private Log() { + } + + /** + * Creates a log + */ + + public Log(Object owner) { + this.owner = owner.getClass().getName(); + } + + /** + * Creates a log + */ + + public Log(Object owner, Properties config) { + this(owner); + + if (config == null) + return; + + // Set logging levels + logDebug = new Boolean(config.getProperty("logDebug", + "false")).booleanValue(); + logWarning = new Boolean(config.getProperty("logWarning", + "false")).booleanValue(); + logError = new Boolean(config.getProperty("logError", + "true")).booleanValue(); + logInfo = new Boolean(config.getProperty("logInfo", + "true")).booleanValue(); + + // Set logging streams + String fileName; + try { + if ((fileName = config.getProperty("debugLog")) != null) + setDebugLog(fileName); + + if ((fileName = config.getProperty("warningLog")) != null) + setWarningLog(fileName); + + if ((fileName = config.getProperty("errorLog")) != null) + setErrorLog(fileName); + + if ((fileName = config.getProperty("infoLog")) != null) + setInfoLog(fileName); + } + catch (IOException ioe) { + if (errorLog == null) + setErrorLog(System.err); + logError("Could not create one or more logging streams! ", ioe); + } + } + + /** + * Checks if we log debug info + * + * @return True if logging + */ + + public boolean getLogDebug() { + return logDebug; + } + + /** + * Sets wheter we are to log debug info + * + * @param logDebug Boolean, true if we want to log debug info + */ + + public void setLogDebug(boolean logDebug) { + this.logDebug = logDebug; + } + + /** + * Checks if we globally log debug info + * + * @return True if global logging + */ + /* + public static boolean getGlobalDebug() { + return globalDebug; + } + */ + /** + * Sets wheter we are to globally log debug info + * + * @param logDebug Boolean, true if we want to globally log debug info + */ + /* + public static void setGlobalDebug(boolean globalDebug) { + Log.globalDebug = globalDebug; + } + /* + /** + * Sets the OutputStream we want to print to + * + * @param os The OutputStream we will use for logging + */ + + public void setDebugLog(OutputStream os) { + debugLog = new PrintStream(os, true); + } + + /** + * Sets the filename of the File we want to print to. Equivalent to + * setDebugLog(new FileOutputStream(fileName, true)) + * + * @param file The File we will use for logging + * @see #setDebugLog(OutputStream) + */ + + public void setDebugLog(String fileName) throws IOException { + setDebugLog(getStream(fileName)); + } + + /** + * Prints debug info to the current debugLog + * + * @param message The message to log + * @see #logDebug(String, Exception) + */ + + public void logDebug(String message) { + logDebug(message, null); + } + + /** + * Prints debug info to the current debugLog + * + * @param exception An Exception + * @see #logDebug(String, Exception) + */ + + public void logDebug(Exception exception) { + logDebug(null, exception); + } + + + /** + * Prints debug info to the current debugLog + * + * @param message The message to log + * @param exception An Exception + */ + + public void logDebug(String message, Exception exception) { + if (!(logDebug || globalLog.logDebug)) + return; + + if (debugLog != null) + log(debugLog, "DEBUG", owner, message, exception); + else + log(globalLog.debugLog, "DEBUG", owner, message, exception); + } + + // WARNING + + /** + * Checks if we log warning info + * + * @return True if logging + */ + + public boolean getLogWarning() { + return logWarning; + } + + /** + * Sets wheter we are to log warning info + * + * @param logWarning Boolean, true if we want to log warning info + */ + + public void setLogWarning(boolean logWarning) { + this.logWarning = logWarning; + } + + /** + * Checks if we globally log warning info + * + * @return True if global logging + */ + /* + public static boolean getGlobalWarning() { + return globalWarning; + } + */ + /** + * Sets wheter we are to globally log warning info + * + * @param logWarning Boolean, true if we want to globally log warning info + */ + /* + public static void setGlobalWarning(boolean globalWarning) { + Log.globalWarning = globalWarning; + } + */ + /** + * Sets the OutputStream we want to print to + * + * @param os The OutputStream we will use for logging + */ + + public void setWarningLog(OutputStream os) { + warningLog = new PrintStream(os, true); + } + + /** + * Sets the filename of the File we want to print to. Equivalent to + * setWarningLog(new FileOutputStream(fileName, true)) + * + * @param file The File we will use for logging + * @see #setWarningLog(OutputStream) + */ + + public void setWarningLog(String fileName) throws IOException { + setWarningLog(getStream(fileName)); + } + + /** + * Prints warning info to the current warningLog + * + * @param message The message to log + * @see #logWarning(String, Exception) + */ + + public void logWarning(String message) { + logWarning(message, null); + } + + /** + * Prints warning info to the current warningLog + * + * @param exception An Exception + * @see #logWarning(String, Exception) + */ + + public void logWarning(Exception exception) { + logWarning(null, exception); + } + + + /** + * Prints warning info to the current warningLog + * + * @param message The message to log + * @param exception An Exception + */ + + public void logWarning(String message, Exception exception) { + if (!(logWarning || globalLog.logWarning)) + return; + + if (warningLog != null) + log(warningLog, "WARNING", owner, message, exception); + else + log(globalLog.warningLog, "WARNING", owner, message, exception); + } + + // ERROR + + /** + * Checks if we log error info + * + * @return True if logging + */ + + public boolean getLogError() { + return logError; + } + + /** + * Sets wheter we are to log error info + * + * @param logError Boolean, true if we want to log error info + */ + + public void setLogError(boolean logError) { + this.logError = logError; + } + + /** + * Checks if we globally log error info + * + * @return True if global logging + */ + /* + public static boolean getGlobalError() { + return globalError; + } + */ + /** + * Sets wheter we are to globally log error info + * + * @param logError Boolean, true if we want to globally log error info + */ + /* + public static void setGlobalError(boolean globalError) { + Log.globalError = globalError; + } + */ + /** + * Sets the OutputStream we want to print to + * + * @param os The OutputStream we will use for logging + */ + + public void setErrorLog(OutputStream os) { + errorLog = new PrintStream(os, true); + } + + /** + * Sets the filename of the File we want to print to. Equivalent to + * setErrorLog(new FileOutputStream(fileName, true)) + * + * @param file The File we will use for logging + * @see #setErrorLog(OutputStream) + */ + + public void setErrorLog(String fileName) throws IOException { + setErrorLog(getStream(fileName)); + } + + /** + * Prints error info to the current errorLog + * + * @param message The message to log + * @see #logError(String, Exception) + */ + + public void logError(String message) { + logError(message, null); + } + + /** + * Prints error info to the current errorLog + * + * @param exception An Exception + * @see #logError(String, Exception) + */ + + public void logError(Exception exception) { + logError(null, exception); + } + + /** + * Prints error info to the current errorLog + * + * @param message The message to log + * @param exception An Exception + */ + + public void logError(String message, Exception exception) { + if (!(logError || globalLog.logError)) + return; + + if (errorLog != null) + log(errorLog, "ERROR", owner, message, exception); + else + log(globalLog.errorLog, "ERROR", owner, message, exception); + } + + // INFO + + /** + * Checks if we log info info + * + * @return True if logging + */ + + public boolean getLogInfo() { + return logInfo; + } + + /** + * Sets wheter we are to log info info + * + * @param logInfo Boolean, true if we want to log info info + */ + + public void setLogInfo(boolean logInfo) { + this.logInfo = logInfo; + } + + /** + * Checks if we globally log info info + * + * @return True if global logging + */ + /* + public static boolean getGlobalInfo() { + return globalInfo; + } + */ + /** + * Sets wheter we are to globally log info info + * + * @param logInfo Boolean, true if we want to globally log info info + */ + /* + public static void setGlobalInfo(boolean globalInfo) { + Log.globalInfo = globalInfo; + } + */ + /** + * Sets the OutputStream we want to print to + * + * @param os The OutputStream we will use for logging + */ + + public void setInfoLog(OutputStream os) { + infoLog = new PrintStream(os, true); + } + + /** + * Sets the filename of the File we want to print to. Equivalent to + * setInfoLog(new FileOutputStream(fileName, true)) + * + * @param file The File we will use for logging + * @see #setInfoLog(OutputStream) + */ + + public void setInfoLog(String fileName) throws IOException { + setInfoLog(getStream(fileName)); + } + + /** + * Prints info info to the current infoLog + * + * @param message The message to log + * @see #logInfo(String, Exception) + */ + + public void logInfo(String message) { + logInfo(message, null); + } + + /** + * Prints info info to the current infoLog + * + * @param exception An Exception + * @see #logInfo(String, Exception) + */ + + public void logInfo(Exception exception) { + logInfo(null, exception); + } + + /** + * Prints info info to the current infoLog + * + * @param message The message to log + * @param exception An Exception + */ + + public void logInfo(String message, Exception exception) { + if (!(logInfo || globalLog.logInfo)) + return; + + if (infoLog != null) + log(infoLog, "INFO", owner, message, exception); + else + log(globalLog.infoLog, "INFO", owner, message, exception); + } + + // LOG + + /** + * Internal method to get a named stream + */ + + private static OutputStream getStream(String name) throws IOException { + OutputStream os = null; + + synchronized (streamCache) { + if ((os = (OutputStream) streamCache.get(name)) != null) + return os; + + os = new FileOutputStream(name, true); + streamCache.put(name, os); + } + + return os; + } + + /** + * Internal log method + */ + + private static void log(PrintStream ps, String header, + String owner, String message, Exception ex) { + // Only allow one instance to print to the given stream. + synchronized (ps) { + // Create output stream for logging + LogStream logStream = new LogStream(ps); + + logStream.time = new Date(System.currentTimeMillis()); + logStream.header = header; + logStream.owner = owner; + + if (message != null) + logStream.println(message); + + if (ex != null) { + logStream.println(ex.getMessage()); + ex.printStackTrace(logStream); + } + } + } +} + +/** + * Utility class for logging. + * + * Minimal overloading of PrintStream + */ + +class LogStream extends PrintStream { + Date time = null; + String header = null; + String owner = null; + + public LogStream(OutputStream ps) { + super(ps); + } + + public void println(Object o) { + if (o == null) + println("null"); + else + println(o.toString()); + } + + public void println(String str) { + super.println("*** " + header + " (" + time + ", " + time.getTime() + + ") " + owner + ": " + str); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectManager.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectManager.java new file mode 100755 index 00000000..70486f32 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectManager.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + + +import java.lang.reflect.*; +import java.util.*; +import java.sql.SQLException; +import java.sql.Connection; + +/* + Det vi trenger er en mapping mellom + - abstrakt navn/klasse/type/identifikator (tilsv. repository) + - java klasse + - selve mappingen av db kolonne/java property + + I tillegg en mapping mellom alle objektene som brukes i VM'en, og deres id'er + +*/ + +/** + * Under construction. + * + * @author Harald Kuhr (haraldk@iconmedialab.no), + * @version 0.5 + */ +public abstract class ObjectManager { + private ObjectReader mObjectReader = null; + + private WeakHashMap mLiveObjects = new WeakHashMap(); // object/id + + private Hashtable mTypes = new Hashtable(); // type name/java class + private Hashtable mMappings = new Hashtable(); // type name/mapping + + /** + * Creates an Object Manager with the default JDBC connection + */ + + public ObjectManager() { + this(DatabaseConnection.getConnection()); + } + + /** + * Creates an Object Manager with the given JDBC connection + */ + + public ObjectManager(Connection pConnection) { + mObjectReader = new ObjectReader(pConnection); + } + + + /** + * Gets the property/column mapping for a given type + */ + + protected Hashtable getMapping(String pType) { + return (Hashtable) mMappings.get(pType); + } + + /** + * Gets the class for a type + * + * @return The class for a type. If the type is not found, this method will + * throw an excpetion, and will never return null. + */ + + protected Class getType(String pType) { + Class cl = (Class) mTypes.get(pType); + + if (cl == null) { + // throw new NoSuchTypeException(); + } + + return cl; + } + + /** + * Gets a java object of the class for a given type. + */ + + protected Object getObject(String pType) + /*throws XxxException*/ { + // Get class + Class cl = getType(pType); + + // Return the new instance (requires empty public constructor) + try { + return cl.newInstance(); + } + catch (Exception e) { + // throw new XxxException(e); + throw new RuntimeException(e.getMessage()); + } + + // Can't happen + //return null; + } + + /** + * Gets a DatabaseReadable object that can be used for looking up the + * object properties from the database. + */ + + protected DatabaseReadable getDatabaseReadable(String pType) { + + return new DatabaseObject(getObject(pType), getMapping(pType)); + } + + /** + * Reads the object of the given type and with the given id from the + * database + */ + + // interface + public Object getObject(String pType, Object pId) + throws SQLException { + + // Create DatabaseReadable and set id + DatabaseObject dbObject = (DatabaseObject) getDatabaseReadable(pType); + dbObject.setId(pId); + + // Read it + dbObject = (DatabaseObject) mObjectReader.readObject(dbObject); + + // Return it + return dbObject.getObject(); + } + + /** + * Reads the objects of the given type and with the given ids from the + * database + */ + + // interface + public Object[] getObjects(String pType, Object[] pIds) + throws SQLException { + + // Create Vector to hold the result + Vector result = new Vector(pIds.length); + + // Loop through Id's and fetch one at a time (no good performance...) + for (int i = 0; i < pIds.length; i++) { + // Create DBObject, set id and read it + DatabaseObject dbObject = + (DatabaseObject) getDatabaseReadable(pType); + + dbObject.setId(pIds[i]); + dbObject = (DatabaseObject) mObjectReader.readObject(dbObject); + + // Add to result if not null + if (dbObject != null) { + result.add(dbObject.getObject()); + } + } + + // Create array of correct type, length equal to Vector + Class cl = getType(pType); + Object[] arr = (Object[]) Array.newInstance(cl, result.size()); + + // Return the vector as an array + return result.toArray(arr); + } + + /** + * Reads the objects of the given type and with the given properties from + * the database + */ + + // interface + public Object[] getObjects(String pType, Hashtable pWhere) + throws SQLException { + return mObjectReader.readObjects(getDatabaseReadable(pType), pWhere); + } + + /** + * Reads all objects of the given type from the database + */ + + // interface + public Object[] getObjects(String pType) + throws SQLException { + return mObjectReader.readObjects(getDatabaseReadable(pType)); + } + + // interface + public Object addObject(Object pObject) { + // get id... + + return pObject; + } + + // interface + public Object updateObject(Object pObject) { + // get id... + + return pObject; + } + + // interface + public abstract Object deleteObject(String pType, Object pId); + + // interface + public abstract Object deleteObject(Object pObject); + + // interface + public abstract Object createObject(String pType, Object pId); + + // interface + public abstract Object createObject(String pType); + +} + +/** + * Utility class for reading Objects from the database + */ + +class DatabaseObject implements DatabaseReadable { + Hashtable mMapping = null; + + Object mId = null; + Object mObject = null; + + public DatabaseObject(Object pObject, Hashtable pMapping) { + setObject(pObject); + setMapping(pMapping); + } + + public Object getId() { + return mId; + } + + public void setId(Object pId) { + mId = pId; + } + + public void setObject(Object pObject) { + mObject = pObject; + } + public Object getObject() { + return mObject; + } + + public void setMapping(Hashtable pMapping) { + mMapping = pMapping; + } + + public Hashtable getMapping() { + return mMapping; + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectMapper.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectMapper.java new file mode 100755 index 00000000..a5922979 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectMapper.java @@ -0,0 +1,663 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + +import com.twelvemonkeys.lang.*; + +import java.lang.reflect.*; + +// Single-type import, to avoid util.Date/sql.Date confusion +import java.util.Hashtable; +import java.util.Vector; +import java.util.Enumeration; +import java.util.StringTokenizer; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +/** + * A class for mapping JDBC ResultSet rows to Java objects. + * + * @see ObjectReader + * + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/ObjectMapper.java#1 $ + * + * @todo Use JDK logging instead of proprietary logging. + */ +public class ObjectMapper { + final static String DIRECTMAP = "direct"; + final static String OBJECTMAP = "object"; + final static String COLLECTIONMAP = "collection"; + final static String OBJCOLLMAP = "objectcollection"; + + Class mInstanceClass = null; + + Hashtable mMethods = null; + + Hashtable mColumnMap = null; + Hashtable mPropertiesMap = null; + + Hashtable mJoins = null; + + private Hashtable mTables = null; + private Vector mColumns = null; + + Hashtable mForeignKeys = null; + Hashtable mPrimaryKeys = null; + Hashtable mMapTypes = null; + Hashtable mClasses = null; + + String mPrimaryKey = null; + String mForeignKey = null; + String mIdentityJoin = null; + + Log mLog = null; + + /** + * Creates a new ObjectMapper for a DatabaseReadable + * + * @param obj An object of type DatabaseReadable + */ + + /* + public ObjectMapper(DatabaseReadable obj) { + this(obj.getClass(), obj.getMapping()); + } + */ + /** + * Creates a new ObjectMapper for any object, given a mapping + * + * @param objClass The class of the object(s) created by this OM + * @param mapping an Hashtable containing the mapping information + * for this OM + */ + + public ObjectMapper(Class pObjClass, Hashtable pMapping) { + mLog = new Log(this); + + mInstanceClass = pObjClass; + + mJoins = new Hashtable(); + mPropertiesMap = new Hashtable(); + mColumnMap = new Hashtable(); + + mClasses = new Hashtable(); + mMapTypes = new Hashtable(); + mForeignKeys = new Hashtable(); + mPrimaryKeys = new Hashtable(); + + // Unpack and store mapping information + for (Enumeration keys = pMapping.keys(); keys.hasMoreElements();) { + String key = (String) keys.nextElement(); + String value = (String) pMapping.get(key); + + int dotIdx = key.indexOf("."); + + if (dotIdx >= 0) { + if (key.equals(".primaryKey")) { + // Primary key + mPrimaryKey = (String) pMapping.get(value); + } + else if (key.equals(".foreignKey")) { + // Foreign key + mForeignKey = (String) pMapping.get(value); + } + else if (key.equals(".join")) { + // Identity join + mIdentityJoin = (String) pMapping.get(key); + } + else if (key.endsWith(".primaryKey")) { + // Primary key in joining table + mPrimaryKeys.put(key.substring(0, dotIdx), value); + } + else if (key.endsWith(".foreignKey")) { + // Foreign key + mForeignKeys.put(key.substring(0, dotIdx), value); + } + else if (key.endsWith(".join")) { + // Joins + mJoins.put(key.substring(0, dotIdx), value); + } + else if (key.endsWith(".mapType")) { + // Maptypes + value = value.toLowerCase(); + + if (value.equals(DIRECTMAP) || value.equals(OBJECTMAP) || + value.equals(COLLECTIONMAP) || + value.equals(OBJCOLLMAP)) { + mMapTypes.put(key.substring(0, dotIdx), value); + } + else { + mLog.logError("Illegal mapType: \"" + value + "\"! " + + "Legal types are: direct, object, " + + "collection and objectCollection."); + } + } + else if (key.endsWith(".class")) { + // Classes + try { + mClasses.put(key.substring(0, dotIdx), + Class.forName(value)); + } + catch (ClassNotFoundException e) { + mLog.logError(e); + //e.printStackTrace(); + } + } + else if (key.endsWith(".collection")) { + // TODO!! + } + } + else { + // Property to column mappings + mPropertiesMap.put(key, value); + mColumnMap.put(value.substring(value.lastIndexOf(".") + 1), + key); + } + } + + mMethods = new Hashtable(); + Method[] methods = mInstanceClass.getMethods(); + for (int i = 0; i < methods.length; i++) { + // Two methods CAN have same name... + mMethods.put(methods[i].getName(), methods[i]); + } + } + + public void setPrimaryKey(String pPrimaryKey) { + mPrimaryKey = pPrimaryKey; + } + + /** + * Gets the name of the property, that acts as the unique identifier for + * this ObjectMappers type. + * + * @return The name of the primary key property + */ + + public String getPrimaryKey() { + return mPrimaryKey; + } + + public String getForeignKey() { + return mForeignKey; + } + + /** + * Gets the join, that is needed to find this ObjectMappers type. + * + * @return The name of the primary key property + */ + + public String getIdentityJoin() { + return mIdentityJoin; + } + + Hashtable getPropertyMapping(String pProperty) { + Hashtable mapping = new Hashtable(); + + if (pProperty != null) { + // Property + if (mPropertiesMap.containsKey(pProperty)) + mapping.put("object", mPropertiesMap.get(pProperty)); + + // Primary key + if (mPrimaryKeys.containsKey(pProperty)) { + mapping.put(".primaryKey", "id"); + mapping.put("id", mPrimaryKeys.get(pProperty)); + } + + //Foreign key + if (mForeignKeys.containsKey(pProperty)) + mapping.put(".foreignKey", mPropertiesMap.get(mForeignKeys.get(pProperty))); + + // Join + if (mJoins.containsKey(pProperty)) + mapping.put(".join", mJoins.get(pProperty)); + + // mapType + mapping.put(".mapType", "object"); + } + + return mapping; + } + + + /** + * Gets the column for a given property. + * + * @param property The property + * @return The name of the matching database column, on the form + * table.column + */ + + public String getColumn(String pProperty) { + if (mPropertiesMap == null || pProperty == null) + return null; + return (String) mPropertiesMap.get(pProperty); + } + + /** + * Gets the table name for a given property. + * + * @param property The property + * @return The name of the matching database table. + */ + + public String getTable(String pProperty) { + String table = getColumn(pProperty); + + if (table != null) { + int dotIdx = 0; + if ((dotIdx = table.lastIndexOf(".")) >= 0) + table = table.substring(0, dotIdx); + else + return null; + } + + return table; + } + + /** + * Gets the property for a given database column. If the column incudes + * table qualifier, the table qualifier is removed. + * + * @param column The name of the column + * @return The name of the mathcing property + */ + + public String getProperty(String pColumn) { + if (mColumnMap == null || pColumn == null) + return null; + + String property = (String) mColumnMap.get(pColumn); + + int dotIdx = 0; + if (property == null && (dotIdx = pColumn.lastIndexOf(".")) >= 0) + property = (String) mColumnMap.get(pColumn.substring(dotIdx + 1)); + + return property; + } + + + /** + * Maps each row of the given result set to an object ot this OM's type. + * + * @param rs The ResultSet to process (map to objects) + * @return An array of objects (of this OM's class). If there are no rows + * in the ResultSet, an empty (zero-length) array will be returned. + */ + + public synchronized Object[] mapObjects(ResultSet pRSet) throws SQLException { + Vector result = new Vector(); + + ResultSetMetaData meta = pRSet.getMetaData(); + int cols = meta.getColumnCount(); + + // Get colum names + String[] colNames = new String[cols]; + for (int i = 0; i < cols; i++) { + colNames[i] = meta.getColumnName(i + 1); // JDBC cols start at 1... + + /* + System.out.println(meta.getColumnLabel(i + 1)); + System.out.println(meta.getColumnName(i + 1)); + System.out.println(meta.getColumnType(i + 1)); + System.out.println(meta.getColumnTypeName(i + 1)); + // System.out.println(meta.getTableName(i + 1)); + // System.out.println(meta.getCatalogName(i + 1)); + // System.out.println(meta.getSchemaName(i + 1)); + // Last three NOT IMPLEMENTED!! + */ + } + + // Loop through rows in resultset + while (pRSet.next()) { + Object obj = null; + + try { + obj = mInstanceClass.newInstance(); // Asserts empty constructor! + } + catch (IllegalAccessException iae) { + mLog.logError(iae); + // iae.printStackTrace(); + } + catch (InstantiationException ie) { + mLog.logError(ie); + // ie.printStackTrace(); + } + + // Read each colum from this row into object + for (int i = 0; i < cols; i++) { + + String property = (String) mColumnMap.get(colNames[i]); + + if (property != null) { + // This column is mapped to a property + mapColumnProperty(pRSet, i + 1, property, obj); + } + } + + // Add object to the result Vector + result.addElement(obj); + } + + return result.toArray((Object[]) Array.newInstance(mInstanceClass, + result.size())); + } + + /** + * Maps a ResultSet column (from the current ResultSet row) to a named + * property of an object, using reflection. + * + * @param rs The JDBC ResultSet + * @param index The column index to get the value from + * @param property The name of the property to set the value of + * @param obj The object to set the property to + */ + + void mapColumnProperty(ResultSet pRSet, int pIndex, String pProperty, + Object pObj) { + if (pRSet == null || pProperty == null || pObj == null) + throw new IllegalArgumentException("ResultSet, Property or Object" + + " arguments cannot be null!"); + if (pIndex <= 0) + throw new IllegalArgumentException("Index parameter must be > 0!"); + + String methodName = "set" + StringUtil.capitalize(pProperty); + Method setMethod = (Method) mMethods.get(methodName); + + if (setMethod == null) { + // No setMethod for this property + mLog.logError("No set method for property \"" + + pProperty + "\" in " + pObj.getClass() + "!"); + return; + } + + // System.err.println("DEBUG: setMethod=" + setMethod); + + Method getMethod = null; + + String type = ""; + try { + Class[] cl = {Integer.TYPE}; + type = setMethod.getParameterTypes()[0].getName(); + + type = type.substring(type.lastIndexOf(".") + 1); + + // There is no getInteger, use getInt instead + if (type.equals("Integer")) { + type = "int"; + } + + // System.err.println("DEBUG: type=" + type); + getMethod = pRSet.getClass(). + getMethod("get" + StringUtil.capitalize(type), cl); + } + catch (Exception e) { + mLog.logError("Can't find method \"get" + + StringUtil.capitalize(type) + "(int)\" " + + "(for class " + StringUtil.capitalize(type) + + ") in ResultSet", e); + + return; + } + + try { + // Get the data from the DB + // System.err.println("DEBUG: " + getMethod.getName() + "(" + (i + 1) + ")"); + + Object[] colIdx = {new Integer(pIndex)}; + Object[] arg = {getMethod.invoke(pRSet, colIdx)}; + + // Set it to the object + // System.err.println("DEBUG: " + setMethod.getName() + "(" + arg[0] + ")"); + setMethod.invoke(pObj, arg); + } + catch (InvocationTargetException ite) { + mLog.logError(ite); + // ite.printStackTrace(); + } + catch (IllegalAccessException iae) { + mLog.logError(iae); + // iae.printStackTrace(); + } + } + + /** + * Creates a SQL query string to get the primary keys for this + * ObjectMapper. + */ + + String buildIdentitySQL(String[] pKeys) { + mTables = new Hashtable(); + mColumns = new Vector(); + + // Get columns to select + mColumns.addElement(getPrimaryKey()); + + // Get tables to select (and join) from and their joins + tableJoins(null, false); + + for (int i = 0; i < pKeys.length; i++) { + tableJoins(getColumn(pKeys[i]), true); + } + + // All data read, build SQL query string + return "SELECT " + getPrimaryKey() + " " + buildFromClause() + + buildWhereClause(); + } + + /** + * Creates a SQL query string to get objects for this ObjectMapper. + */ + + public String buildSQL() { + mTables = new Hashtable(); + mColumns = new Vector(); + + String key = null; + for (Enumeration keys = mPropertiesMap.keys(); keys.hasMoreElements();) { + key = (String) keys.nextElement(); + + // Get columns to select + String column = (String) mPropertiesMap.get(key); + mColumns.addElement(column); + + tableJoins(column, false); + } + + // All data read, build SQL query string + return buildSelectClause() + buildFromClause() + + buildWhereClause(); + } + + /** + * Builds a SQL SELECT clause from the columns Vector + */ + + private String buildSelectClause() { + StringBuilder sqlBuf = new StringBuilder(); + + sqlBuf.append("SELECT "); + + String column = null; + for (Enumeration select = mColumns.elements(); select.hasMoreElements();) { + column = (String) select.nextElement(); + + /* + String subColumn = column.substring(column.indexOf(".") + 1); + // System.err.println("DEBUG: col=" + subColumn); + String mapType = (String) mMapTypes.get(mColumnMap.get(subColumn)); + */ + String mapType = (String) mMapTypes.get(getProperty(column)); + + if (mapType == null || mapType.equals(DIRECTMAP)) { + sqlBuf.append(column); + + sqlBuf.append(select.hasMoreElements() ? ", " : " "); + } + } + + return sqlBuf.toString(); + } + + /** + * Builds a SQL FROM clause from the tables/joins Hashtable + */ + + private String buildFromClause() { + StringBuilder sqlBuf = new StringBuilder(); + + sqlBuf.append("FROM "); + + String table = null; + String schema = null; + for (Enumeration from = mTables.keys(); from.hasMoreElements();) { + table = (String) from.nextElement(); + /* + schema = (String) schemas.get(table); + + if (schema != null) + sqlBuf.append(schema + "."); + */ + + sqlBuf.append(table); + sqlBuf.append(from.hasMoreElements() ? ", " : " "); + } + + return sqlBuf.toString(); + } + + /** + * Builds a SQL WHERE clause from the tables/joins Hashtable + * + * @return Currently, this metod will return "WHERE 1 = 1", if no other + * WHERE conditions are specified. This can be considered a hack. + */ + + private String buildWhereClause() { + + StringBuilder sqlBuf = new StringBuilder(); + + String join = null; + boolean first = true; + + for (Enumeration where = mTables.elements(); where.hasMoreElements();) { + join = (String) where.nextElement(); + + if (join.length() > 0) { + if (first) { + // Skip " AND " in first iteration + first = false; + } + else { + sqlBuf.append(" AND "); + } + } + + sqlBuf.append(join); + } + + if (sqlBuf.length() > 0) + return "WHERE " + sqlBuf.toString(); + + return "WHERE 1 = 1"; // Hacky... + } + + /** + * Finds tables used in mappings and joins and adds them to the tables + * Hashtable, with the table name as key, and the join as value. + */ + + private void tableJoins(String pColumn, boolean pWhereJoin) { + String join = null; + String table = null; + + if (pColumn == null) { + // Identity + join = getIdentityJoin(); + table = getTable(getProperty(getPrimaryKey())); + } + else { + // Normal + int dotIndex = -1; + if ((dotIndex = pColumn.lastIndexOf(".")) <= 0) { + // No table qualifier + return; + } + + // Get table qualifier. + table = pColumn.substring(0, dotIndex); + + // Don't care about the tables that are not supposed to be selected from + String property = (String) getProperty(pColumn); + + if (property != null) { + String mapType = (String) mMapTypes.get(property); + if (!pWhereJoin && mapType != null && !mapType.equals(DIRECTMAP)) { + return; + } + + join = (String) mJoins.get(property); + } + } + + // If table is not in the tables Hash, add it, and check for joins. + if (mTables.get(table) == null) { + if (join != null) { + mTables.put(table, join); + + StringTokenizer tok = new StringTokenizer(join, "= "); + String next = null; + + while(tok.hasMoreElements()) { + next = tok.nextToken(); + // Don't care about SQL keywords + if (next.equals("AND") || next.equals("OR") + || next.equals("NOT") || next.equals("IN")) { + continue; + } + // Check for new tables and joins in this join clause. + tableJoins(next, false); + } + } + else { + // No joins for this table. + join = ""; + mTables.put(table, join); + } + } + } + +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectReader.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectReader.java new file mode 100755 index 00000000..ffaf8a16 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/ObjectReader.java @@ -0,0 +1,879 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.SystemUtil; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; +import java.util.Vector; + +/** + * Class used for reading table data from a database through JDBC, and map + * the data to Java classes. + * + * @see ObjectMapper + * + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sql/ObjectReader.java#1 $ + * + * @todo Use JDK logging instead of proprietary logging. + * + */ +public class ObjectReader { + + /** + * Main method, for testing purposes only. + */ + + public final static void main(String[] pArgs) throws SQLException { + /* + System.err.println("Testing only!"); + + // Get default connection + ObjectReader obr = new ObjectReader(DatabaseConnection.getConnection()); + + com.twelvemonkeys.usedcars.DBCar car = new com.twelvemonkeys.usedcars.DBCar(new Integer(1)); + com.twelvemonkeys.usedcars.DBDealer dealer = new com.twelvemonkeys.usedcars.DBDealer("NO4537"); + + System.out.println(obr.readObject(dealer)); + com.twelvemonkeys.usedcars.Dealer[] dealers = (com.twelvemonkeys.usedcars.Dealer[]) obr.readObjects(dealer); + + for (int i = 0; i < dealers.length; i++) { + System.out.println(dealers[i]); + } + + System.out.println("------------------------------------------------------------------------------\n" + + "Total: " + dealers.length + " dealers in database\n"); + + Hashtable where = new Hashtable(); + + where.put("zipCode", "0655"); + dealers = (com.twelvemonkeys.usedcars.Dealer[]) obr.readObjects(dealer, where); + + for (int i = 0; i < dealers.length; i++) { + System.out.println(dealers[i]); + } + + System.out.println("------------------------------------------------------------------------------\n" + + "Total: " + dealers.length + " dealers matching query: " + + where + "\n"); + + + com.twelvemonkeys.usedcars.Car[] cars = null; + cars = (com.twelvemonkeys.usedcars.Car[]) obr.readObjects(car); + + for (int i = 0; i < cars.length; i++) { + System.out.println(cars[i]); + } + + System.out.println("------------------------------------------------------------------------------\n" + + "Total: " + cars.length + " cars in database\n"); + + where = new Hashtable(); + where.put("year", new Integer(1995)); + cars = (com.twelvemonkeys.usedcars.Car[]) obr.readObjects(car, where); + + for (int i = 0; i < cars.length; i++) { + System.out.println(cars[i]); + } + + System.out.println("------------------------------------------------------------------------------\n" + + "Total: " + cars.length + " cars matching query: " + + where + " \n"); + + + where = new Hashtable(); + where.put("publishers", "Bilguiden"); + cars = (com.twelvemonkeys.usedcars.Car[]) obr.readObjects(car, where); + + for (int i = 0; i < cars.length; i++) { + System.out.println(cars[i]); + } + + System.out.println("------------------------------------------------------------------------------\n" + + "Total: " + cars.length + " cars matching query: " + + where + "\n"); + + System.out.println("==============================================================================\n" + + getStats()); + */ + } + + + protected Log mLog = null; + protected Properties mConfig = null; + + /** + * The connection used for all database operations executed by this + * ObjectReader. + */ + + Connection mConnection = null; + + /** + * The cache for this ObjectReader. + * Probably a source for memory leaks, as it has no size limitations. + */ + + private Hashtable mCache = new Hashtable(); + + /** + * Creates a new ObjectReader, using the given JDBC Connection. The + * Connection will be used for all database reads by this ObjectReader. + * + * @param connection A JDBC Connection + */ + + public ObjectReader(Connection pConnection) { + mConnection = pConnection; + + try { + mConfig = SystemUtil.loadProperties(getClass()); + } + catch (FileNotFoundException fnf) { + // Just go with defaults + } + catch (IOException ioe) { + new Log(this).logError(ioe); + } + + mLog = new Log(this, mConfig); + } + + /** + * Gets a string containing the stats for this ObjectReader. + * + * @return A string to display the stats. + */ + + public static String getStats() { + long total = sCacheHit + sCacheMiss + sCacheUn; + double hit = ((double) sCacheHit / (double) total) * 100.0; + double miss = ((double) sCacheMiss / (double) total) * 100.0; + double un = ((double) sCacheUn / (double) total) * 100.0; + + // Default locale + java.text.NumberFormat nf = java.text.NumberFormat.getInstance(); + + return "Total: " + total + " reads. " + + "Cache hits: " + sCacheHit + " (" + nf.format(hit) + "%), " + + "Cache misses: " + sCacheMiss + " (" + nf.format(miss) + "%), " + + "Unattempted: " + sCacheUn + " (" + nf.format(un) + "%) "; + } + + /** + * Get an array containing Objects of type objClass, with the + * identity values for the given class set. + */ + + private Object[] readIdentities(Class pObjClass, Hashtable pMapping, + Hashtable pWhere, ObjectMapper pOM) + throws SQLException { + sCacheUn++; + // Build SQL query string + if (pWhere == null) + pWhere = new Hashtable(); + + String[] keys = new String[pWhere.size()]; + int i = 0; + for (Enumeration en = pWhere.keys(); en.hasMoreElements(); i++) { + keys[i] = (String) en.nextElement(); + } + + // Get SQL for reading identity column + String sql = pOM.buildIdentitySQL(keys) + + buildWhereClause(keys, pMapping); + + // Log? + mLog.logDebug(sql + " (" + pWhere + ")"); + + // Prepare statement and set values + PreparedStatement statement = mConnection.prepareStatement(sql); + for (int j = 0; j < keys.length; j++) { + Object key = pWhere.get(keys[j]); + + if (key instanceof Integer) + statement.setInt(j + 1, ((Integer) key).intValue()); + else if (key instanceof BigDecimal) + statement.setBigDecimal(j + 1, (BigDecimal) key); + else + statement.setString(j + 1, key.toString()); + } + + // Execute query + ResultSet rs = null; + try { + rs = statement.executeQuery(); + } + catch (SQLException e) { + mLog.logError(sql + " (" + pWhere + ")", e); + throw e; + } + Vector result = new Vector(); + + // Map query to objects + while (rs.next()) { + Object obj = null; + + try { + obj = pObjClass.newInstance(); + } + catch (IllegalAccessException iae) { + iae.printStackTrace(); + } + catch (InstantiationException ie) { + ie.printStackTrace(); + } + + // Map it + pOM.mapColumnProperty(rs, 1, + pOM.getProperty(pOM.getPrimaryKey()), obj); + result.addElement(obj); + } + + // Return array of identifiers + return result.toArray((Object[]) Array.newInstance(pObjClass, + result.size())); + } + + + /** + * Reads one object implementing the DatabaseReadable interface from the + * database. + * + * @param readable A DatabaseReadable object + * @return The Object read, or null in no object is found + */ + + public Object readObject(DatabaseReadable pReadable) throws SQLException { + return readObject(pReadable.getId(), pReadable.getClass(), + pReadable.getMapping()); + } + + /** + * Reads the object with the given id from the database, using the given + * mapping. + * + * @param id An object uniquely identifying the object to read + * @param objClass The clas + * @return The Object read, or null in no object is found + */ + + public Object readObject(Object pId, Class pObjClass, Hashtable pMapping) + throws SQLException { + return readObject(pId, pObjClass, pMapping, null); + } + + /** + * Reads all the objects of the given type from the + * database. The object must implement the DatabaseReadable interface. + * + * @return An array of Objects, or an zero-length array if none was found + */ + + public Object[] readObjects(DatabaseReadable pReadable) + throws SQLException { + return readObjects(pReadable.getClass(), + pReadable.getMapping(), null); + } + + /** + * Sets the property value to an object using reflection + * + * @param obj The object to get a property from + * @param property The name of the property + * @param value The property value + * + */ + + private void setPropertyValue(Object pObj, String pProperty, + Object pValue) { + + Method m = null; + Class[] cl = {pValue.getClass()}; + + try { + //Util.setPropertyValue(pObj, pProperty, pValue); + + // Find method + m = pObj.getClass(). + getMethod("set" + StringUtil.capitalize(pProperty), cl); + // Invoke it + Object[] args = {pValue}; + m.invoke(pObj, args); + + } + catch (NoSuchMethodException e) { + e.printStackTrace(); + } + catch (IllegalAccessException iae) { + iae.printStackTrace(); + } + catch (InvocationTargetException ite) { + ite.printStackTrace(); + } + + } + + /** + * Gets the property value from an object using reflection + * + * @param obj The object to get a property from + * @param property The name of the property + * + * @return The property value as an Object + */ + + private Object getPropertyValue(Object pObj, String pProperty) { + + Method m = null; + Class[] cl = new Class[0]; + + try { + //return Util.getPropertyValue(pObj, pProperty); + + // Find method + m = pObj.getClass(). + getMethod("get" + StringUtil.capitalize(pProperty), + new Class[0]); + // Invoke it + Object result = m.invoke(pObj, new Object[0]); + return result; + + } + catch (NoSuchMethodException e) { + e.printStackTrace(); + } + catch (IllegalAccessException iae) { + iae.printStackTrace(); + } + catch (InvocationTargetException ite) { + ite.printStackTrace(); + } + return null; + } + + /** + * Reads and sets the child properties of the given parent object. + * + * @param parent The object to set the child obects to. + * @param om The ObjectMapper of the parent object. + */ + + private void setChildObjects(Object pParent, ObjectMapper pOM) + throws SQLException { + if (pOM == null) { + throw new NullPointerException("ObjectMapper in readChildObjects " + + "cannot be null!!"); + } + + for (Enumeration keys = pOM.mMapTypes.keys(); keys.hasMoreElements();) { + String property = (String) keys.nextElement(); + String mapType = (String) pOM.mMapTypes.get(property); + + if (property.length() <= 0 || mapType == null) { + continue; + } + + // Get the id of the parent + Object id = getPropertyValue(pParent, + pOM.getProperty(pOM.getPrimaryKey())); + + if (mapType.equals(ObjectMapper.OBJECTMAP)) { + // OBJECT Mapping + + // Get the class for this property + Class objectClass = (Class) pOM.mClasses.get(property); + + DatabaseReadable dbr = null; + try { + dbr = (DatabaseReadable) objectClass.newInstance(); + } + catch (Exception e) { + mLog.logError(e); + } + + /* + Properties mapping = readMapping(objectClass); + */ + + // Get property mapping for child object + if (pOM.mJoins.containsKey(property)) + // mapping.setProperty(".join", (String) pOM.joins.get(property)); + dbr.getMapping().put(".join", pOM.mJoins.get(property)); + + // Find id and put in where hash + Hashtable where = new Hashtable(); + + // String foreignKey = mapping.getProperty(".foreignKey"); + String foreignKey = (String) + dbr.getMapping().get(".foreignKey"); + + if (foreignKey != null) { + where.put(".foreignKey", id); + } + + Object[] child = readObjects(dbr, where); + // Object[] child = readObjects(objectClass, mapping, where); + + if (child.length < 1) + throw new SQLException("No child object with foreign key " + + foreignKey + "=" + id); + else if (child.length != 1) + throw new SQLException("More than one object with foreign " + + "key " + foreignKey + "=" + id); + + // Set child object to the parent + setPropertyValue(pParent, property, child[0]); + } + else if (mapType.equals(ObjectMapper.COLLECTIONMAP)) { + // COLLECTION Mapping + + // Get property mapping for child object + Hashtable mapping = pOM.getPropertyMapping(property); + + // Find id and put in where hash + Hashtable where = new Hashtable(); + String foreignKey = (String) mapping.get(".foreignKey"); + if (foreignKey != null) { + where.put(".foreignKey", id); + } + + DBObject dbr = new DBObject(); + dbr.mapping = mapping; // ugh... + // Read the objects + Object[] objs = readObjects(dbr, where); + + // Put the objects in a hash + Hashtable children = new Hashtable(); + for (int i = 0; i < objs.length; i++) { + children.put(((DBObject) objs[i]).getId(), + ((DBObject) objs[i]).getObject()); + } + + // Set child properties to parent object + setPropertyValue(pParent, property, children); + } + } + } + + /** + * Reads all objects from the database, using the given mapping. + * + * @param objClass The class of the objects to read + * @param mapping The hashtable containing the object mapping + * + * @return An array of Objects, or an zero-length array if none was found + */ + + public Object[] readObjects(Class pObjClass, Hashtable pMapping) + throws SQLException { + return readObjects(pObjClass, pMapping, null); + } + + /** + * Builds extra SQL WHERE clause + * + * @param keys An array of ID names + * @param mapping The hashtable containing the object mapping + * + * @return A string containing valid SQL + */ + + private String buildWhereClause(String[] pKeys, Hashtable pMapping) { + StringBuilder sqlBuf = new StringBuilder(); + + for (int i = 0; i < pKeys.length; i++) { + String column = (String) pMapping.get(pKeys[i]); + sqlBuf.append(" AND "); + sqlBuf.append(column); + sqlBuf.append(" = ?"); + } + + return sqlBuf.toString(); + + } + + private String buildIdInClause(Object[] pIds, Hashtable pMapping) { + StringBuilder sqlBuf = new StringBuilder(); + + if (pIds != null && pIds.length > 0) { + sqlBuf.append(" AND "); + sqlBuf.append(pMapping.get(".primaryKey")); + sqlBuf.append(" IN ("); + + for (int i = 0; i < pIds.length; i++) { + sqlBuf.append(pIds[i]); // SETTE INN '?' ??? + sqlBuf.append(", "); + } + sqlBuf.append(")"); + } + + return sqlBuf.toString(); + + } + + /** + * Reads all objects from the database, using the given mapping. + * + * @param readable A DatabaseReadable object + * @param mapping The hashtable containing the object mapping + * + * @return An array of Objects, or an zero-length array if none was found + */ + + public Object[] readObjects(DatabaseReadable pReadable, Hashtable pWhere) + throws SQLException { + return readObjects(pReadable.getClass(), + pReadable.getMapping(), pWhere); + } + + + /** + * Reads the object with the given id from the database, using the given + * mapping. + * This is the most general form of readObject(). + * + * @param id An object uniquely identifying the object to read + * @param objClass The class of the object to read + * @param mapping The hashtable containing the object mapping + * @param where An hashtable containing extra criteria for the read + * + * @return An array of Objects, or an zero-length array if none was found + */ + + public Object readObject(Object pId, Class pObjClass, + Hashtable pMapping, Hashtable pWhere) + throws SQLException { + ObjectMapper om = new ObjectMapper(pObjClass, pMapping); + return readObject0(pId, pObjClass, om, pWhere); + } + + public Object readObjects(Object[] pIds, Class pObjClass, + Hashtable pMapping, Hashtable pWhere) + throws SQLException { + ObjectMapper om = new ObjectMapper(pObjClass, pMapping); + return readObjects0(pIds, pObjClass, om, pWhere); + } + + /** + * Reads all objects from the database, using the given mapping. + * This is the most general form of readObjects(). + * + * @param objClass The class of the objects to read + * @param mapping The hashtable containing the object mapping + * @param where An hashtable containing extra criteria for the read + * + * @return An array of Objects, or an zero-length array if none was found + */ + + public Object[] readObjects(Class pObjClass, Hashtable pMapping, + Hashtable pWhere) throws SQLException { + return readObjects0(pObjClass, pMapping, pWhere); + } + + // readObjects implementation + + private Object[] readObjects0(Class pObjClass, Hashtable pMapping, + Hashtable pWhere) throws SQLException { + ObjectMapper om = new ObjectMapper(pObjClass, pMapping); + + Object[] ids = readIdentities(pObjClass, pMapping, pWhere, om); + + Object[] result = readObjects0(ids, pObjClass, om, pWhere); + + return result; + } + + private Object[] readObjects0(Object[] pIds, Class pObjClass, + ObjectMapper pOM, Hashtable pWhere) + throws SQLException { + Object[] result = new Object[pIds.length]; + + // Read each object from ID + for (int i = 0; i < pIds.length; i++) { + + // TODO: For better cahce efficiency/performance: + // - Read as many objects from cache as possible + // - Read all others in ONE query, and add to cache + /* + sCacheUn++; + // Build SQL query string + if (pWhere == null) + pWhere = new Hashtable(); + + String[] keys = new String[pWhere.size()]; + int i = 0; + for (Enumeration en = pWhere.keys(); en.hasMoreElements(); i++) { + keys[i] = (String) en.nextElement(); + } + + // Get SQL for reading identity column + String sql = pOM.buildSelectClause() + pOM.buildFromClause() + + + buildWhereClause(keys, pMapping) + buildIdInClause(pIds, pMapping); + + // Log? + mLog.logDebug(sql + " (" + pWhere + ")"); + + + // Log? + mLog.logDebug(sql + " (" + pWhere + ")"); + + PreparedStatement statement = null; + + // Execute query, and map columns/properties + try { + statement = mConnection.prepareStatement(sql); + + // Set keys + for (int j = 0; j < keys.length; j++) { + Object value = pWhere.get(keys[j]); + + if (value instanceof Integer) + statement.setInt(j + 1, ((Integer) value).intValue()); + else if (value instanceof BigDecimal) + statement.setBigDecimal(j + 1, (BigDecimal) value); + else + statement.setString(j + 1, value.toString()); + } + // Set ids + for (int j = 0; j < pIds.length; j++) { + Object id = pIds[i]; + + if (id instanceof Integer) + statement.setInt(j + 1, ((Integer) id).intValue()); + else if (id instanceof BigDecimal) + statement.setBigDecimal(j + 1, (BigDecimal) id); + else + statement.setString(j + 1, id.toString()); + } + + ResultSet rs = statement.executeQuery(); + + Object[] result = pOM.mapObjects(rs); + + // Set child objects and return + for (int i = 0; i < result.length; i++) { + // FOR THIS TO REALLY GET EFFECTIVE, WE NEED TO SET ALL + // CHILDREN IN ONE GO! + setChildObjects(result[i], pOM); + mContent.put(pOM.getPrimaryKey() + "=" + pId, result[0]); + + } + // Return result + return result[0]; + + } + */ + + Object id = getPropertyValue(result[i], + pOM.getProperty(pOM.getPrimaryKey())); + + result[i] = readObject0(id, pObjClass, pOM, null); + + } + + return result; + } + + // readObject implementation, used for ALL database reads + + static long sCacheHit; + static long sCacheMiss; + static long sCacheUn; + + private Object readObject0(Object pId, Class pObjClass, ObjectMapper pOM, + Hashtable pWhere) throws SQLException { + if (pId == null && pWhere == null) + throw new IllegalArgumentException("Either id or where argument" + + "must be non-null!"); + + // First check if object exists in cache + if (pId != null) { + Object o = mCache.get(pOM.getPrimaryKey() + "=" + pId); + if (o != null) { + sCacheHit++; + return o; + } + sCacheMiss++; + } + else { + sCacheUn++; + } + + // Create where hash + if (pWhere == null) + pWhere = new Hashtable(); + + // Make sure the ID is in the where hash + if (pId != null) + pWhere.put(pOM.getProperty(pOM.getPrimaryKey()), pId); + + String[] keys = new String[pWhere.size()]; + Enumeration en = pWhere.keys(); + for (int i = 0; en.hasMoreElements(); i++) { + keys[i] = (String) en.nextElement(); + } + + // Get SQL query string + String sql = pOM.buildSQL() + buildWhereClause(keys, pOM.mPropertiesMap); + + // Log? + mLog.logDebug(sql + " (" + pWhere + ")"); + + PreparedStatement statement = null; + + // Execute query, and map columns/properties + try { + statement = mConnection.prepareStatement(sql); + + for (int j = 0; j < keys.length; j++) { + Object value = pWhere.get(keys[j]); + + if (value instanceof Integer) + statement.setInt(j + 1, ((Integer) value).intValue()); + else if (value instanceof BigDecimal) + statement.setBigDecimal(j + 1, (BigDecimal) value); + else + statement.setString(j + 1, value.toString()); + } + + ResultSet rs = statement.executeQuery(); + + Object[] result = pOM.mapObjects(rs); + + // Set child objects and return + if (result.length == 1) { + setChildObjects(result[0], pOM); + mCache.put(pOM.getPrimaryKey() + "=" + pId, result[0]); + + // Return result + return result[0]; + } + // More than 1 is an error... + else if (result.length > 1) { + throw new SQLException("More than one object with primary key " + + pOM.getPrimaryKey() + "=" + + pWhere.get(pOM.getProperty(pOM.getPrimaryKey())) + "!"); + } + } + catch (SQLException e) { + mLog.logError(sql + " (" + pWhere + ")", e); + throw e; + } + finally { + try { + statement.close(); + } + catch (SQLException e) { + mLog.logError(e); + } + } + + return null; + } + + /** + * Utility method for reading a property mapping from a properties-file + * + */ + + public static Properties loadMapping(Class pClass) { + try { + return SystemUtil.loadProperties(pClass); + } + catch (FileNotFoundException fnf) { + // System.err... err... + System.err.println("ERROR: " + fnf.getMessage()); + } + catch (IOException ioe) { + ioe.printStackTrace(); + } + return new Properties(); + } + + /** + * @deprecated Use loadMapping(Class) instead + * @see #loadMapping(Class) + */ + + public static Properties readMapping(Class pClass) { + return loadMapping(pClass); + } + + +} + +/** + * Utility class + */ + +class DBObject implements DatabaseReadable { + Object id; + Object o; + static Hashtable mapping; // WHOA, STATIC!?!? + + public DBObject() { + } + + public void setId(Object id) { + this.id = id; + } + public Object getId() { + return id; + } + + public void setObject(Object o) { + this.o = o; + } + public Object getObject() { + return o; + } + + public Hashtable getMapping() { + return mapping; + } +} + + diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/SQLUtil.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/SQLUtil.java new file mode 100755 index 00000000..04052d71 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/SQLUtil.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.sql; + +import com.twelvemonkeys.lang.StringUtil; + +import java.sql.*; +import java.io.*; +import java.util.Properties; + + +/** + * A class used to test a JDBC database connection. It can also be used as a + * really simple form of command line SQL interface, that passes all command + * line parameters to the database as plain SQL, and returns all rows to + * Sytem.out. Be aware that the wildcard character (*) is intercepted by + * the console, so you have to quote your string, or escape the wildcard + * character, otherwise you may get unpredictable results. + *

    + * Exmaple use + *
    + *

    + * $ java -cp lib\jconn2.jar;build com.twelvemonkeys.sql.SQLUtil
    + * -d com.sybase.jdbc2.jdbc.SybDriver -u "jdbc:sybase:Tds:10.248.136.42:6100"
    + * -l scott -p tiger "SELECT * FROM emp"
    + * Make sure sure to include the path to your JDBC driver in the java class + * path! + * + * @author Philippe Béal (phbe@iconmedialab.no) + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @author last modified by $author: WMHAKUR $ + * @version $id: $ + * @see DatabaseConnection + */ +public class SQLUtil { + /** + * Method main + * + * @param pArgs + * @throws SQLException + * + * @todo Refactor the long and ugly main method... + * Consider: - extract method parserArgs(String[])::Properties (how do we + * get the rest of the arguments? getProperty("_ARGV")? + * Make the Properties/Map an argument and return int with last + * option index? + * - extract method getStatementReader(Properties) + */ + public static void main(String[] pArgs) throws SQLException, IOException { + String user = null; + String password = null; + String url = null; + String driver = null; + String configFileName = null; + String scriptFileName = null; + String scriptSQLDelim = "go"; + int argIdx = 0; + boolean errArgs = false; + + while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { + if ((pArgs[argIdx].charAt(1) == 'l') || pArgs[argIdx].equals("--login")) { + argIdx++; + user = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--password")) { + argIdx++; + password = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--url")) { + argIdx++; + url = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--driver")) { + argIdx++; + driver = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'c') || pArgs[argIdx].equals("--config")) { + argIdx++; + configFileName = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 's') || pArgs[argIdx].equals("--script")) { + argIdx++; + scriptFileName = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) { + argIdx++; + errArgs = true; + } + else { + System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); + } + } + if (errArgs || (scriptFileName == null && (pArgs.length < (argIdx + 1)))) { + System.err.println("Usage: SQLUtil [--help|-h] [--login|-l ] [--password|-p ] [--driver|-d ] [--url|-u ] [--config|-c ] [--script|-s ] "); + System.exit(5); + } + + // If config file, read config and use as defaults + // NOTE: Command line options override! + if (!StringUtil.isEmpty(configFileName)) { + Properties config = new Properties(); + File configFile = new File(configFileName); + if (!configFile.exists()) { + System.err.println("Config file " + configFile.getAbsolutePath() + " does not exist."); + System.exit(10); + } + + InputStream in = new FileInputStream(configFile); + try { + config.load(in); + } + finally { + in.close(); + } + + if (driver == null) { + driver = config.getProperty("driver"); + } + if (url == null) { + url = config.getProperty("url"); + } + if (user == null) { + user = config.getProperty("login"); + } + if (password == null) { + password = config.getProperty("password"); + } + } + + // Register JDBC driver + if (driver != null) { + registerDriver(driver); + } + Connection conn = null; + + try { + // Use default connection from DatabaseConnection.properties + conn = DatabaseConnection.getConnection(user, password, url); + if (conn == null) { + System.err.println("No connection."); + System.exit(10); + } + + BufferedReader reader; + if (scriptFileName != null) { + // Read SQL from file + File file = new File(scriptFileName); + if (!file.exists()) { + System.err.println("Script file " + file.getAbsolutePath() + " does not exist."); + System.exit(10); + } + + reader = new BufferedReader(new FileReader(file)); + } + else { + // Create SQL statement from command line params + StringBuilder sql = new StringBuilder(); + for (int i = argIdx; i < pArgs.length; i++) { + sql.append(pArgs[i]).append(" "); + } + + reader = new BufferedReader(new StringReader(sql.toString())); + } + + //reader.mark(10000000); + //for (int i = 0; i < 5; i++) { + StringBuilder sql = new StringBuilder(); + while (true) { + // Read next line + String line = reader.readLine(); + if (line == null) { + // End of file, execute and quit + String str = sql.toString(); + if (!StringUtil.isEmpty(str)) { + executeSQL(str, conn); + } + break; + } + else if (line.trim().endsWith(scriptSQLDelim)) { + // End of statement, execute and continue + sql.append(line.substring(0, line.lastIndexOf(scriptSQLDelim))); + executeSQL(sql.toString(), conn); + sql.setLength(0); + } + else { + sql.append(line).append(" "); + } + } + //reader.reset(); + //} + } + finally { + // Close the connection + if (conn != null) { + conn.close(); + } + } + } + + private static void executeSQL(String pSQL, Connection pConn) throws SQLException { + System.out.println("Executing: " + pSQL); + + Statement stmt = null; + try { + // NOTE: Experimental + //stmt = pConn.prepareCall(pSQL); + //boolean results = ((CallableStatement) stmt).execute(); + + // Create statement and execute + stmt = pConn.createStatement(); + boolean results = stmt.execute(pSQL); + + int updateCount = -1; + + SQLWarning warning = stmt.getWarnings(); + while (warning != null) { + System.out.println("Warning: " + warning.getMessage()); + warning = warning.getNextWarning(); + } + + // More result sets to process? + while (results || (updateCount = stmt.getUpdateCount()) != -1) { + // INSERT, UPDATE or DELETE statement (no result set). + if (!results && (updateCount >= 0)) { + System.out.println("Operation successfull. " + updateCount + " row" + ((updateCount != 1) ? "s" : "") + " affected."); + System.out.println(); + } + // SELECT statement or stored procedure + else { + processResultSet(stmt.getResultSet()); + } + + // More results? + results = stmt.getMoreResults(); + } + } + catch (SQLException sqle) { + System.err.println("Error: " + sqle.getMessage()); + while ((sqle = sqle.getNextException()) != null) { + System.err.println(" " + sqle); + } + } + finally { + // Close the statement + if (stmt != null) { + stmt.close(); + } + } + } + + // TODO: Create interface ResultSetProcessor + // -- processWarnings(SQLWarning pWarnings); + // -- processMetaData(ResultSetMetaData pMetas); ?? + // -- processResultSet(ResultSet pResult); + // TODO: Add parameter pResultSetProcessor to method + // TODO: Extract contents of this method to class Default/CLIRSP + // TODO: Create new class JTableRSP that creates (?) and populates a JTable + // or a TableModel (?) + private static void processResultSet(ResultSet pResultSet) throws SQLException { + try { + // Get meta data + ResultSetMetaData meta = pResultSet.getMetaData(); + + // Print any warnings that might have occured + SQLWarning warning = pResultSet.getWarnings(); + while (warning != null) { + System.out.println("Warning: " + warning.getMessage()); + warning = warning.getNextWarning(); + } + + // Get the number of columns in the result set + int numCols = meta.getColumnCount(); + + for (int i = 1; i <= numCols; i++) { + boolean prepend = isNumeric(meta.getColumnType(i)); + + String label = maybePad(meta.getColumnLabel(i), meta.getColumnDisplaySize(i), " ", prepend); + + System.out.print(label + "\t"); + } + System.out.println(); + for (int i = 1; i <= numCols; i++) { + boolean prepend = isNumeric(meta.getColumnType(i)); + String label = maybePad("(" + meta.getColumnTypeName(i) + "/" + meta.getColumnClassName(i) + ")", meta.getColumnDisplaySize(i), " ", prepend); + System.out.print(label + "\t"); + } + System.out.println(); + for (int i = 1; i <= numCols; i++) { + String label = maybePad("", meta.getColumnDisplaySize(i), "-", false); + System.out.print(label + "\t"); + } + System.out.println(); + while (pResultSet.next()) { + for (int i = 1; i <= numCols; i++) { + boolean prepend = isNumeric(meta.getColumnType(i)); + String value = maybePad(String.valueOf(pResultSet.getString(i)), meta.getColumnDisplaySize(i), " ", prepend); + System.out.print(value + "\t"); + //System.out.print(pResultSet.getString(i) + "\t"); + } + System.out.println(); + } + System.out.println(); + } + catch (SQLException sqle) { + System.err.println("Error: " + sqle.getMessage()); + while ((sqle = sqle.getNextException()) != null) { + System.err.println(" " + sqle); + } + throw sqle; + } + finally { + if (pResultSet != null) { + pResultSet.close(); + } + } + } + + private static String maybePad(String pString, int pColumnDisplaySize, String pPad, boolean pPrepend) { + String padded; + if (pColumnDisplaySize < 100) { + padded = StringUtil.pad(pString, pColumnDisplaySize, pPad, pPrepend); + } + else { + padded = StringUtil.pad(pString, 100, pPad, pPrepend); + } + return padded; + } + + private static boolean isNumeric(int pColumnType) { + return (pColumnType == Types.INTEGER || pColumnType == Types.DECIMAL + || pColumnType == Types.TINYINT || pColumnType == Types.BIGINT + || pColumnType == Types.DOUBLE || pColumnType == Types.FLOAT + || pColumnType == Types.NUMERIC || pColumnType == Types.REAL + || pColumnType == Types.SMALLINT); + } + + public static boolean isDriverAvailable(String pDriver) { + //ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Class.forName(pDriver, false, null); // null means the caller's ClassLoader + return true; + } + catch (ClassNotFoundException ignore) { + // Ignore + } + return false; + } + + public static void registerDriver(String pDriver) { + // Register JDBC driver + try { + Class.forName(pDriver).newInstance(); + } + catch (ClassNotFoundException e) { + throw new RuntimeException("Driver class not found: " + e.getMessage(), e); + //System.err.println("Driver class not found: " + e.getMessage()); + //System.exit(5); + } + catch (InstantiationException e) { + throw new RuntimeException("Driver class could not be instantiated: " + e.getMessage(), e); + //System.err.println("Driver class could not be instantiated: " + e.getMessage()); + //System.exit(5); + } + catch (IllegalAccessException e) { + throw new RuntimeException("Driver class could not be instantiated: " + e.getMessage(), e); + //System.err.println("Driver class could not be instantiated: " + e.getMessage()); + //System.exit(5); + } + } +} \ No newline at end of file diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/package.html b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/package.html new file mode 100755 index 00000000..73bd0b1c --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/sql/package.html @@ -0,0 +1,12 @@ + + + +Provides classes for database access through JDBC. +The package contains warious mechanisms to et connections, read (currently) and write (future) objects from a database, etc. + +@see java.sql +@see com.twelvemonkeys.sql.ObjectReader +@see com.twelvemonkeys.sql.DatabaseConnection + + + diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractResource.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/AbstractResource.java similarity index 87% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractResource.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/AbstractResource.java index 6c7ba54a..39aa8937 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractResource.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/AbstractResource.java @@ -36,8 +36,8 @@ package com.twelvemonkeys.util; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractResource.java#1 $ */ abstract class AbstractResource implements Resource { - protected final Object mResourceId; - protected final Object mWrappedResource; + protected final Object resourceId; + protected final Object wrappedResource; /** * Creates a {@code Resource}. @@ -53,12 +53,12 @@ abstract class AbstractResource implements Resource { throw new IllegalArgumentException("resource == null"); } - mResourceId = pResourceId; - mWrappedResource = pWrappedResource; + resourceId = pResourceId; + wrappedResource = pWrappedResource; } public final Object getId() { - return mResourceId; + return resourceId; } /** @@ -76,7 +76,7 @@ abstract class AbstractResource implements Resource { * @return {@code mWrapped.hashCode()} */ public int hashCode() { - return mWrappedResource.hashCode(); + return wrappedResource.hashCode(); } /** @@ -87,6 +87,6 @@ abstract class AbstractResource implements Resource { */ public boolean equals(Object pObject) { return pObject instanceof AbstractResource - && mWrappedResource.equals(((AbstractResource) pObject).mWrappedResource); + && wrappedResource.equals(((AbstractResource) pObject).wrappedResource); } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/BooleanKey.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/BooleanKey.java similarity index 100% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/BooleanKey.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/BooleanKey.java diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/util/DebugUtil.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/DebugUtil.java similarity index 97% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/util/DebugUtil.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/DebugUtil.java index a3878c3c..3d45d4e9 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/util/DebugUtil.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/DebugUtil.java @@ -1,1757 +1,1757 @@ -/**************************************************** - * * - * (c) 2000-2003 TwelveMonkeys * - * All rights reserved * - * http://www.twelvemonkeys.no * - * * - * $RCSfile: DebugUtil.java,v $ - * @version $Revision: #2 $ - * $Date: 2009/06/19 $ - * * - * @author Last modified by: $Author: haku $ - * * - ****************************************************/ - - - -/* - * Produced (p) 2002 TwelveMonkeys - * Address : Svovelstikka 1, Box 6432 Etterstad, 0605 Oslo, Norway. - * Phone : +47 22 57 70 00 - * Fax : +47 22 57 70 70 - */ -package com.twelvemonkeys.util; - - -import com.twelvemonkeys.lang.StringUtil; - -import java.io.PrintStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.text.DateFormat; -import java.util.*; - - -/** - * A utility class to simplify debugging. - * This includes viewing generic data structures, printing timestamps, printing object info and more... - * NB! Only use this class for instrumentation purposes - *

    - * @author Eirik Torske - */ -@Deprecated -public class DebugUtil { - - // Constants - - /** Field PRINTSTREAM_IS_NULL_ERROR_MESSAGE */ - public static final String PRINTSTREAM_IS_NULL_ERROR_MESSAGE = "PrintStream is null"; - - /** Field OBJECT_IS_NULL_ERROR_MESSAGE */ - public static final String OBJECT_IS_NULL_ERROR_MESSAGE = "Object is null"; - - /** Field INTARRAY_IS_NULL_ERROR_MESSAGE */ - public static final String INTARRAY_IS_NULL_ERROR_MESSAGE = "int array is null"; - - /** Field STRINGARRAY_IS_NULL_ERROR_MESSAGE */ - public static final String STRINGARRAY_IS_NULL_ERROR_MESSAGE = "String array is null"; - - /** Field ENUMERATION_IS_NULL_ERROR_MESSAGE */ - public static final String ENUMERATION_IS_NULL_ERROR_MESSAGE = "Enumeration is null"; - - /** Field COLLECTION_IS_NULL_ERROR_MESSAGE */ - public static final String COLLECTION_IS_NULL_ERROR_MESSAGE = "Collection is null"; - - /** Field COLLECTION_IS_EMPTY_ERROR_MESSAGE */ - public static final String COLLECTION_IS_EMPTY_ERROR_MESSAGE = "Collection contains no elements"; - - /** Field MAP_IS_NULL_ERROR_MESSAGE */ - public static final String MAP_IS_NULL_ERROR_MESSAGE = "Map is null"; - - /** Field MAP_IS_EMPTY_ERROR_MESSAGE */ - public static final String MAP_IS_EMPTY_ERROR_MESSAGE = "Map contains no elements"; - - /** Field PROPERTIES_IS_NULL_ERROR_MESSAGE */ - public static final String PROPERTIES_IS_NULL_ERROR_MESSAGE = "Properties is null"; - - /** Field PROPERTIES_IS_EMPTY_ERROR_MESSAGE */ - public static final String PROPERTIES_IS_EMPTY_ERROR_MESSAGE = "Properties contains no elements"; - - /** Field CALENDAR_IS_NULL_ERROR_MESSAGE */ - public static final String CALENDAR_IS_NULL_ERROR_MESSAGE = "Calendar is null"; - - /** Field CALENDAR_CAUSATION_ERROR_MESSAGE */ - public static final String CALENDAR_CAUSATION_ERROR_MESSAGE = "The causation of the calendars is wrong"; - - /** Field TIMEDIFFERENCES_IS_NULL_ERROR_MESSAGE */ - public static final String TIMEDIFFERENCES_IS_NULL_ERROR_MESSAGE = "Inner TimeDifference object is null"; - - /** Field TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE */ - public static final String TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE = - "Element in TimeDifference collection is not a TimeDifference object"; - - /** Field DEBUG */ - public static final String DEBUG = "**** external debug: "; - - /** Field INFO */ - public static final String INFO = "**** external info: "; - - /** Field WARNING */ - public static final String WARNING = "**** external warning: "; - - /** Field ERROR */ - public static final String ERROR = "**** external error: "; - - /** - * Builds a prefix message to be used in front of info messages for identification purposes. - * The message format is: - *

    -   * **** external info: [timestamp] [class name]:
    -   * 
    - *

    - * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. - * @return a prefix for an info message. - */ - public static String getPrefixInfoMessage(final Object pObject) { - - StringBuilder buffer = new StringBuilder(); - - buffer.append(INFO); - buffer.append(getTimestamp()); - buffer.append(" "); - if (pObject == null) { - buffer.append("[unknown class]"); - } else { - if (pObject instanceof String) { - buffer.append((String) pObject); - } else { - buffer.append(getClassName(pObject)); - } - } - buffer.append(": "); - return buffer.toString(); - } - - /** - * Builds a prefix message to be used in front of debug messages for identification purposes. - * The message format is: - *

    -   * **** external debug: [timestamp] [class name]:
    -   * 
    - *

    - * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. - * @return a prefix for a debug message. - */ - public static String getPrefixDebugMessage(final Object pObject) { - - StringBuilder buffer = new StringBuilder(); - - buffer.append(DEBUG); - buffer.append(getTimestamp()); - buffer.append(" "); - if (pObject == null) { - buffer.append("[unknown class]"); - } else { - if (pObject instanceof String) { - buffer.append((String) pObject); - } else { - buffer.append(getClassName(pObject)); - } - } - buffer.append(": "); - return buffer.toString(); - } - - /** - * Builds a prefix message to be used in front of warning messages for identification purposes. - * The message format is: - *

    -   * **** external warning: [timestamp] [class name]:
    -   * 
    - *

    - * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. - * @return a prefix for a warning message. - */ - public static String getPrefixWarningMessage(final Object pObject) { - - StringBuilder buffer = new StringBuilder(); - - buffer.append(WARNING); - buffer.append(getTimestamp()); - buffer.append(" "); - if (pObject == null) { - buffer.append("[unknown class]"); - } else { - if (pObject instanceof String) { - buffer.append((String) pObject); - } else { - buffer.append(getClassName(pObject)); - } - } - buffer.append(": "); - return buffer.toString(); - } - - /** - * Builds a prefix message to be used in front of error messages for identification purposes. - * The message format is: - *

    -   * **** external error: [timestamp] [class name]:
    -   * 
    - *

    - * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. - * @return a prefix for an error message. - */ - public static String getPrefixErrorMessage(final Object pObject) { - - StringBuilder buffer = new StringBuilder(); - - buffer.append(ERROR); - buffer.append(getTimestamp()); - buffer.append(" "); - if (pObject == null) { - buffer.append("[unknown class]"); - } else { - if (pObject instanceof String) { - buffer.append((String) pObject); - } else { - buffer.append(getClassName(pObject)); - } - } - buffer.append(": "); - return buffer.toString(); - } - - /** - * The "default" method that invokes a given method of an object and prints the results to a {@code java.io.PrintStream}.
    - * The method for invocation must have no formal parameters. If the invoking method does not exist, the {@code toString()} method is called. - * The {@code toString()} method of the returning object is called. - *

    - * @param pObject the {@code java.lang.Object} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printDebug(final Object pObject, final String pMethodName, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pObject == null) { - pPrintStream.println(OBJECT_IS_NULL_ERROR_MESSAGE); - return; - } - if (!StringUtil.isEmpty(pMethodName)) { - try { - Method objectMethod = pObject.getClass().getMethod(pMethodName, null); - Object retVal = objectMethod.invoke(pObject, null); - - if (retVal != null) { - printDebug(retVal, null, pPrintStream); - } else { - throw new Exception(); - } - } catch (Exception e) { - - // Default - pPrintStream.println(pObject.toString()); - } - } else { // Ultimate default - pPrintStream.println(pObject.toString()); - } - } - - /** - * Prints the object's {@code toString()} method to a {@code java.io.PrintStream}. - *

    - * @param pObject the {@code java.lang.Object} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printDebug(final Object pObject, final PrintStream pPrintStream) { - printDebug(pObject, null, pPrintStream); - } - - /** - * Prints the object's {@code toString()} method to {@code System.out}. - *

    - * @param pObject the {@code java.lang.Object} to be printed. - */ - public static void printDebug(final Object pObject) { - printDebug(pObject, System.out); - } - - /** - * Prints a line break. - */ - public static void printDebug() { - System.out.println(); - } - - /** - * Prints a primitive {@code boolean} to {@code System.out}. - *

    - * @param pBoolean the {@code boolean} to be printed. - */ - public static void printDebug(final boolean pBoolean) { - printDebug(new Boolean(pBoolean).toString()); - } - - /** - * Prints a primitive {@code int} to {@code System.out}. - *

    - * - * @param pInt - */ - public static void printDebug(final int pInt) { - printDebug(new Integer(pInt).toString()); - } - - /** - * Prints the content of a {@code int[]} to a {@code java.io.PrintStream}. - *

    - * @param pIntArray the {@code int[]} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printDebug(final int[] pIntArray, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pIntArray == null) { - pPrintStream.println(INTARRAY_IS_NULL_ERROR_MESSAGE); - return; - } - for (int i = 0; i < pIntArray.length; i++) { - pPrintStream.println(pIntArray[i]); - } - } - - /** - * Prints the content of a {@code int[]} to {@code System.out}. - *

    - * @param pIntArray the {@code int[]} to be printed. - */ - public static void printDebug(final int[] pIntArray) { - printDebug(pIntArray, System.out); - } - - /** - * Prints a number of character check methods from the {@code java.lang.Character} class to a {@code java.io.PrintStream}. - *

    - * @param pChar the {@code java.lang.char} to be debugged. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printDebug(final char pChar, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - pPrintStream.println("Character.getNumericValue(pChar): " + Character.getNumericValue(pChar)); - pPrintStream.println("Character.getType(pChar): " + Character.getType(pChar)); - pPrintStream.println("pChar.hashCode(): " + new Character(pChar).hashCode()); - pPrintStream.println("Character.isDefined(pChar): " + Character.isDefined(pChar)); - pPrintStream.println("Character.isDigit(pChar): " + Character.isDigit(pChar)); - pPrintStream.println("Character.isIdentifierIgnorable(pChar): " + Character.isIdentifierIgnorable(pChar)); - pPrintStream.println("Character.isISOControl(pChar): " + Character.isISOControl(pChar)); - pPrintStream.println("Character.isJavaIdentifierPart(pChar): " + Character.isJavaIdentifierPart(pChar)); - pPrintStream.println("Character.isJavaIdentifierStart(pChar): " + Character.isJavaIdentifierStart(pChar)); - pPrintStream.println("Character.isLetter(pChar): " + Character.isLetter(pChar)); - pPrintStream.println("Character.isLetterOrDigit(pChar): " + Character.isLetterOrDigit(pChar)); - pPrintStream.println("Character.isLowerCase(pChar): " + Character.isLowerCase(pChar)); - pPrintStream.println("Character.isSpaceChar(pChar): " + Character.isSpaceChar(pChar)); - pPrintStream.println("Character.isTitleCase(pChar): " + Character.isTitleCase(pChar)); - pPrintStream.println("Character.isUnicodeIdentifierPart(pChar): " + Character.isUnicodeIdentifierPart(pChar)); - pPrintStream.println("Character.isUnicodeIdentifierStart(pChar): " + Character.isUnicodeIdentifierStart(pChar)); - pPrintStream.println("Character.isUpperCase(pChar): " + Character.isUpperCase(pChar)); - pPrintStream.println("Character.isWhitespace(pChar): " + Character.isWhitespace(pChar)); - pPrintStream.println("pChar.toString(): " + new Character(pChar).toString()); - } - - /** - * Prints a number of character check methods from the {@code java.lang.Character} class to {@code System.out}. - *

    - * @param pChar the {@code java.lang.char} to be debugged. - */ - public static void printDebug(final char pChar) { - printDebug(pChar, System.out); - } - - /** - * Prints the content of a {@code java.lang.String[]} to a {@code java.io.PrintStream}. - *

    - * @param pStringArray the {@code java.lang.String[]} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printDebug(final String[] pStringArray, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pStringArray == null) { - pPrintStream.println(STRINGARRAY_IS_NULL_ERROR_MESSAGE); - return; - } - for (int i = 0; i < pStringArray.length; i++) { - pPrintStream.println(pStringArray[i]); - } - } - - /** - * Prints the content of a {@code java.lang.String[]} to {@code System.out}. - *

    - * @param pStringArray the {@code java.lang.String[]} to be printed. - */ - public static void printDebug(final String[] pStringArray) { - printDebug(pStringArray, System.out); - } - - /** - * Invokes a given method of every element in a {@code java.util.Enumeration} and prints the results to a {@code java.io.PrintStream}. - * The method to be invoked must have no formal parameters. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pEnumeration the {@code java.util.Enumeration} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Enumeration} - */ - public static void printDebug(final Enumeration pEnumeration, final String pMethodName, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pEnumeration == null) { - pPrintStream.println(ENUMERATION_IS_NULL_ERROR_MESSAGE); - return; - } - while (pEnumeration.hasMoreElements()) { - printDebug(pEnumeration.nextElement(), pMethodName, pPrintStream); - } - } - - /** - * Invokes a given method of every element in a {@code java.util.Enumeration} and prints the results to a {@code java.io.PrintStream}. - * The method to be invoked must have no formal parameters. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pEnumeration the {@code java.util.Enumeration} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. - * @see {@code java.util.Enumeration} - */ - public static void printDebug(final Enumeration pEnumeration, final String pMethodName) { - printDebug(pEnumeration, pMethodName, System.out); - } - - /** - * Invokes a given method of every element in a {@code java.util.Enumeration} and prints the results to a {@code java.io.PrintStream}. - * The method to be invoked must have no formal parameters. The default is calling an element's {@code toString()} method. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pEnumeration the {@code java.util.Enumeration} to be printed. - * @see {@code java.util.Enumeration} - */ - public static void printDebug(final Enumeration pEnumeration) { - printDebug(pEnumeration, null, System.out); - } - - /** - * Invokes a given method of every element in a {@code java.util.Collection} and prints the results to a {@code java.io.PrintStream}. - * The method to be invoked must have no formal parameters. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, - * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. - *

    - * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. - *

    - * @param pCollection the {@code java.util.Collection} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Collection} - */ - public static void printDebug(final Collection pCollection, final String pMethodName, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pCollection == null) { - pPrintStream.println(COLLECTION_IS_NULL_ERROR_MESSAGE); - return; - } else if (pCollection.isEmpty()) { - pPrintStream.println(COLLECTION_IS_EMPTY_ERROR_MESSAGE); - return; - } - for (Iterator i = pCollection.iterator(); i.hasNext(); ) { - printDebug(i.next(), pMethodName, pPrintStream); - } - } - - /** - * Invokes a given method of every element in a {@code java.util.Collection} and prints the results to {@code System.out}. - * The method to be invoked must have no formal parameters. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, - * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. - *

    - * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. - *

    - * @param pCollection the {@code java.util.Collection} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. - * @see {@code java.util.Collection} - */ - public static void printDebug(final Collection pCollection, final String pMethodName) { - printDebug(pCollection, pMethodName, System.out); - } - - /** - * Prints the content of a {@code java.util.Collection} to a {@code java.io.PrintStream}. - *

    - * Not all data types are supported so far. The default is calling an element's {@code toString()} method. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, - * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. - *

    - * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. - *

    - * @param pCollection the {@code java.util.Collection} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Collection} - */ - public static void printDebug(final Collection pCollection, final PrintStream pPrintStream) { - printDebug(pCollection, null, pPrintStream); - } - - /** - * Prints the content of a {@code java.util.Collection} to {@code System.out}. - *

    - * Not all data types are supported so far. The default is calling an element's {@code toString()} method. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, - * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. - *

    - * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. - *

    - * @param pCollection the {@code java.util.Collection} to be printed. - * @see {@code java.util.Collection} - */ - public static void printDebug(final Collection pCollection) { - printDebug(pCollection, System.out); - } - - /** - * Invokes a given method of every object in a {@code java.util.Map} and prints the results to a {@code java.io.PrintStream}. - * The method called must have no formal parameters. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pMap the {@code java.util.Map} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each mapped object. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Map} - */ - public static void printDebug(final Map pMap, final String pMethodName, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pMap == null) { - pPrintStream.println(MAP_IS_NULL_ERROR_MESSAGE); - return; - } else if (pMap.isEmpty()) { - pPrintStream.println(MAP_IS_EMPTY_ERROR_MESSAGE); - return; - } - Object mKeyObject; - Object mEntryObject; - - for (Iterator i = pMap.keySet().iterator(); i.hasNext(); ) { - mKeyObject = i.next(); - mEntryObject = pMap.get(mKeyObject); - if ((mKeyObject instanceof String) && (mEntryObject instanceof String)) { - pPrintStream.println((String) mKeyObject + ": " + mEntryObject); - } else if ((mKeyObject instanceof String) && (mEntryObject instanceof List)) { - printDebug((List) mEntryObject, pPrintStream); - } else if ((mKeyObject instanceof String) && (mEntryObject instanceof Set)) { - printDebug((Set) mEntryObject, pPrintStream); - } else if (mKeyObject instanceof String) { - if (!StringUtil.isEmpty(pMethodName)) { - try { - Method objectMethod = mEntryObject.getClass().getMethod(pMethodName, null); - Object retVal = objectMethod.invoke(mEntryObject, null); - - if (retVal != null) { - pPrintStream.println((String) mKeyObject + ": " + retVal.toString()); - } else { // Default execution - throw new Exception(); - } - } catch (Exception e) { - - // Default - pPrintStream.println((String) mKeyObject + ": " + mEntryObject.toString()); - } - } else { // Default - pPrintStream.println((String) mKeyObject + ": " + mEntryObject.toString()); - } - } else if ((mKeyObject instanceof Integer) && (mEntryObject instanceof String)) { - pPrintStream.println((Integer) mKeyObject + ": " + mEntryObject); - } else if ((mKeyObject instanceof Integer) && (mEntryObject instanceof List)) { - printDebug((List) mEntryObject, pPrintStream); - } else if ((mKeyObject instanceof String) && (mEntryObject instanceof Set)) { - printDebug((Set) mEntryObject, pPrintStream); - } else if (mKeyObject instanceof Integer) { - if (!StringUtil.isEmpty(pMethodName)) { - try { - Method objectMethod = mEntryObject.getClass().getMethod(pMethodName, null); - Object retVal = objectMethod.invoke(mEntryObject, null); - - if (retVal != null) { - pPrintStream.println((Integer) mKeyObject + ": " + retVal.toString()); - } else { // Default execution - throw new Exception(); - } - } catch (Exception e) { - - // Default - pPrintStream.println((Integer) mKeyObject + ": " + mEntryObject.toString()); - } - } else { // Default - pPrintStream.println((Integer) mKeyObject + ": " + mEntryObject.toString()); - } - } - - // More.. - //else if - } - } - - /** - * Invokes a given method of every object in a {@code java.util.Map} to {@code System.out}. - * The method called must have no formal parameters. - *

    - * If an exception is throwed during the method invocation, the element's {@code toString()} method is called.
    - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pMap the {@code java.util.Map} to be printed. - * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each mapped object. - * @see {@code java.util.Map} - */ - public static void printDebug(final Map pMap, final String pMethodName) { - printDebug(pMap, pMethodName, System.out); - } - - /** - * Prints the content of a {@code java.util.Map} to a {@code java.io.PrintStream}. - *

    - * Not all data types are supported so far. The default is calling an element's {@code toString()} method.
    - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pMap the {@code java.util.Map} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Map} - */ - public static void printDebug(final Map pMap, final PrintStream pPrintStream) { - printDebug(pMap, null, pPrintStream); - } - - /** - * Prints the content of a {@code java.util.Map} to {@code System.out}. - *

    - * Not all data types are supported so far. The default is calling an element's {@code toString()} method.
    - * For bulk data types, recursive invocations and invocations of other methods in this class, are used. - *

    - * @param pMap the {@code java.util.Map} to be printed. - * @see {@code java.util.Map} - */ - public static void printDebug(final Map pMap) { - printDebug(pMap, System.out); - } - - /** - * Prints the content of a {@code java.util.Properties} to a {@code java.io.PrintStream}. - *

    - * @param pProperties the {@code java.util.Properties} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Properties} - */ - public static void printDebug(final Properties pProperties, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pProperties == null) { - pPrintStream.println(PROPERTIES_IS_NULL_ERROR_MESSAGE); - return; - } else if (pProperties.isEmpty()) { - pPrintStream.println(PROPERTIES_IS_EMPTY_ERROR_MESSAGE); - return; - } - for (Enumeration e = pProperties.propertyNames(); e.hasMoreElements(); ) { - String key = (String) e.nextElement(); - - pPrintStream.println(key + ": " + pProperties.getProperty(key)); - } - } - - /** - * Prints the content of a {@code java.util.Properties} to {@code System.out}. - *

    - * @param pProperties the {@code java.util.Properties} to be printed. - * @see {@code java.util.Properties} - */ - public static void printDebug(final Properties pProperties) { - printDebug(pProperties, System.out); - } - - // Timestamp utilities - - /** - * Prints out the calendar time. - *

    - * @param pCalendar the {@code java.util.Calendar} object from which to extract the date information. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Calendar} - */ - public static void printTimestamp(final Calendar pCalendar, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - pPrintStream.println(getTimestamp(pCalendar)); - } - - /** - * Prints out the system time. - *

    - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.GregorianCalendar} - */ - public static void printTimestamp(final PrintStream pPrintStream) { - - GregorianCalendar cal = new GregorianCalendar(); - - printTimestamp(cal, pPrintStream); - } - - /** - * Prints out the system time to {@code System.out}. - */ - public static void printTimestamp() { - printTimestamp(System.out); - } - - /** - * Returns a presentation of the date based on the given milliseconds. - *

    - * @param pMilliseconds The specified number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT. - * @return a presentation of the calendar time. - * @see {@code java.util.Calendar} - */ - public static String getTimestamp(final String pMilliseconds) { - return getTimestamp(Long.parseLong(pMilliseconds)); - } - - /** - * Returns a presentation of the date based on the given milliseconds. - *

    - * @param pMilliseconds The specified number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT. - * @return a presentation of the calendar time. - * @see {@code java.util.Calendar} - */ - public static String getTimestamp(final long pMilliseconds) { - - java.util.Date date = new java.util.Date(pMilliseconds); - java.util.Calendar calendar = new GregorianCalendar(); - - calendar.setTime(date); - return getTimestamp(calendar); - } - - /** - * Returns a presentation of the given calendar's time. - *

    - * @param pCalendar the {@code java.util.Calendar} object from which to extract the date information. - * @return a presentation of the calendar time. - * @see {@code java.util.Calendar} - */ - public static String getTimestamp(final Calendar pCalendar) { - return buildTimestamp(pCalendar); - } - - /** - * @return a presentation of the system time. - */ - public static String getTimestamp() { - - GregorianCalendar cal = new GregorianCalendar(); - - return getTimestamp(cal); - } - - /** - * Builds a presentation of the given calendar's time. This method contains the common timestamp format used in this class. - * @return a presentation of the calendar time. - */ - protected static String buildTimestamp(final Calendar pCalendar) { - - if (pCalendar == null) { - return CALENDAR_IS_NULL_ERROR_MESSAGE; - } - - // The timestamp format - StringBuilder timestamp = new StringBuilder(); - - //timestamp.append(DateUtil.getMonthName(new Integer(pCalendar.get(Calendar.MONTH)).toString(), "0", "us", "MEDIUM", false) + " "); - timestamp.append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(pCalendar.getTime())); - - //timestamp.append(pCalendar.get(Calendar.DAY_OF_MONTH) + " "); - timestamp.append(" "); - timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.HOUR_OF_DAY)).toString(), 2, "0", true) + ":"); - timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.MINUTE)).toString(), 2, "0", true) + ":"); - timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.SECOND)).toString(), 2, "0", true) + ":"); - timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.MILLISECOND)).toString(), 3, "0", true)); - return timestamp.toString(); - } - - /** - * Builds the time difference between two millisecond representations. - *

    - * This method is to be used with small time intervals between 0 ms up to a couple of minutes. - *

    - * @param pStartTime the start time. - * @param pEndTime the end time. - * @return the time difference in milliseconds. - */ - public static String buildTimeDifference(final long pStartTime, final long pEndTime) { - - //return pEndTime - pStartTime; - StringBuilder retVal = new StringBuilder(); - - // The time difference in milliseconds - long timeDifference = pEndTime - pStartTime; - - if (timeDifference < 1000) { - retVal.append(timeDifference); - retVal.append(" ms"); - } else { - long seconds = timeDifference / 1000; - - timeDifference = timeDifference % 1000; - retVal.append(seconds); - retVal.append("s "); - retVal.append(timeDifference); - retVal.append("ms"); - } - - //return retVal.toString() + " (original timeDifference: " + new String(new Long(pEndTime - pStartTime).toString()) + ")"; - return retVal.toString(); - } - - /** - * Builds the time difference between the given time and present time. - *

    - * This method is to be used with small time intervals between 0 ms up to a couple of minutes. - *

    - * @param pStartTime the start time. - * @return the time difference in milliseconds. - */ - public static String buildTimeDifference(final long pStartTime) { - - long presentTime = System.currentTimeMillis(); - - return buildTimeDifference(pStartTime, presentTime); - } - - /** - * Prints out the difference between two millisecond representations. - * The start time is subtracted from the end time. - *

    - * @param pStartTime the start time. - * @param pEndTime the end time. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printTimeDifference(final long pStartTime, final long pEndTime, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - pPrintStream.println(buildTimeDifference(pStartTime, pEndTime)); - } - - /** - * Prints out the difference between two millisecond representations. - * The start time is subtracted from the end time. - *

    - * @param pStartTime the start time. - * @param pEndTime the end time. - */ - public static void printTimeDifference(final long pStartTime, final long pEndTime) { - printTimeDifference(pStartTime, pEndTime, System.out); - } - - /** - * Prints out the difference between the given time and present time. - * The start time is subtracted from the present time. - *

    - * @param pStartTime the start time. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printTimeDifference(final long pStartTime, final PrintStream pPrintStream) { - printTimeDifference(pStartTime, System.currentTimeMillis(), pPrintStream); - } - - /** - * Prints out the difference between the given time and present time to {@code System.out}. - * The start time is subtracted from the present time. - *

    - * usage: - *

    -   * long startTime = System.currentTimeMillis();
    -   * ...
    -   * com.iml.oslo.eito.util.DebugUtil.printTimeDifference(startTime);
    -   * 
    - *

    - * @param pStartTime the start time. - */ - public static void printTimeDifference(final long pStartTime) { - printTimeDifference(pStartTime, System.out); - } - - /** - * Builds a string representing the difference between two calendar times. - * The first calendar object is subtracted from the second one. - *

    - * This method is to be used with time intervals between 0 ms up to several hours. - *

    - * @param pStartCalendar the first {@code java.util.Calendar}. - * @param pEndCalendar the second {@code java.util.Calendar}. - * @return a string representation of the time difference. - * @see {@code java.util.Calendar} - */ - public static String buildTimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar) { - - if (pStartCalendar == null) { - return CALENDAR_IS_NULL_ERROR_MESSAGE; - } - if (pEndCalendar == null) { - return CALENDAR_IS_NULL_ERROR_MESSAGE; - } - if (pEndCalendar.before(pStartCalendar)) { - return CALENDAR_CAUSATION_ERROR_MESSAGE; - } - int dateDiff = pEndCalendar.get(Calendar.DATE) - pStartCalendar.get(Calendar.DATE); - int hourDiff = pEndCalendar.get(Calendar.HOUR_OF_DAY) - pStartCalendar.get(Calendar.HOUR_OF_DAY); - int minuteDiff = pEndCalendar.get(Calendar.MINUTE) - pStartCalendar.get(Calendar.MINUTE); - int secondDiff = pEndCalendar.get(Calendar.SECOND) - pStartCalendar.get(Calendar.SECOND); - int milliSecondDiff = pEndCalendar.get(Calendar.MILLISECOND) - pStartCalendar.get(Calendar.MILLISECOND); - - if (milliSecondDiff < 0) { - secondDiff--; - milliSecondDiff += 1000; - } - if (secondDiff < 0) { - minuteDiff--; - secondDiff += 60; - } - if (minuteDiff < 0) { - hourDiff--; - minuteDiff += 60; - } - while (dateDiff > 0) { - dateDiff--; - hourDiff += 24; - } - - // Time difference presentation format - StringBuilder buffer = new StringBuilder(); - - if ((hourDiff == 0) && (minuteDiff == 0) && (secondDiff == 0)) { - buffer.append(milliSecondDiff); - buffer.append("ms"); - } else if ((hourDiff == 0) && (minuteDiff == 0)) { - buffer.append(secondDiff); - buffer.append("s "); - buffer.append(milliSecondDiff); - buffer.append("ms"); - } else if (hourDiff == 0) { - buffer.append(minuteDiff); - buffer.append("m "); - buffer.append(secondDiff); - buffer.append(","); - buffer.append(milliSecondDiff); - buffer.append("s"); - } else { - buffer.append(hourDiff); - buffer.append("h "); - buffer.append(minuteDiff); - buffer.append("m "); - buffer.append(secondDiff); - buffer.append(","); - buffer.append(milliSecondDiff); - buffer.append("s"); - } - return buffer.toString(); - } - - /** - * Prints out the difference between to calendar times. - * The first calendar object is subtracted from the second one. - *

    - * @param pStartCalendar the first {@code java.util.Calendar}. - * @param pEndCalendar the second {@code java.util.Calendar}. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Calendar} - */ - public static void printTimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - pPrintStream.println(buildTimeDifference(pStartCalendar, pEndCalendar)); - } - - /** - * Prints out the difference between to calendar times two {@code System.out}. - * The first calendar object is subtracted from the second one. - *

    - * @param pStartCalendar the first {@code java.util.Calendar}. - * @param pEndCalendar the second {@code java.util.Calendar}. - * @see {@code java.util.Calendar} - */ - public static void printTimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar) { - printTimeDifference(pStartCalendar, pEndCalendar, System.out); - } - - /** - * Prints out the difference between the given calendar time and present time. - *

    - * @param pStartCalendar the {@code java.util.Calendar} to compare with present time. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.util.Calendar} - */ - public static void printTimeDifference(final Calendar pStartCalendar, final PrintStream pPrintStream) { - - GregorianCalendar endCalendar = new GregorianCalendar(); - - printTimeDifference(pStartCalendar, endCalendar, pPrintStream); - } - - /** - * Prints out the difference between the given calendar time and present time to {@code System.out}. - *

    - * usage: - *

    -   * GregorianCalendar startTime = new GregorianCalendar();
    -   * ...
    -   * com.iml.oslo.eito.util.DebugUtil.printTimeDifference(startTime);
    -   * 
    - *

    - * @param pStartCalendar the {@code java.util.Calendar} to compare with present time. - * @see {@code java.util.Calendar} - */ - public static void printTimeDifference(final Calendar pStartCalendar) { - - GregorianCalendar endCalendar = new GregorianCalendar(); - - printTimeDifference(pStartCalendar, endCalendar); - } - - /** - * Prints out a {@code com.iml.oslo.eito.util.DebugUtil.TimeDifference} object. - *

    - * @param pTimeDifference the {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} to investigate. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printTimeDifference(final TimeDifference pTimeDifference, final PrintStream pPrintStream) { - printTimeDifference(pTimeDifference.getStartCalendar(), pTimeDifference.getEndCalendar(), pPrintStream); - } - - /** - * Prints out a {@code com.iml.oslo.eito.util.DebugUtil.TimeDifference} object to {@code System.out}. - *

    - * @param pTimeDifference the {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} to investigate. - */ - public static void printTimeDifference(final TimeDifference pTimeDifference) { - printTimeDifference(pTimeDifference.getStartCalendar(), pTimeDifference.getEndCalendar(), System.out); - } - - /** - * A convenience class for embracing two {@code java.util.Calendar} objects. - * The class is used for building a collection of time differences according to the {@code printTimeAverage} method. - */ - public static class TimeDifference { - - Calendar mStartCalendar; - Calendar mEndCalendar; - - /** - * Constructor TimeDifference - * - * - */ - public TimeDifference() {} - - /** - * Constructor TimeDifference - * - * - * @param pStartCalendar - * @param pEndCalendar - * - */ - public TimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar) { - this.mStartCalendar = pStartCalendar; - this.mEndCalendar = pEndCalendar; - } - - /** - * Method setStartCalendar - * - * - * @param pStartCalendar - * - */ - public void setStartCalendar(Calendar pStartCalendar) { - this.mStartCalendar = pStartCalendar; - } - - /** - * Method getStartCalendar - * - * - * @return - * - */ - public Calendar getStartCalendar() { - return this.mStartCalendar; - } - - /** - * Method setEndCalendar - * - * - * @param pEndCalendar - * - */ - public void setEndCalendar(Calendar pEndCalendar) { - this.mEndCalendar = pEndCalendar; - } - - /** - * Method getEndCalendar - * - * - * @return - * - */ - public Calendar getEndCalendar() { - return this.mEndCalendar; - } - } - - /** - * Prints out the average time difference from a collection of {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} objects. - *

    - * - * @param pTimeDifferences - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - */ - public static void printTimeAverage(final Collection pTimeDifferences, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - if (pTimeDifferences == null) { - pPrintStream.println(TIMEDIFFERENCES_IS_NULL_ERROR_MESSAGE); - return; - } - Object o; - TimeDifference timeDifference; - Calendar startCalendar = null; - Calendar endCalendar = null; - Calendar totalStartCalendar = null; - Calendar totalEndCalendar = null; - long startCalendarMilliSeconds, endCalendarMilliSeconds; - List timeDifferenceList = new Vector(); - Iterator i = pTimeDifferences.iterator(); - - if (i.hasNext()) { - o = i.next(); - if (!(o instanceof TimeDifference)) { - pPrintStream.println(TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE); - return; - } - timeDifference = (TimeDifference) o; - startCalendar = timeDifference.getStartCalendar(); - totalStartCalendar = startCalendar; - endCalendar = timeDifference.getEndCalendar(); - startCalendarMilliSeconds = startCalendar.getTime().getTime(); - endCalendarMilliSeconds = endCalendar.getTime().getTime(); - timeDifferenceList.add(new Long(endCalendarMilliSeconds - startCalendarMilliSeconds)); - } - while (i.hasNext()) { - o = i.next(); - if (!(o instanceof TimeDifference)) { - pPrintStream.println(TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE); - return; - } - timeDifference = (TimeDifference) o; - startCalendar = timeDifference.getStartCalendar(); - endCalendar = timeDifference.getEndCalendar(); - startCalendarMilliSeconds = startCalendar.getTime().getTime(); - endCalendarMilliSeconds = endCalendar.getTime().getTime(); - timeDifferenceList.add(new Long(endCalendarMilliSeconds - startCalendarMilliSeconds)); - } - totalEndCalendar = endCalendar; - int numberOfElements = timeDifferenceList.size(); - long timeDifferenceElement; - long timeDifferenceSum = 0; - - for (Iterator i2 = timeDifferenceList.iterator(); i2.hasNext(); ) { - timeDifferenceElement = ((Long) i2.next()).longValue(); - timeDifferenceSum += timeDifferenceElement; - } - - // Total elapsed time - String totalElapsedTime = buildTimeDifference(totalStartCalendar, totalEndCalendar); - - // Time average presentation format - pPrintStream.println("Average time difference: " + timeDifferenceSum / numberOfElements + "ms (" + numberOfElements - + " elements, total elapsed time: " + totalElapsedTime + ")"); - } - - /** - * Prints out the average time difference from a collection of {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} objects to {@code System.out}. - *

    - * - * @param pTimeDifferences - */ - public static void printTimeAverage(final Collection pTimeDifferences) { - printTimeAverage(pTimeDifferences, System.out); - } - - // Reflective methods - - /** - * Prints the top-wrapped class name of a {@code java.lang.Object} to a {@code java.io.PrintStream}. - *

    - * @param pObject the {@code java.lang.Object} to be printed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.lang.Class} - */ - public static void printClassName(final Object pObject, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - pPrintStream.println(getClassName(pObject)); - } - - /** - * Prints the top-wrapped class name of a {@code java.lang.Object} to {@code System.out}. - *

    - * @param pObject the {@code java.lang.Object} to be printed. - * @see {@code java.lang.Class} - */ - public static void printClassName(final Object pObject) { - printClassName(pObject, System.out); - } - - /** - * Builds the top-wrapped class name of a {@code java.lang.Object}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @return the object's class name. - * @see {@code java.lang.Class} - */ - public static String getClassName(final Object pObject) { - - if (pObject == null) { - return OBJECT_IS_NULL_ERROR_MESSAGE; - } - return pObject.getClass().getName(); - } - - /** - * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to a {@code java.io.PrintStream}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @param pObjectName the name of the object instance, for identification purposes. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.lang.Class} - * @see {@code java.lang.reflect.Modifier} - * @see {@code java.lang.reflect.Field} - * @see {@code java.lang.reflect.Constructor} - * @see {@code java.lang.reflect.Method} - */ - public static void printClassDetails(final Object pObject, final String pObjectName, final PrintStream pPrintStream) { - - if (pPrintStream == null) { - System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); - return; - } - pPrintStream.println(getClassDetails(pObject, pObjectName)); - } - - /** - * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to {@code System.out}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @param pObjectName the name of the object instance, for identification purposes. - * @see {@code java.lang.Class} - * @see {@code java.lang.reflect.Modifier} - * @see {@code java.lang.reflect.Field} - * @see {@code java.lang.reflect.Constructor} - * @see {@code java.lang.reflect.Method} - */ - public static void printClassDetails(final Object pObject, final String pObjectName) { - printClassDetails(pObject, pObjectName, System.out); - } - - /** - * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to {@code System.out}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @see {@code java.lang.Class} - * @see {@code java.lang.reflect.Modifier} - * @see {@code java.lang.reflect.Field} - * @see {@code java.lang.reflect.Constructor} - * @see {@code java.lang.reflect.Method} - */ - public static void printClassDetails(final Object pObject) { - printClassDetails(pObject, null, System.out); - } - - /** - * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to a {@code java.io.PrintStream}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. - * @see {@code java.lang.Class} - * @see {@code java.lang.reflect.Modifier} - * @see {@code java.lang.reflect.Field} - * @see {@code java.lang.reflect.Constructor} - * @see {@code java.lang.reflect.Method} - */ - public static void printClassDetails(final Object pObject, final PrintStream pPrintStream) { - printClassDetails(pObject, null, pPrintStream); - } - - /** - * Builds a javadoc-like presentation of the top wrapped class fields and methods of a {@code java.lang.Object}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @return a listing of the object's class details. - * @see {@code java.lang.Class} - * @see {@code java.lang.reflect.Modifier} - * @see {@code java.lang.reflect.Field} - * @see {@code java.lang.reflect.Constructor} - * @see {@code java.lang.reflect.Method} - */ - public static String getClassDetails(final Object pObject) { - return getClassDetails(pObject, null); - } - - /** - * Builds a javadoc-like presentation of the top wrapped class fields and methods of a {@code java.lang.Object}. - *

    - * @param pObject the {@code java.lang.Object} to be analysed. - * @param pObjectName the name of the object instance, for identification purposes. - * @return a listing of the object's class details. - * @see {@code java.lang.Class} - * @see {@code java.lang.reflect.Modifier} - * @see {@code java.lang.reflect.Field} - * @see {@code java.lang.reflect.Constructor} - * @see {@code java.lang.reflect.Method} - */ - public static String getClassDetails(final Object pObject, final String pObjectName) { - - if (pObject == null) { - return OBJECT_IS_NULL_ERROR_MESSAGE; - } - final String endOfLine = System.getProperty("line.separator"); - final String dividerLine = "---------------------------------------------------------"; - Class c = pObject.getClass(); - StringTokenizer tokenizedString; - String str; - String className = new String(); - String superClassName = new String(); - StringBuilder buffer = new StringBuilder(); - - // Heading - buffer.append(endOfLine); - buffer.append("**** class details"); - if (!StringUtil.isEmpty(pObjectName)) { - buffer.append(" for \"" + pObjectName + "\""); - } - buffer.append(" ****"); - buffer.append(endOfLine); - - // Package - Package p = c.getPackage(); - - if (p != null) { - buffer.append(p.getName()); - } - buffer.append(endOfLine); - - // Class or Interface - if (c.isInterface()) { - buffer.append("I n t e r f a c e "); - } else { - buffer.append("C l a s s "); - } - str = c.getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - className = tokenizedString.nextToken().trim(); - } - str = new String(); - char[] charArray = className.toCharArray(); - - for (int i = 0; i < charArray.length; i++) { - str += charArray[i] + " "; - } - buffer.append(str); - buffer.append(endOfLine); - buffer.append(endOfLine); - - // Class Hierarch - List classNameList = new Vector(); - - classNameList.add(c.getName()); - Class superclass = c.getSuperclass(); - - while (superclass != null) { - classNameList.add(superclass.getName()); - superclass = superclass.getSuperclass(); - } - Object[] classNameArray = classNameList.toArray(); - int counter = 0; - - for (int i = classNameArray.length - 1; i >= 0; i--) { - for (int j = 0; j < counter; j++) { - buffer.append(" "); - } - if (counter > 0) { - buffer.append("|"); - buffer.append(endOfLine); - } - for (int j = 0; j < counter; j++) { - buffer.append(" "); - } - if (counter > 0) { - buffer.append("+-"); - } - buffer.append((String) classNameArray[i]); - buffer.append(endOfLine); - counter++; - } - - // Divider - buffer.append(endOfLine); - buffer.append(dividerLine); - buffer.append(endOfLine); - buffer.append(endOfLine); - - // Profile - int classModifier = c.getModifiers(); - - buffer.append(Modifier.toString(classModifier) + " "); - if (c.isInterface()) { - buffer.append("Interface "); - } else { - buffer.append("Class "); - } - buffer.append(className); - buffer.append(endOfLine); - if ((classNameArray != null) && (classNameArray[classNameArray.length - 2] != null)) { - str = (String) classNameArray[classNameArray.length - 2]; - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - superClassName = tokenizedString.nextToken().trim(); - } - buffer.append("extends " + superClassName); - buffer.append(endOfLine); - } - if (!c.isInterface()) { - Class[] interfaces = c.getInterfaces(); - - if ((interfaces != null) && (interfaces.length > 0)) { - buffer.append("implements "); - str = interfaces[0].getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(str); - for (int i = 1; i < interfaces.length; i++) { - str = interfaces[i].getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(", " + str); - } - buffer.append(endOfLine); - } - } - - // Divider - buffer.append(endOfLine); - buffer.append(dividerLine); - buffer.append(endOfLine); - buffer.append(endOfLine); - - // Fields - buffer.append("F I E L D S U M M A R Y"); - buffer.append(endOfLine); - Field[] fields = c.getFields(); - - if (fields != null) { - for (int i = 0; i < fields.length; i++) { - buffer.append(Modifier.toString(fields[i].getType().getModifiers()) + " "); - str = fields[i].getType().getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(str + " "); - buffer.append(fields[i].getName()); - buffer.append(endOfLine); - } - } - buffer.append(endOfLine); - - // Constructors - buffer.append("C O N S T R U C T O R S U M M A R Y"); - buffer.append(endOfLine); - Constructor[] constructors = c.getConstructors(); - - if (constructors != null) { - for (int i = 0; i < constructors.length; i++) { - buffer.append(className + "("); - Class[] parameterTypes = constructors[i].getParameterTypes(); - - if (parameterTypes != null) { - if (parameterTypes.length > 0) { - str = parameterTypes[0].getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(str); - for (int j = 1; j < parameterTypes.length; j++) { - str = parameterTypes[j].getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(", " + str); - } - } - } - buffer.append(")"); - buffer.append(endOfLine); - } - } - buffer.append(endOfLine); - - // Methods - buffer.append("M E T H O D S U M M A R Y"); - buffer.append(endOfLine); - Method[] methods = c.getMethods(); - - if (methods != null) { - for (int i = 0; i < methods.length; i++) { - buffer.append(Modifier.toString(methods[i].getModifiers()) + " "); - str = methods[i].getReturnType().getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(str + " "); - buffer.append(methods[i].getName() + "("); - Class[] parameterTypes = methods[i].getParameterTypes(); - - if ((parameterTypes != null) && (parameterTypes.length > 0)) { - if (parameterTypes[0] != null) { - str = parameterTypes[0].getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - - // array bugfix - if (str.charAt(str.length() - 1) == ';') { - str = str.substring(0, str.length() - 1) + "[]"; - } - buffer.append(str); - for (int j = 1; j < parameterTypes.length; j++) { - str = parameterTypes[j].getName(); - tokenizedString = new StringTokenizer(str, "."); - while (tokenizedString.hasMoreTokens()) { - str = tokenizedString.nextToken().trim(); - } - buffer.append(", " + str); - } - } - } - buffer.append(")"); - buffer.append(endOfLine); - } - } - buffer.append(endOfLine); - - // Ending - buffer.append("**** class details"); - if (!StringUtil.isEmpty(pObjectName)) { - buffer.append(" for \"" + pObjectName + "\""); - } - buffer.append(" end ****"); - buffer.append(endOfLine); - return buffer.toString(); - } - - /** - * Prettyprints a large number. - *

    - * - * @param pBigNumber - * @return prettyprinted number with dot-separation each 10e3. - */ - public static String getLargeNumber(final long pBigNumber) { - - StringBuilder buffer = new StringBuilder(new Long(pBigNumber).toString()); - char[] number = new Long(pBigNumber).toString().toCharArray(); - int reverseIndex = 0; - - for (int i = number.length; i >= 0; i--) { - reverseIndex++; - if ((reverseIndex % 3 == 0) && (i > 1)) { - buffer = buffer.insert(i - 1, '.'); - } - } - return buffer.toString(); - } - - /** - * Prettyprints milliseconds to ?day(s) ?h ?m ?s ?ms. - *

    - * - * @param pMilliseconds - * @return prettyprinted time duration. - */ - public static String getTimeInterval(final long pMilliseconds) { - - long timeIntervalMilliseconds = pMilliseconds; - long timeIntervalSeconds = 0; - long timeIntervalMinutes = 0; - long timeIntervalHours = 0; - long timeIntervalDays = 0; - boolean printMilliseconds = true; - boolean printSeconds = false; - boolean printMinutes = false; - boolean printHours = false; - boolean printDays = false; - final long MILLISECONDS_IN_SECOND = 1000; - final long MILLISECONDS_IN_MINUTE = 60 * MILLISECONDS_IN_SECOND; // 60000 - final long MILLISECONDS_IN_HOUR = 60 * MILLISECONDS_IN_MINUTE; // 3600000 - final long MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; // 86400000 - StringBuilder timeIntervalBuffer = new StringBuilder(); - - // Days - if (timeIntervalMilliseconds >= MILLISECONDS_IN_DAY) { - timeIntervalDays = timeIntervalMilliseconds / MILLISECONDS_IN_DAY; - timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_DAY; - printDays = true; - printHours = true; - printMinutes = true; - printSeconds = true; - } - - // Hours - if (timeIntervalMilliseconds >= MILLISECONDS_IN_HOUR) { - timeIntervalHours = timeIntervalMilliseconds / MILLISECONDS_IN_HOUR; - timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_HOUR; - printHours = true; - printMinutes = true; - printSeconds = true; - } - - // Minutes - if (timeIntervalMilliseconds >= MILLISECONDS_IN_MINUTE) { - timeIntervalMinutes = timeIntervalMilliseconds / MILLISECONDS_IN_MINUTE; - timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_MINUTE; - printMinutes = true; - printSeconds = true; - } - - // Seconds - if (timeIntervalMilliseconds >= MILLISECONDS_IN_SECOND) { - timeIntervalSeconds = timeIntervalMilliseconds / MILLISECONDS_IN_SECOND; - timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_SECOND; - printSeconds = true; - } - - // Prettyprint - if (printDays) { - timeIntervalBuffer.append(timeIntervalDays); - if (timeIntervalDays > 1) { - timeIntervalBuffer.append("days "); - } else { - timeIntervalBuffer.append("day "); - } - } - if (printHours) { - timeIntervalBuffer.append(timeIntervalHours); - timeIntervalBuffer.append("h "); - } - if (printMinutes) { - timeIntervalBuffer.append(timeIntervalMinutes); - timeIntervalBuffer.append("m "); - } - if (printSeconds) { - timeIntervalBuffer.append(timeIntervalSeconds); - timeIntervalBuffer.append("s "); - } - if (printMilliseconds) { - timeIntervalBuffer.append(timeIntervalMilliseconds); - timeIntervalBuffer.append("ms"); - } - return timeIntervalBuffer.toString(); - } -} - - -/*--- Formatted in Sun Java Convention Style on ma, des 1, '03 ---*/ - - -/*------ Formatted by Jindent 3.23 Basic 1.0 --- http://www.jindent.de ------*/ +/**************************************************** + * * + * (c) 2000-2003 TwelveMonkeys * + * All rights reserved * + * http://www.twelvemonkeys.no * + * * + * $RCSfile: DebugUtil.java,v $ + * @version $Revision: #2 $ + * $Date: 2009/06/19 $ + * * + * @author Last modified by: $Author: haku $ + * * + ****************************************************/ + + + +/* + * Produced (p) 2002 TwelveMonkeys + * Address : Svovelstikka 1, Box 6432 Etterstad, 0605 Oslo, Norway. + * Phone : +47 22 57 70 00 + * Fax : +47 22 57 70 70 + */ +package com.twelvemonkeys.util; + + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.text.DateFormat; +import java.util.*; + + +/** + * A utility class to simplify debugging. + * This includes viewing generic data structures, printing timestamps, printing object info and more... + * NB! Only use this class for instrumentation purposes + *

    + * @author Eirik Torske + */ +@Deprecated +public class DebugUtil { + + // Constants + + /** Field PRINTSTREAM_IS_NULL_ERROR_MESSAGE */ + public static final String PRINTSTREAM_IS_NULL_ERROR_MESSAGE = "PrintStream is null"; + + /** Field OBJECT_IS_NULL_ERROR_MESSAGE */ + public static final String OBJECT_IS_NULL_ERROR_MESSAGE = "Object is null"; + + /** Field INTARRAY_IS_NULL_ERROR_MESSAGE */ + public static final String INTARRAY_IS_NULL_ERROR_MESSAGE = "int array is null"; + + /** Field STRINGARRAY_IS_NULL_ERROR_MESSAGE */ + public static final String STRINGARRAY_IS_NULL_ERROR_MESSAGE = "String array is null"; + + /** Field ENUMERATION_IS_NULL_ERROR_MESSAGE */ + public static final String ENUMERATION_IS_NULL_ERROR_MESSAGE = "Enumeration is null"; + + /** Field COLLECTION_IS_NULL_ERROR_MESSAGE */ + public static final String COLLECTION_IS_NULL_ERROR_MESSAGE = "Collection is null"; + + /** Field COLLECTION_IS_EMPTY_ERROR_MESSAGE */ + public static final String COLLECTION_IS_EMPTY_ERROR_MESSAGE = "Collection contains no elements"; + + /** Field MAP_IS_NULL_ERROR_MESSAGE */ + public static final String MAP_IS_NULL_ERROR_MESSAGE = "Map is null"; + + /** Field MAP_IS_EMPTY_ERROR_MESSAGE */ + public static final String MAP_IS_EMPTY_ERROR_MESSAGE = "Map contains no elements"; + + /** Field PROPERTIES_IS_NULL_ERROR_MESSAGE */ + public static final String PROPERTIES_IS_NULL_ERROR_MESSAGE = "Properties is null"; + + /** Field PROPERTIES_IS_EMPTY_ERROR_MESSAGE */ + public static final String PROPERTIES_IS_EMPTY_ERROR_MESSAGE = "Properties contains no elements"; + + /** Field CALENDAR_IS_NULL_ERROR_MESSAGE */ + public static final String CALENDAR_IS_NULL_ERROR_MESSAGE = "Calendar is null"; + + /** Field CALENDAR_CAUSATION_ERROR_MESSAGE */ + public static final String CALENDAR_CAUSATION_ERROR_MESSAGE = "The causation of the calendars is wrong"; + + /** Field TIMEDIFFERENCES_IS_NULL_ERROR_MESSAGE */ + public static final String TIMEDIFFERENCES_IS_NULL_ERROR_MESSAGE = "Inner TimeDifference object is null"; + + /** Field TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE */ + public static final String TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE = + "Element in TimeDifference collection is not a TimeDifference object"; + + /** Field DEBUG */ + public static final String DEBUG = "**** external debug: "; + + /** Field INFO */ + public static final String INFO = "**** external info: "; + + /** Field WARNING */ + public static final String WARNING = "**** external warning: "; + + /** Field ERROR */ + public static final String ERROR = "**** external error: "; + + /** + * Builds a prefix message to be used in front of info messages for identification purposes. + * The message format is: + *

    +   * **** external info: [timestamp] [class name]:
    +   * 
    + *

    + * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. + * @return a prefix for an info message. + */ + public static String getPrefixInfoMessage(final Object pObject) { + + StringBuilder buffer = new StringBuilder(); + + buffer.append(INFO); + buffer.append(getTimestamp()); + buffer.append(" "); + if (pObject == null) { + buffer.append("[unknown class]"); + } else { + if (pObject instanceof String) { + buffer.append((String) pObject); + } else { + buffer.append(getClassName(pObject)); + } + } + buffer.append(": "); + return buffer.toString(); + } + + /** + * Builds a prefix message to be used in front of debug messages for identification purposes. + * The message format is: + *

    +   * **** external debug: [timestamp] [class name]:
    +   * 
    + *

    + * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. + * @return a prefix for a debug message. + */ + public static String getPrefixDebugMessage(final Object pObject) { + + StringBuilder buffer = new StringBuilder(); + + buffer.append(DEBUG); + buffer.append(getTimestamp()); + buffer.append(" "); + if (pObject == null) { + buffer.append("[unknown class]"); + } else { + if (pObject instanceof String) { + buffer.append((String) pObject); + } else { + buffer.append(getClassName(pObject)); + } + } + buffer.append(": "); + return buffer.toString(); + } + + /** + * Builds a prefix message to be used in front of warning messages for identification purposes. + * The message format is: + *

    +   * **** external warning: [timestamp] [class name]:
    +   * 
    + *

    + * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. + * @return a prefix for a warning message. + */ + public static String getPrefixWarningMessage(final Object pObject) { + + StringBuilder buffer = new StringBuilder(); + + buffer.append(WARNING); + buffer.append(getTimestamp()); + buffer.append(" "); + if (pObject == null) { + buffer.append("[unknown class]"); + } else { + if (pObject instanceof String) { + buffer.append((String) pObject); + } else { + buffer.append(getClassName(pObject)); + } + } + buffer.append(": "); + return buffer.toString(); + } + + /** + * Builds a prefix message to be used in front of error messages for identification purposes. + * The message format is: + *

    +   * **** external error: [timestamp] [class name]:
    +   * 
    + *

    + * @param pObject the {@code java.lang.Object} to be debugged. If the object ia a {@code java.lang.String} object, it is assumed that it is the class name given directly. + * @return a prefix for an error message. + */ + public static String getPrefixErrorMessage(final Object pObject) { + + StringBuilder buffer = new StringBuilder(); + + buffer.append(ERROR); + buffer.append(getTimestamp()); + buffer.append(" "); + if (pObject == null) { + buffer.append("[unknown class]"); + } else { + if (pObject instanceof String) { + buffer.append((String) pObject); + } else { + buffer.append(getClassName(pObject)); + } + } + buffer.append(": "); + return buffer.toString(); + } + + /** + * The "default" method that invokes a given method of an object and prints the results to a {@code java.io.PrintStream}.
    + * The method for invocation must have no formal parameters. If the invoking method does not exist, the {@code toString()} method is called. + * The {@code toString()} method of the returning object is called. + *

    + * @param pObject the {@code java.lang.Object} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printDebug(final Object pObject, final String pMethodName, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pObject == null) { + pPrintStream.println(OBJECT_IS_NULL_ERROR_MESSAGE); + return; + } + if (!StringUtil.isEmpty(pMethodName)) { + try { + Method objectMethod = pObject.getClass().getMethod(pMethodName, null); + Object retVal = objectMethod.invoke(pObject, null); + + if (retVal != null) { + printDebug(retVal, null, pPrintStream); + } else { + throw new Exception(); + } + } catch (Exception e) { + + // Default + pPrintStream.println(pObject.toString()); + } + } else { // Ultimate default + pPrintStream.println(pObject.toString()); + } + } + + /** + * Prints the object's {@code toString()} method to a {@code java.io.PrintStream}. + *

    + * @param pObject the {@code java.lang.Object} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printDebug(final Object pObject, final PrintStream pPrintStream) { + printDebug(pObject, null, pPrintStream); + } + + /** + * Prints the object's {@code toString()} method to {@code System.out}. + *

    + * @param pObject the {@code java.lang.Object} to be printed. + */ + public static void printDebug(final Object pObject) { + printDebug(pObject, System.out); + } + + /** + * Prints a line break. + */ + public static void printDebug() { + System.out.println(); + } + + /** + * Prints a primitive {@code boolean} to {@code System.out}. + *

    + * @param pBoolean the {@code boolean} to be printed. + */ + public static void printDebug(final boolean pBoolean) { + printDebug(new Boolean(pBoolean).toString()); + } + + /** + * Prints a primitive {@code int} to {@code System.out}. + *

    + * + * @param pInt + */ + public static void printDebug(final int pInt) { + printDebug(new Integer(pInt).toString()); + } + + /** + * Prints the content of a {@code int[]} to a {@code java.io.PrintStream}. + *

    + * @param pIntArray the {@code int[]} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printDebug(final int[] pIntArray, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pIntArray == null) { + pPrintStream.println(INTARRAY_IS_NULL_ERROR_MESSAGE); + return; + } + for (int i = 0; i < pIntArray.length; i++) { + pPrintStream.println(pIntArray[i]); + } + } + + /** + * Prints the content of a {@code int[]} to {@code System.out}. + *

    + * @param pIntArray the {@code int[]} to be printed. + */ + public static void printDebug(final int[] pIntArray) { + printDebug(pIntArray, System.out); + } + + /** + * Prints a number of character check methods from the {@code java.lang.Character} class to a {@code java.io.PrintStream}. + *

    + * @param pChar the {@code java.lang.char} to be debugged. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printDebug(final char pChar, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + pPrintStream.println("Character.getNumericValue(pChar): " + Character.getNumericValue(pChar)); + pPrintStream.println("Character.getType(pChar): " + Character.getType(pChar)); + pPrintStream.println("pChar.hashCode(): " + new Character(pChar).hashCode()); + pPrintStream.println("Character.isDefined(pChar): " + Character.isDefined(pChar)); + pPrintStream.println("Character.isDigit(pChar): " + Character.isDigit(pChar)); + pPrintStream.println("Character.isIdentifierIgnorable(pChar): " + Character.isIdentifierIgnorable(pChar)); + pPrintStream.println("Character.isISOControl(pChar): " + Character.isISOControl(pChar)); + pPrintStream.println("Character.isJavaIdentifierPart(pChar): " + Character.isJavaIdentifierPart(pChar)); + pPrintStream.println("Character.isJavaIdentifierStart(pChar): " + Character.isJavaIdentifierStart(pChar)); + pPrintStream.println("Character.isLetter(pChar): " + Character.isLetter(pChar)); + pPrintStream.println("Character.isLetterOrDigit(pChar): " + Character.isLetterOrDigit(pChar)); + pPrintStream.println("Character.isLowerCase(pChar): " + Character.isLowerCase(pChar)); + pPrintStream.println("Character.isSpaceChar(pChar): " + Character.isSpaceChar(pChar)); + pPrintStream.println("Character.isTitleCase(pChar): " + Character.isTitleCase(pChar)); + pPrintStream.println("Character.isUnicodeIdentifierPart(pChar): " + Character.isUnicodeIdentifierPart(pChar)); + pPrintStream.println("Character.isUnicodeIdentifierStart(pChar): " + Character.isUnicodeIdentifierStart(pChar)); + pPrintStream.println("Character.isUpperCase(pChar): " + Character.isUpperCase(pChar)); + pPrintStream.println("Character.isWhitespace(pChar): " + Character.isWhitespace(pChar)); + pPrintStream.println("pChar.toString(): " + new Character(pChar).toString()); + } + + /** + * Prints a number of character check methods from the {@code java.lang.Character} class to {@code System.out}. + *

    + * @param pChar the {@code java.lang.char} to be debugged. + */ + public static void printDebug(final char pChar) { + printDebug(pChar, System.out); + } + + /** + * Prints the content of a {@code java.lang.String[]} to a {@code java.io.PrintStream}. + *

    + * @param pStringArray the {@code java.lang.String[]} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printDebug(final String[] pStringArray, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pStringArray == null) { + pPrintStream.println(STRINGARRAY_IS_NULL_ERROR_MESSAGE); + return; + } + for (int i = 0; i < pStringArray.length; i++) { + pPrintStream.println(pStringArray[i]); + } + } + + /** + * Prints the content of a {@code java.lang.String[]} to {@code System.out}. + *

    + * @param pStringArray the {@code java.lang.String[]} to be printed. + */ + public static void printDebug(final String[] pStringArray) { + printDebug(pStringArray, System.out); + } + + /** + * Invokes a given method of every element in a {@code java.util.Enumeration} and prints the results to a {@code java.io.PrintStream}. + * The method to be invoked must have no formal parameters. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pEnumeration the {@code java.util.Enumeration} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Enumeration} + */ + public static void printDebug(final Enumeration pEnumeration, final String pMethodName, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pEnumeration == null) { + pPrintStream.println(ENUMERATION_IS_NULL_ERROR_MESSAGE); + return; + } + while (pEnumeration.hasMoreElements()) { + printDebug(pEnumeration.nextElement(), pMethodName, pPrintStream); + } + } + + /** + * Invokes a given method of every element in a {@code java.util.Enumeration} and prints the results to a {@code java.io.PrintStream}. + * The method to be invoked must have no formal parameters. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pEnumeration the {@code java.util.Enumeration} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. + * @see {@code java.util.Enumeration} + */ + public static void printDebug(final Enumeration pEnumeration, final String pMethodName) { + printDebug(pEnumeration, pMethodName, System.out); + } + + /** + * Invokes a given method of every element in a {@code java.util.Enumeration} and prints the results to a {@code java.io.PrintStream}. + * The method to be invoked must have no formal parameters. The default is calling an element's {@code toString()} method. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pEnumeration the {@code java.util.Enumeration} to be printed. + * @see {@code java.util.Enumeration} + */ + public static void printDebug(final Enumeration pEnumeration) { + printDebug(pEnumeration, null, System.out); + } + + /** + * Invokes a given method of every element in a {@code java.util.Collection} and prints the results to a {@code java.io.PrintStream}. + * The method to be invoked must have no formal parameters. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, + * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. + *

    + * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. + *

    + * @param pCollection the {@code java.util.Collection} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Collection} + */ + public static void printDebug(final Collection pCollection, final String pMethodName, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pCollection == null) { + pPrintStream.println(COLLECTION_IS_NULL_ERROR_MESSAGE); + return; + } else if (pCollection.isEmpty()) { + pPrintStream.println(COLLECTION_IS_EMPTY_ERROR_MESSAGE); + return; + } + for (Iterator i = pCollection.iterator(); i.hasNext(); ) { + printDebug(i.next(), pMethodName, pPrintStream); + } + } + + /** + * Invokes a given method of every element in a {@code java.util.Collection} and prints the results to {@code System.out}. + * The method to be invoked must have no formal parameters. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, + * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. + *

    + * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. + *

    + * @param pCollection the {@code java.util.Collection} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each collection element. + * @see {@code java.util.Collection} + */ + public static void printDebug(final Collection pCollection, final String pMethodName) { + printDebug(pCollection, pMethodName, System.out); + } + + /** + * Prints the content of a {@code java.util.Collection} to a {@code java.io.PrintStream}. + *

    + * Not all data types are supported so far. The default is calling an element's {@code toString()} method. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, + * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. + *

    + * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. + *

    + * @param pCollection the {@code java.util.Collection} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Collection} + */ + public static void printDebug(final Collection pCollection, final PrintStream pPrintStream) { + printDebug(pCollection, null, pPrintStream); + } + + /** + * Prints the content of a {@code java.util.Collection} to {@code System.out}. + *

    + * Not all data types are supported so far. The default is calling an element's {@code toString()} method. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * Be aware that the {@code Collection} interface embraces a large portion of the bulk data types in the {@code java.util} package, + * e.g. {@code List}, {@code Set}, {@code Vector} and {@code HashSet}. + *

    + * For debugging of arrays, use the method {@code java.util.Arrays.asList(Object[])} method for converting the object array to a list before calling this method. + *

    + * @param pCollection the {@code java.util.Collection} to be printed. + * @see {@code java.util.Collection} + */ + public static void printDebug(final Collection pCollection) { + printDebug(pCollection, System.out); + } + + /** + * Invokes a given method of every object in a {@code java.util.Map} and prints the results to a {@code java.io.PrintStream}. + * The method called must have no formal parameters. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called. + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pMap the {@code java.util.Map} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each mapped object. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Map} + */ + public static void printDebug(final Map pMap, final String pMethodName, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pMap == null) { + pPrintStream.println(MAP_IS_NULL_ERROR_MESSAGE); + return; + } else if (pMap.isEmpty()) { + pPrintStream.println(MAP_IS_EMPTY_ERROR_MESSAGE); + return; + } + Object mKeyObject; + Object mEntryObject; + + for (Iterator i = pMap.keySet().iterator(); i.hasNext(); ) { + mKeyObject = i.next(); + mEntryObject = pMap.get(mKeyObject); + if ((mKeyObject instanceof String) && (mEntryObject instanceof String)) { + pPrintStream.println((String) mKeyObject + ": " + mEntryObject); + } else if ((mKeyObject instanceof String) && (mEntryObject instanceof List)) { + printDebug((List) mEntryObject, pPrintStream); + } else if ((mKeyObject instanceof String) && (mEntryObject instanceof Set)) { + printDebug((Set) mEntryObject, pPrintStream); + } else if (mKeyObject instanceof String) { + if (!StringUtil.isEmpty(pMethodName)) { + try { + Method objectMethod = mEntryObject.getClass().getMethod(pMethodName, null); + Object retVal = objectMethod.invoke(mEntryObject, null); + + if (retVal != null) { + pPrintStream.println((String) mKeyObject + ": " + retVal.toString()); + } else { // Default execution + throw new Exception(); + } + } catch (Exception e) { + + // Default + pPrintStream.println((String) mKeyObject + ": " + mEntryObject.toString()); + } + } else { // Default + pPrintStream.println((String) mKeyObject + ": " + mEntryObject.toString()); + } + } else if ((mKeyObject instanceof Integer) && (mEntryObject instanceof String)) { + pPrintStream.println((Integer) mKeyObject + ": " + mEntryObject); + } else if ((mKeyObject instanceof Integer) && (mEntryObject instanceof List)) { + printDebug((List) mEntryObject, pPrintStream); + } else if ((mKeyObject instanceof String) && (mEntryObject instanceof Set)) { + printDebug((Set) mEntryObject, pPrintStream); + } else if (mKeyObject instanceof Integer) { + if (!StringUtil.isEmpty(pMethodName)) { + try { + Method objectMethod = mEntryObject.getClass().getMethod(pMethodName, null); + Object retVal = objectMethod.invoke(mEntryObject, null); + + if (retVal != null) { + pPrintStream.println((Integer) mKeyObject + ": " + retVal.toString()); + } else { // Default execution + throw new Exception(); + } + } catch (Exception e) { + + // Default + pPrintStream.println((Integer) mKeyObject + ": " + mEntryObject.toString()); + } + } else { // Default + pPrintStream.println((Integer) mKeyObject + ": " + mEntryObject.toString()); + } + } + + // More.. + //else if + } + } + + /** + * Invokes a given method of every object in a {@code java.util.Map} to {@code System.out}. + * The method called must have no formal parameters. + *

    + * If an exception is throwed during the method invocation, the element's {@code toString()} method is called.
    + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pMap the {@code java.util.Map} to be printed. + * @param pMethodName a {@code java.lang.String} holding the name of the method to be invoked on each mapped object. + * @see {@code java.util.Map} + */ + public static void printDebug(final Map pMap, final String pMethodName) { + printDebug(pMap, pMethodName, System.out); + } + + /** + * Prints the content of a {@code java.util.Map} to a {@code java.io.PrintStream}. + *

    + * Not all data types are supported so far. The default is calling an element's {@code toString()} method.
    + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pMap the {@code java.util.Map} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Map} + */ + public static void printDebug(final Map pMap, final PrintStream pPrintStream) { + printDebug(pMap, null, pPrintStream); + } + + /** + * Prints the content of a {@code java.util.Map} to {@code System.out}. + *

    + * Not all data types are supported so far. The default is calling an element's {@code toString()} method.
    + * For bulk data types, recursive invocations and invocations of other methods in this class, are used. + *

    + * @param pMap the {@code java.util.Map} to be printed. + * @see {@code java.util.Map} + */ + public static void printDebug(final Map pMap) { + printDebug(pMap, System.out); + } + + /** + * Prints the content of a {@code java.util.Properties} to a {@code java.io.PrintStream}. + *

    + * @param pProperties the {@code java.util.Properties} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Properties} + */ + public static void printDebug(final Properties pProperties, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pProperties == null) { + pPrintStream.println(PROPERTIES_IS_NULL_ERROR_MESSAGE); + return; + } else if (pProperties.isEmpty()) { + pPrintStream.println(PROPERTIES_IS_EMPTY_ERROR_MESSAGE); + return; + } + for (Enumeration e = pProperties.propertyNames(); e.hasMoreElements(); ) { + String key = (String) e.nextElement(); + + pPrintStream.println(key + ": " + pProperties.getProperty(key)); + } + } + + /** + * Prints the content of a {@code java.util.Properties} to {@code System.out}. + *

    + * @param pProperties the {@code java.util.Properties} to be printed. + * @see {@code java.util.Properties} + */ + public static void printDebug(final Properties pProperties) { + printDebug(pProperties, System.out); + } + + // Timestamp utilities + + /** + * Prints out the calendar time. + *

    + * @param pCalendar the {@code java.util.Calendar} object from which to extract the date information. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Calendar} + */ + public static void printTimestamp(final Calendar pCalendar, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + pPrintStream.println(getTimestamp(pCalendar)); + } + + /** + * Prints out the system time. + *

    + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.GregorianCalendar} + */ + public static void printTimestamp(final PrintStream pPrintStream) { + + GregorianCalendar cal = new GregorianCalendar(); + + printTimestamp(cal, pPrintStream); + } + + /** + * Prints out the system time to {@code System.out}. + */ + public static void printTimestamp() { + printTimestamp(System.out); + } + + /** + * Returns a presentation of the date based on the given milliseconds. + *

    + * @param pMilliseconds The specified number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT. + * @return a presentation of the calendar time. + * @see {@code java.util.Calendar} + */ + public static String getTimestamp(final String pMilliseconds) { + return getTimestamp(Long.parseLong(pMilliseconds)); + } + + /** + * Returns a presentation of the date based on the given milliseconds. + *

    + * @param pMilliseconds The specified number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT. + * @return a presentation of the calendar time. + * @see {@code java.util.Calendar} + */ + public static String getTimestamp(final long pMilliseconds) { + + java.util.Date date = new java.util.Date(pMilliseconds); + java.util.Calendar calendar = new GregorianCalendar(); + + calendar.setTime(date); + return getTimestamp(calendar); + } + + /** + * Returns a presentation of the given calendar's time. + *

    + * @param pCalendar the {@code java.util.Calendar} object from which to extract the date information. + * @return a presentation of the calendar time. + * @see {@code java.util.Calendar} + */ + public static String getTimestamp(final Calendar pCalendar) { + return buildTimestamp(pCalendar); + } + + /** + * @return a presentation of the system time. + */ + public static String getTimestamp() { + + GregorianCalendar cal = new GregorianCalendar(); + + return getTimestamp(cal); + } + + /** + * Builds a presentation of the given calendar's time. This method contains the common timestamp format used in this class. + * @return a presentation of the calendar time. + */ + protected static String buildTimestamp(final Calendar pCalendar) { + + if (pCalendar == null) { + return CALENDAR_IS_NULL_ERROR_MESSAGE; + } + + // The timestamp format + StringBuilder timestamp = new StringBuilder(); + + //timestamp.append(DateUtil.getMonthName(new Integer(pCalendar.get(Calendar.MONTH)).toString(), "0", "us", "MEDIUM", false) + " "); + timestamp.append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(pCalendar.getTime())); + + //timestamp.append(pCalendar.get(Calendar.DAY_OF_MONTH) + " "); + timestamp.append(" "); + timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.HOUR_OF_DAY)).toString(), 2, "0", true) + ":"); + timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.MINUTE)).toString(), 2, "0", true) + ":"); + timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.SECOND)).toString(), 2, "0", true) + ":"); + timestamp.append(StringUtil.pad(new Integer(pCalendar.get(Calendar.MILLISECOND)).toString(), 3, "0", true)); + return timestamp.toString(); + } + + /** + * Builds the time difference between two millisecond representations. + *

    + * This method is to be used with small time intervals between 0 ms up to a couple of minutes. + *

    + * @param pStartTime the start time. + * @param pEndTime the end time. + * @return the time difference in milliseconds. + */ + public static String buildTimeDifference(final long pStartTime, final long pEndTime) { + + //return pEndTime - pStartTime; + StringBuilder retVal = new StringBuilder(); + + // The time difference in milliseconds + long timeDifference = pEndTime - pStartTime; + + if (timeDifference < 1000) { + retVal.append(timeDifference); + retVal.append(" ms"); + } else { + long seconds = timeDifference / 1000; + + timeDifference = timeDifference % 1000; + retVal.append(seconds); + retVal.append("s "); + retVal.append(timeDifference); + retVal.append("ms"); + } + + //return retVal.toString() + " (original timeDifference: " + new String(new Long(pEndTime - pStartTime).toString()) + ")"; + return retVal.toString(); + } + + /** + * Builds the time difference between the given time and present time. + *

    + * This method is to be used with small time intervals between 0 ms up to a couple of minutes. + *

    + * @param pStartTime the start time. + * @return the time difference in milliseconds. + */ + public static String buildTimeDifference(final long pStartTime) { + + long presentTime = System.currentTimeMillis(); + + return buildTimeDifference(pStartTime, presentTime); + } + + /** + * Prints out the difference between two millisecond representations. + * The start time is subtracted from the end time. + *

    + * @param pStartTime the start time. + * @param pEndTime the end time. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printTimeDifference(final long pStartTime, final long pEndTime, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + pPrintStream.println(buildTimeDifference(pStartTime, pEndTime)); + } + + /** + * Prints out the difference between two millisecond representations. + * The start time is subtracted from the end time. + *

    + * @param pStartTime the start time. + * @param pEndTime the end time. + */ + public static void printTimeDifference(final long pStartTime, final long pEndTime) { + printTimeDifference(pStartTime, pEndTime, System.out); + } + + /** + * Prints out the difference between the given time and present time. + * The start time is subtracted from the present time. + *

    + * @param pStartTime the start time. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printTimeDifference(final long pStartTime, final PrintStream pPrintStream) { + printTimeDifference(pStartTime, System.currentTimeMillis(), pPrintStream); + } + + /** + * Prints out the difference between the given time and present time to {@code System.out}. + * The start time is subtracted from the present time. + *

    + * usage: + *

    +   * long startTime = System.currentTimeMillis();
    +   * ...
    +   * com.iml.oslo.eito.util.DebugUtil.printTimeDifference(startTime);
    +   * 
    + *

    + * @param pStartTime the start time. + */ + public static void printTimeDifference(final long pStartTime) { + printTimeDifference(pStartTime, System.out); + } + + /** + * Builds a string representing the difference between two calendar times. + * The first calendar object is subtracted from the second one. + *

    + * This method is to be used with time intervals between 0 ms up to several hours. + *

    + * @param pStartCalendar the first {@code java.util.Calendar}. + * @param pEndCalendar the second {@code java.util.Calendar}. + * @return a string representation of the time difference. + * @see {@code java.util.Calendar} + */ + public static String buildTimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar) { + + if (pStartCalendar == null) { + return CALENDAR_IS_NULL_ERROR_MESSAGE; + } + if (pEndCalendar == null) { + return CALENDAR_IS_NULL_ERROR_MESSAGE; + } + if (pEndCalendar.before(pStartCalendar)) { + return CALENDAR_CAUSATION_ERROR_MESSAGE; + } + int dateDiff = pEndCalendar.get(Calendar.DATE) - pStartCalendar.get(Calendar.DATE); + int hourDiff = pEndCalendar.get(Calendar.HOUR_OF_DAY) - pStartCalendar.get(Calendar.HOUR_OF_DAY); + int minuteDiff = pEndCalendar.get(Calendar.MINUTE) - pStartCalendar.get(Calendar.MINUTE); + int secondDiff = pEndCalendar.get(Calendar.SECOND) - pStartCalendar.get(Calendar.SECOND); + int milliSecondDiff = pEndCalendar.get(Calendar.MILLISECOND) - pStartCalendar.get(Calendar.MILLISECOND); + + if (milliSecondDiff < 0) { + secondDiff--; + milliSecondDiff += 1000; + } + if (secondDiff < 0) { + minuteDiff--; + secondDiff += 60; + } + if (minuteDiff < 0) { + hourDiff--; + minuteDiff += 60; + } + while (dateDiff > 0) { + dateDiff--; + hourDiff += 24; + } + + // Time difference presentation format + StringBuilder buffer = new StringBuilder(); + + if ((hourDiff == 0) && (minuteDiff == 0) && (secondDiff == 0)) { + buffer.append(milliSecondDiff); + buffer.append("ms"); + } else if ((hourDiff == 0) && (minuteDiff == 0)) { + buffer.append(secondDiff); + buffer.append("s "); + buffer.append(milliSecondDiff); + buffer.append("ms"); + } else if (hourDiff == 0) { + buffer.append(minuteDiff); + buffer.append("m "); + buffer.append(secondDiff); + buffer.append(","); + buffer.append(milliSecondDiff); + buffer.append("s"); + } else { + buffer.append(hourDiff); + buffer.append("h "); + buffer.append(minuteDiff); + buffer.append("m "); + buffer.append(secondDiff); + buffer.append(","); + buffer.append(milliSecondDiff); + buffer.append("s"); + } + return buffer.toString(); + } + + /** + * Prints out the difference between to calendar times. + * The first calendar object is subtracted from the second one. + *

    + * @param pStartCalendar the first {@code java.util.Calendar}. + * @param pEndCalendar the second {@code java.util.Calendar}. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Calendar} + */ + public static void printTimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + pPrintStream.println(buildTimeDifference(pStartCalendar, pEndCalendar)); + } + + /** + * Prints out the difference between to calendar times two {@code System.out}. + * The first calendar object is subtracted from the second one. + *

    + * @param pStartCalendar the first {@code java.util.Calendar}. + * @param pEndCalendar the second {@code java.util.Calendar}. + * @see {@code java.util.Calendar} + */ + public static void printTimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar) { + printTimeDifference(pStartCalendar, pEndCalendar, System.out); + } + + /** + * Prints out the difference between the given calendar time and present time. + *

    + * @param pStartCalendar the {@code java.util.Calendar} to compare with present time. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.util.Calendar} + */ + public static void printTimeDifference(final Calendar pStartCalendar, final PrintStream pPrintStream) { + + GregorianCalendar endCalendar = new GregorianCalendar(); + + printTimeDifference(pStartCalendar, endCalendar, pPrintStream); + } + + /** + * Prints out the difference between the given calendar time and present time to {@code System.out}. + *

    + * usage: + *

    +   * GregorianCalendar startTime = new GregorianCalendar();
    +   * ...
    +   * com.iml.oslo.eito.util.DebugUtil.printTimeDifference(startTime);
    +   * 
    + *

    + * @param pStartCalendar the {@code java.util.Calendar} to compare with present time. + * @see {@code java.util.Calendar} + */ + public static void printTimeDifference(final Calendar pStartCalendar) { + + GregorianCalendar endCalendar = new GregorianCalendar(); + + printTimeDifference(pStartCalendar, endCalendar); + } + + /** + * Prints out a {@code com.iml.oslo.eito.util.DebugUtil.TimeDifference} object. + *

    + * @param pTimeDifference the {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} to investigate. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printTimeDifference(final TimeDifference pTimeDifference, final PrintStream pPrintStream) { + printTimeDifference(pTimeDifference.getStartCalendar(), pTimeDifference.getEndCalendar(), pPrintStream); + } + + /** + * Prints out a {@code com.iml.oslo.eito.util.DebugUtil.TimeDifference} object to {@code System.out}. + *

    + * @param pTimeDifference the {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} to investigate. + */ + public static void printTimeDifference(final TimeDifference pTimeDifference) { + printTimeDifference(pTimeDifference.getStartCalendar(), pTimeDifference.getEndCalendar(), System.out); + } + + /** + * A convenience class for embracing two {@code java.util.Calendar} objects. + * The class is used for building a collection of time differences according to the {@code printTimeAverage} method. + */ + public static class TimeDifference { + + Calendar mStartCalendar; + Calendar mEndCalendar; + + /** + * Constructor TimeDifference + * + * + */ + public TimeDifference() {} + + /** + * Constructor TimeDifference + * + * + * @param pStartCalendar + * @param pEndCalendar + * + */ + public TimeDifference(final Calendar pStartCalendar, final Calendar pEndCalendar) { + this.mStartCalendar = pStartCalendar; + this.mEndCalendar = pEndCalendar; + } + + /** + * Method setStartCalendar + * + * + * @param pStartCalendar + * + */ + public void setStartCalendar(Calendar pStartCalendar) { + this.mStartCalendar = pStartCalendar; + } + + /** + * Method getStartCalendar + * + * + * @return + * + */ + public Calendar getStartCalendar() { + return this.mStartCalendar; + } + + /** + * Method setEndCalendar + * + * + * @param pEndCalendar + * + */ + public void setEndCalendar(Calendar pEndCalendar) { + this.mEndCalendar = pEndCalendar; + } + + /** + * Method getEndCalendar + * + * + * @return + * + */ + public Calendar getEndCalendar() { + return this.mEndCalendar; + } + } + + /** + * Prints out the average time difference from a collection of {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} objects. + *

    + * + * @param pTimeDifferences + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + */ + public static void printTimeAverage(final Collection pTimeDifferences, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + if (pTimeDifferences == null) { + pPrintStream.println(TIMEDIFFERENCES_IS_NULL_ERROR_MESSAGE); + return; + } + Object o; + TimeDifference timeDifference; + Calendar startCalendar = null; + Calendar endCalendar = null; + Calendar totalStartCalendar = null; + Calendar totalEndCalendar = null; + long startCalendarMilliSeconds, endCalendarMilliSeconds; + List timeDifferenceList = new Vector(); + Iterator i = pTimeDifferences.iterator(); + + if (i.hasNext()) { + o = i.next(); + if (!(o instanceof TimeDifference)) { + pPrintStream.println(TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE); + return; + } + timeDifference = (TimeDifference) o; + startCalendar = timeDifference.getStartCalendar(); + totalStartCalendar = startCalendar; + endCalendar = timeDifference.getEndCalendar(); + startCalendarMilliSeconds = startCalendar.getTime().getTime(); + endCalendarMilliSeconds = endCalendar.getTime().getTime(); + timeDifferenceList.add(new Long(endCalendarMilliSeconds - startCalendarMilliSeconds)); + } + while (i.hasNext()) { + o = i.next(); + if (!(o instanceof TimeDifference)) { + pPrintStream.println(TIMEDIFFERENCES_WRONG_DATATYPE_ERROR_MESSAGE); + return; + } + timeDifference = (TimeDifference) o; + startCalendar = timeDifference.getStartCalendar(); + endCalendar = timeDifference.getEndCalendar(); + startCalendarMilliSeconds = startCalendar.getTime().getTime(); + endCalendarMilliSeconds = endCalendar.getTime().getTime(); + timeDifferenceList.add(new Long(endCalendarMilliSeconds - startCalendarMilliSeconds)); + } + totalEndCalendar = endCalendar; + int numberOfElements = timeDifferenceList.size(); + long timeDifferenceElement; + long timeDifferenceSum = 0; + + for (Iterator i2 = timeDifferenceList.iterator(); i2.hasNext(); ) { + timeDifferenceElement = ((Long) i2.next()).longValue(); + timeDifferenceSum += timeDifferenceElement; + } + + // Total elapsed time + String totalElapsedTime = buildTimeDifference(totalStartCalendar, totalEndCalendar); + + // Time average presentation format + pPrintStream.println("Average time difference: " + timeDifferenceSum / numberOfElements + "ms (" + numberOfElements + + " elements, total elapsed time: " + totalElapsedTime + ")"); + } + + /** + * Prints out the average time difference from a collection of {@code com.twelvemonkeys.util.DebugUtil.TimeDifference} objects to {@code System.out}. + *

    + * + * @param pTimeDifferences + */ + public static void printTimeAverage(final Collection pTimeDifferences) { + printTimeAverage(pTimeDifferences, System.out); + } + + // Reflective methods + + /** + * Prints the top-wrapped class name of a {@code java.lang.Object} to a {@code java.io.PrintStream}. + *

    + * @param pObject the {@code java.lang.Object} to be printed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.lang.Class} + */ + public static void printClassName(final Object pObject, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + pPrintStream.println(getClassName(pObject)); + } + + /** + * Prints the top-wrapped class name of a {@code java.lang.Object} to {@code System.out}. + *

    + * @param pObject the {@code java.lang.Object} to be printed. + * @see {@code java.lang.Class} + */ + public static void printClassName(final Object pObject) { + printClassName(pObject, System.out); + } + + /** + * Builds the top-wrapped class name of a {@code java.lang.Object}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @return the object's class name. + * @see {@code java.lang.Class} + */ + public static String getClassName(final Object pObject) { + + if (pObject == null) { + return OBJECT_IS_NULL_ERROR_MESSAGE; + } + return pObject.getClass().getName(); + } + + /** + * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to a {@code java.io.PrintStream}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @param pObjectName the name of the object instance, for identification purposes. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.lang.Class} + * @see {@code java.lang.reflect.Modifier} + * @see {@code java.lang.reflect.Field} + * @see {@code java.lang.reflect.Constructor} + * @see {@code java.lang.reflect.Method} + */ + public static void printClassDetails(final Object pObject, final String pObjectName, final PrintStream pPrintStream) { + + if (pPrintStream == null) { + System.err.println(PRINTSTREAM_IS_NULL_ERROR_MESSAGE); + return; + } + pPrintStream.println(getClassDetails(pObject, pObjectName)); + } + + /** + * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to {@code System.out}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @param pObjectName the name of the object instance, for identification purposes. + * @see {@code java.lang.Class} + * @see {@code java.lang.reflect.Modifier} + * @see {@code java.lang.reflect.Field} + * @see {@code java.lang.reflect.Constructor} + * @see {@code java.lang.reflect.Method} + */ + public static void printClassDetails(final Object pObject, final String pObjectName) { + printClassDetails(pObject, pObjectName, System.out); + } + + /** + * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to {@code System.out}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @see {@code java.lang.Class} + * @see {@code java.lang.reflect.Modifier} + * @see {@code java.lang.reflect.Field} + * @see {@code java.lang.reflect.Constructor} + * @see {@code java.lang.reflect.Method} + */ + public static void printClassDetails(final Object pObject) { + printClassDetails(pObject, null, System.out); + } + + /** + * Prints javadoc-like, the top wrapped class fields and methods of a {@code java.lang.Object} to a {@code java.io.PrintStream}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the results. + * @see {@code java.lang.Class} + * @see {@code java.lang.reflect.Modifier} + * @see {@code java.lang.reflect.Field} + * @see {@code java.lang.reflect.Constructor} + * @see {@code java.lang.reflect.Method} + */ + public static void printClassDetails(final Object pObject, final PrintStream pPrintStream) { + printClassDetails(pObject, null, pPrintStream); + } + + /** + * Builds a javadoc-like presentation of the top wrapped class fields and methods of a {@code java.lang.Object}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @return a listing of the object's class details. + * @see {@code java.lang.Class} + * @see {@code java.lang.reflect.Modifier} + * @see {@code java.lang.reflect.Field} + * @see {@code java.lang.reflect.Constructor} + * @see {@code java.lang.reflect.Method} + */ + public static String getClassDetails(final Object pObject) { + return getClassDetails(pObject, null); + } + + /** + * Builds a javadoc-like presentation of the top wrapped class fields and methods of a {@code java.lang.Object}. + *

    + * @param pObject the {@code java.lang.Object} to be analysed. + * @param pObjectName the name of the object instance, for identification purposes. + * @return a listing of the object's class details. + * @see {@code java.lang.Class} + * @see {@code java.lang.reflect.Modifier} + * @see {@code java.lang.reflect.Field} + * @see {@code java.lang.reflect.Constructor} + * @see {@code java.lang.reflect.Method} + */ + public static String getClassDetails(final Object pObject, final String pObjectName) { + + if (pObject == null) { + return OBJECT_IS_NULL_ERROR_MESSAGE; + } + final String endOfLine = System.getProperty("line.separator"); + final String dividerLine = "---------------------------------------------------------"; + Class c = pObject.getClass(); + StringTokenizer tokenizedString; + String str; + String className = new String(); + String superClassName = new String(); + StringBuilder buffer = new StringBuilder(); + + // Heading + buffer.append(endOfLine); + buffer.append("**** class details"); + if (!StringUtil.isEmpty(pObjectName)) { + buffer.append(" for \"" + pObjectName + "\""); + } + buffer.append(" ****"); + buffer.append(endOfLine); + + // Package + Package p = c.getPackage(); + + if (p != null) { + buffer.append(p.getName()); + } + buffer.append(endOfLine); + + // Class or Interface + if (c.isInterface()) { + buffer.append("I n t e r f a c e "); + } else { + buffer.append("C l a s s "); + } + str = c.getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + className = tokenizedString.nextToken().trim(); + } + str = new String(); + char[] charArray = className.toCharArray(); + + for (int i = 0; i < charArray.length; i++) { + str += charArray[i] + " "; + } + buffer.append(str); + buffer.append(endOfLine); + buffer.append(endOfLine); + + // Class Hierarch + List classNameList = new Vector(); + + classNameList.add(c.getName()); + Class superclass = c.getSuperclass(); + + while (superclass != null) { + classNameList.add(superclass.getName()); + superclass = superclass.getSuperclass(); + } + Object[] classNameArray = classNameList.toArray(); + int counter = 0; + + for (int i = classNameArray.length - 1; i >= 0; i--) { + for (int j = 0; j < counter; j++) { + buffer.append(" "); + } + if (counter > 0) { + buffer.append("|"); + buffer.append(endOfLine); + } + for (int j = 0; j < counter; j++) { + buffer.append(" "); + } + if (counter > 0) { + buffer.append("+-"); + } + buffer.append((String) classNameArray[i]); + buffer.append(endOfLine); + counter++; + } + + // Divider + buffer.append(endOfLine); + buffer.append(dividerLine); + buffer.append(endOfLine); + buffer.append(endOfLine); + + // Profile + int classModifier = c.getModifiers(); + + buffer.append(Modifier.toString(classModifier) + " "); + if (c.isInterface()) { + buffer.append("Interface "); + } else { + buffer.append("Class "); + } + buffer.append(className); + buffer.append(endOfLine); + if ((classNameArray != null) && (classNameArray[classNameArray.length - 2] != null)) { + str = (String) classNameArray[classNameArray.length - 2]; + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + superClassName = tokenizedString.nextToken().trim(); + } + buffer.append("extends " + superClassName); + buffer.append(endOfLine); + } + if (!c.isInterface()) { + Class[] interfaces = c.getInterfaces(); + + if ((interfaces != null) && (interfaces.length > 0)) { + buffer.append("implements "); + str = interfaces[0].getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(str); + for (int i = 1; i < interfaces.length; i++) { + str = interfaces[i].getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(", " + str); + } + buffer.append(endOfLine); + } + } + + // Divider + buffer.append(endOfLine); + buffer.append(dividerLine); + buffer.append(endOfLine); + buffer.append(endOfLine); + + // Fields + buffer.append("F I E L D S U M M A R Y"); + buffer.append(endOfLine); + Field[] fields = c.getFields(); + + if (fields != null) { + for (int i = 0; i < fields.length; i++) { + buffer.append(Modifier.toString(fields[i].getType().getModifiers()) + " "); + str = fields[i].getType().getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(str + " "); + buffer.append(fields[i].getName()); + buffer.append(endOfLine); + } + } + buffer.append(endOfLine); + + // Constructors + buffer.append("C O N S T R U C T O R S U M M A R Y"); + buffer.append(endOfLine); + Constructor[] constructors = c.getConstructors(); + + if (constructors != null) { + for (int i = 0; i < constructors.length; i++) { + buffer.append(className + "("); + Class[] parameterTypes = constructors[i].getParameterTypes(); + + if (parameterTypes != null) { + if (parameterTypes.length > 0) { + str = parameterTypes[0].getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(str); + for (int j = 1; j < parameterTypes.length; j++) { + str = parameterTypes[j].getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(", " + str); + } + } + } + buffer.append(")"); + buffer.append(endOfLine); + } + } + buffer.append(endOfLine); + + // Methods + buffer.append("M E T H O D S U M M A R Y"); + buffer.append(endOfLine); + Method[] methods = c.getMethods(); + + if (methods != null) { + for (int i = 0; i < methods.length; i++) { + buffer.append(Modifier.toString(methods[i].getModifiers()) + " "); + str = methods[i].getReturnType().getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(str + " "); + buffer.append(methods[i].getName() + "("); + Class[] parameterTypes = methods[i].getParameterTypes(); + + if ((parameterTypes != null) && (parameterTypes.length > 0)) { + if (parameterTypes[0] != null) { + str = parameterTypes[0].getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + + // array bugfix + if (str.charAt(str.length() - 1) == ';') { + str = str.substring(0, str.length() - 1) + "[]"; + } + buffer.append(str); + for (int j = 1; j < parameterTypes.length; j++) { + str = parameterTypes[j].getName(); + tokenizedString = new StringTokenizer(str, "."); + while (tokenizedString.hasMoreTokens()) { + str = tokenizedString.nextToken().trim(); + } + buffer.append(", " + str); + } + } + } + buffer.append(")"); + buffer.append(endOfLine); + } + } + buffer.append(endOfLine); + + // Ending + buffer.append("**** class details"); + if (!StringUtil.isEmpty(pObjectName)) { + buffer.append(" for \"" + pObjectName + "\""); + } + buffer.append(" end ****"); + buffer.append(endOfLine); + return buffer.toString(); + } + + /** + * Prettyprints a large number. + *

    + * + * @param pBigNumber + * @return prettyprinted number with dot-separation each 10e3. + */ + public static String getLargeNumber(final long pBigNumber) { + + StringBuilder buffer = new StringBuilder(new Long(pBigNumber).toString()); + char[] number = new Long(pBigNumber).toString().toCharArray(); + int reverseIndex = 0; + + for (int i = number.length; i >= 0; i--) { + reverseIndex++; + if ((reverseIndex % 3 == 0) && (i > 1)) { + buffer = buffer.insert(i - 1, '.'); + } + } + return buffer.toString(); + } + + /** + * Prettyprints milliseconds to ?day(s) ?h ?m ?s ?ms. + *

    + * + * @param pMilliseconds + * @return prettyprinted time duration. + */ + public static String getTimeInterval(final long pMilliseconds) { + + long timeIntervalMilliseconds = pMilliseconds; + long timeIntervalSeconds = 0; + long timeIntervalMinutes = 0; + long timeIntervalHours = 0; + long timeIntervalDays = 0; + boolean printMilliseconds = true; + boolean printSeconds = false; + boolean printMinutes = false; + boolean printHours = false; + boolean printDays = false; + final long MILLISECONDS_IN_SECOND = 1000; + final long MILLISECONDS_IN_MINUTE = 60 * MILLISECONDS_IN_SECOND; // 60000 + final long MILLISECONDS_IN_HOUR = 60 * MILLISECONDS_IN_MINUTE; // 3600000 + final long MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; // 86400000 + StringBuilder timeIntervalBuffer = new StringBuilder(); + + // Days + if (timeIntervalMilliseconds >= MILLISECONDS_IN_DAY) { + timeIntervalDays = timeIntervalMilliseconds / MILLISECONDS_IN_DAY; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_DAY; + printDays = true; + printHours = true; + printMinutes = true; + printSeconds = true; + } + + // Hours + if (timeIntervalMilliseconds >= MILLISECONDS_IN_HOUR) { + timeIntervalHours = timeIntervalMilliseconds / MILLISECONDS_IN_HOUR; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_HOUR; + printHours = true; + printMinutes = true; + printSeconds = true; + } + + // Minutes + if (timeIntervalMilliseconds >= MILLISECONDS_IN_MINUTE) { + timeIntervalMinutes = timeIntervalMilliseconds / MILLISECONDS_IN_MINUTE; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_MINUTE; + printMinutes = true; + printSeconds = true; + } + + // Seconds + if (timeIntervalMilliseconds >= MILLISECONDS_IN_SECOND) { + timeIntervalSeconds = timeIntervalMilliseconds / MILLISECONDS_IN_SECOND; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_SECOND; + printSeconds = true; + } + + // Prettyprint + if (printDays) { + timeIntervalBuffer.append(timeIntervalDays); + if (timeIntervalDays > 1) { + timeIntervalBuffer.append("days "); + } else { + timeIntervalBuffer.append("day "); + } + } + if (printHours) { + timeIntervalBuffer.append(timeIntervalHours); + timeIntervalBuffer.append("h "); + } + if (printMinutes) { + timeIntervalBuffer.append(timeIntervalMinutes); + timeIntervalBuffer.append("m "); + } + if (printSeconds) { + timeIntervalBuffer.append(timeIntervalSeconds); + timeIntervalBuffer.append("s "); + } + if (printMilliseconds) { + timeIntervalBuffer.append(timeIntervalMilliseconds); + timeIntervalBuffer.append("ms"); + } + return timeIntervalBuffer.toString(); + } +} + + +/*--- Formatted in Sun Java Convention Style on ma, des 1, '03 ---*/ + + +/*------ Formatted by Jindent 3.23 Basic 1.0 --- http://www.jindent.de ------*/ diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/FileResource.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/FileResource.java similarity index 96% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/FileResource.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/FileResource.java index bc2c93c8..388eaf7a 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/FileResource.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/FileResource.java @@ -55,7 +55,7 @@ final class FileResource extends AbstractResource { } private File getFile() { - return (File) mWrappedResource; + return (File) wrappedResource; } public URL asURL() { diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/FloatKey.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/FloatKey.java similarity index 100% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/FloatKey.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/FloatKey.java diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/IntegerKey.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/IntegerKey.java similarity index 100% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/IntegerKey.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/IntegerKey.java diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/MappedBeanFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/MappedBeanFactory.java new file mode 100755 index 00000000..49acf390 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/MappedBeanFactory.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2009, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * MappedBeanFactory + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/main/java/com/twelvemonkeys/sandbox/MappedBeanFactory.java#1 $ + */ +public final class MappedBeanFactory { + + // TODO: Map getMap(Object pProxy) + + // TODO: Consider a @NotNull annotation that will allow for throwing IllegalArgumentExceptions + // - Or a more general validator approach for custom fields... + + // NOTE: Specifying default values does not make much sense, as it would be possible to just add values to the map + // in the first place + + // TODO: Replace Converter varargs with new class a PropertyConverterConfiguration + // - setPropertyConverter(String propertyName, Converter from, Converter to) + // - setDefaultConverter(Class from, Class to, Converter) + // TODO: Validators? Allows for more than just NotNull checking + // TODO: Mixin support for other methods, and we are on the way to full-blown AOP.. ;-) + // TODO: Delegate for behaviour? + + // TODO: Consider being fail-fast for primitives without default values? + // Or have default values be the same as they would have been if class members (false/0/null) + // NOTE: There's a difference between a map with a null value for a key, and no presence of that key at all + + // TODO: ProperyChange support! + + private MappedBeanFactory() { + } + + static T as(final Class pClass, final Converter... pConverters) { + // TODO: Add neccessary default initializer stuff here. + return as(pClass, new LinkedHashMap(), pConverters); + } + + @SuppressWarnings({"unchecked"}) + static T as(final Class pClass, final Map pMap, final Converter... pConverters) { + return asImpl(pClass, (Map) pMap, pConverters); + } + + static T asImpl(final Class pClass, final Map pMap, final Converter[] pConverters) { + // TODO: Report clashing? Named converters? + final Map converters = new HashMap() { + @Override + public Converter get(Object key) { + Converter converter = super.get(key); + return converter != null ? converter : Converter.NULL; + } + }; + + for (Converter converter : pConverters) { + converters.put(new ConverterKey(converter.getFromType(), converter.getToType()), converter); + } + + return pClass.cast( + Proxy.newProxyInstance( + pClass.getClassLoader(), + new Class[]{pClass, Serializable.class}, // TODO: Maybe Serializable should be specified by pClass? + new MappedBeanInvocationHandler(pClass, pMap, converters) + ) + ); + } + + private static class ConverterKey { + private Class to; + private Class from; + + ConverterKey(Class pFrom, Class pTo) { + to = pTo; + from = pFrom; + } + + @Override + public boolean equals(Object pOther) { + if (this == pOther) { + return true; + } + if (pOther == null || getClass() != pOther.getClass()) { + return false; + } + + ConverterKey that = (ConverterKey) pOther; + + return from == that.from && to == that.to; + } + + @Override + public int hashCode() { + int result = to != null ? to.hashCode() : 0; + result = 31 * result + (from != null ? from.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return String.format("%s->%s", from, to); + } + } + + public static interface Converter { + + Converter NULL = new Converter() { + public Class getFromType() { + return null; + } + + public Class getToType() { + return null; + } + + public Object convert(Object value, Object old) { + if (value == null) { + return value; + } + throw new ClassCastException(value.getClass().getName()); + } + }; + + Class getFromType(); + + Class getToType(); + + T convert(F value, T old); + } + + // Add guards for null values by throwing IllegalArgumentExceptions for parameters + // TODO: Throw IllegalArgumentException at CREATION time, if value in map is null for a method with @NotNull return type + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) + static @interface NotNull { + } + + // For setter methods to have automatic property change support + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + // TODO: Consider field as well? + static @interface Observable { + } + + // TODO: Decide on default value annotation + // Alternate default value annotation + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultValue { + boolean booleanValue() default false; + byte byteValue() default 0; + char charValue() default 0; + short shortValue() default 0; + int intValue() default 0; + float floatValue() default 0f; + long longValue() default 0l; + double doubleValue() default 0d; + } + + + // Default values for primitive types + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultBooleanValue { + boolean value() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultByteValue { + byte value() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultCharValue { + char value() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultShortValue { + short value() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultIntValue { + int value() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultFloatValue { + float value() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultLongValue { + long value() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + static @interface DefaultDouleValue { + double value() default 0; + } + + // TODO: Does it make sense to NOT just put the value in the map? + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + private static @interface DefaultStringValue { + String value(); // TODO: Do we want a default empty string? + } + + private static class MappedBeanInvocationHandler implements InvocationHandler, Serializable { + + private static final Method OBJECT_TO_STRING = getObjectMethod("toString"); + private static final Method OBJECT_HASH_CODE = getObjectMethod("hashCode"); + private static final Method OBJECT_EQUALS = getObjectMethod("equals", Object.class); + private static final Method OBJECT_CLONE = getObjectMethod("clone"); + + private final Class mClass; + private final Map mMap; + private final Map mConverters; + + private transient Map mReadMethods = new HashMap(); + private transient Map mWriteMethods = new HashMap(); + + private static Method getObjectMethod(final String pMethodName, final Class... pParams) { + try { + return Object.class.getDeclaredMethod(pMethodName, pParams); + } + catch (NoSuchMethodException e) { + throw new Error(e.getMessage(), e); + } + } + + private Object readResolve() throws ObjectStreamException { + mReadMethods = new HashMap(); + mWriteMethods = new HashMap(); + + introspectBean(mClass, mReadMethods, mWriteMethods); + + return this; + } + + public MappedBeanInvocationHandler(Class pClass, Map pMap, Map pConverters) { + mClass = pClass; + mMap = pMap; + mConverters = pConverters; + + introspectBean(mClass, mReadMethods, mWriteMethods); + } + + private void introspectBean(Class pClass, Map pReadMethods, Map pWriteMethods) { + try { + BeanInfo info = Introspector.getBeanInfo(pClass); + PropertyDescriptor[] descriptors = info.getPropertyDescriptors(); + for (PropertyDescriptor descriptor : descriptors) { + String name = descriptor.getName(); + + Method read = descriptor.getReadMethod(); + if (read != null) { + pReadMethods.put(read, name); + } + + Method write = descriptor.getWriteMethod(); + if (write != null) { + pWriteMethods.put(write, name); + } + } + } + catch (IntrospectionException e) { + throw new IllegalArgumentException(String.format("Class %s not introspectable: %s", pClass, e.getMessage()) , e); + } + } + + public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArguments) throws Throwable { + String property = mReadMethods.get(pMethod); + if (property != null) { + if (pArguments == null || pArguments.length == 0) { + Object value = mMap.get(property); + Class type = pMethod.getReturnType(); + + if (!isCompatible(value, type)) { + return mConverters.get(new ConverterKey(value != null ? value.getClass() : Void.class, unBoxType(type))).convert(value, null); + } + return value; + } + else { + throw new IllegalArgumentException("Unknown parameters for " + pMethod + ": " + Arrays.toString(pArguments)); + } + } + + property = mWriteMethods.get(pMethod); + if (property != null) { + if (pArguments.length == 1) { + Object value = pArguments[0]; + + // Make sure we don't accidentally overwrite a value that looks like ours... + Object oldValue = mMap.get(property); + Class type = pMethod.getParameterTypes()[0]; + if (oldValue != null && !isCompatible(oldValue, type)) { + value = mConverters.get(new ConverterKey(type, oldValue.getClass())).convert(value, oldValue); + } + return mMap.put(property, value); + } + else { + throw new IllegalArgumentException("Unknown parameters for " + pMethod + ": " + Arrays.toString(pArguments)); + } + } + + if (pMethod.equals(OBJECT_TO_STRING)) { + return proxyToString(); + } + if (pMethod.equals(OBJECT_EQUALS)) { + return proxyEquals(pProxy, pArguments[0]); + } + if (pMethod.equals(OBJECT_HASH_CODE)) { + return proxyHashCode(); + } + if (pMethod.getName().equals(OBJECT_CLONE.getName()) + && Arrays.equals(pMethod.getParameterTypes(), OBJECT_CLONE.getParameterTypes()) + && OBJECT_CLONE.getReturnType().isAssignableFrom(pMethod.getReturnType())) { + return proxyClone(); + } + + // Other methods not handled (for now) + throw new AbstractMethodError(pMethod.getName()); + } + + private boolean isCompatible(final Object pValue, final Class pType) { + return pValue == null && !pType.isPrimitive() || unBoxType(pType).isInstance(pValue); + } + + private Class unBoxType(final Class pType) { + if (pType.isPrimitive()) { + if (pType == boolean.class) { + return Boolean.class; + } + if (pType == byte.class) { + return Byte.class; + } + if (pType == char.class) { + return Character.class; + } + if (pType == short.class) { + return Short.class; + } + if (pType == int.class) { + return Integer.class; + } + if (pType == float.class) { + return Float.class; + } + if (pType == long.class) { + return Long.class; + } + if (pType == double.class) { + return Double.class; + } + + throw new IllegalArgumentException("Unknown type: " + pType); + } + return pType; + } + + private int proxyHashCode() { + // NOTE: Implies mMap instance must follow Map.equals contract + return mMap.hashCode(); + } + + private boolean proxyEquals(final Object pThisProxy, final Object pThat) { + if (pThisProxy == pThat) { + return true; + } + if (pThat == null) { + return false; + } + + // TODO: Document that subclasses are considered equal (if no extra properties) + if (!mClass.isInstance(pThat)) { + return false; + } + if (!Proxy.isProxyClass(pThat.getClass())) { + return false; + } + + // NOTE: This implies that we should put default values in map at creation time + // NOTE: Implies mMap instance must follow Map.equals contract + InvocationHandler handler = Proxy.getInvocationHandler(pThat); + return handler.getClass() == getClass() && mMap.equals(((MappedBeanInvocationHandler) handler).mMap); + + } + + private Object proxyClone() throws CloneNotSupportedException { + return as( + mClass, + new LinkedHashMap(mMap), + mConverters.values().toArray(new Converter[mConverters.values().size()]) + ); + } + + private String proxyToString() { + return String.format("%s$MapProxy@%s: %s", mClass.getName(), System.identityHashCode(this), mMap); + } + } +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/PaintKey.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PaintKey.java similarity index 100% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/PaintKey.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PaintKey.java diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java new file mode 100755 index 00000000..c35ece68 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/PersistentMap.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2009, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS 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 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, 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. + */ + +package com.twelvemonkeys.util; + +/** + * PersistentMap + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$ + */ +public class PersistentMap { + // TODO: Implement Map + // TODO: Delta synchronization (db?) +} + +/* +Persistent format + +Header + File ID 4-8 bytes + Size + + Entry pointer array block + Size + Next entry pointer block address + Entry 1 address + ... + Entry n address + + Entry 1 + ... + Entry n + +*/ \ No newline at end of file diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java similarity index 100% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/Resource.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/Resource.java similarity index 91% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/Resource.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/Resource.java index 5fec4a5b..18f04563 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/Resource.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/Resource.java @@ -45,7 +45,7 @@ public interface Resource { * * The id might be a {@code URL}, a {@code File} or a string * representation of some system resource. - * It will always be equal to the {@code reourceId} parameter + * It will always be equal to the {@code resourceId} parameter * sent to the {@link ResourceMonitor#addResourceChangeListener} method * for a given resource. * @@ -61,9 +61,9 @@ public interface Resource { public URL asURL(); /** - * Opens an input stream, that reads from this reource. + * Opens an input stream, that reads from this resource. * - * @return an intput stream, that reads frmo this resource. + * @return an input stream, that reads from this resource. * * @throws IOException if an I/O exception occurs */ diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java similarity index 91% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java index f510ca61..17208c81 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java @@ -41,8 +41,8 @@ import java.util.EventListener; public interface ResourceChangeListener extends EventListener { /** * Invoked when a resource is changed. - * Implementations that listens for multiple eventes, should check that - * {@code Resource.getId()} is equal to the {@code reourceId} parameter + * Implementations that listens for multiple events, should check that + * {@code Resource.getId()} is equal to the {@code resourceId} parameter * sent to the {@link ResourceMonitor#addResourceChangeListener} method. * * @param pResource changed file/url/etc. diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java similarity index 83% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java index dd37cef4..c594de70 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java @@ -53,9 +53,9 @@ public abstract class ResourceMonitor { private static final ResourceMonitor INSTANCE = new ResourceMonitor() {}; - private Timer mTimer; + private Timer timer; - private Map mTimerEntries; + private final Map timerEntries; public static ResourceMonitor getInstance() { return INSTANCE; @@ -66,8 +66,8 @@ public abstract class ResourceMonitor { */ protected ResourceMonitor() { // Create timer, run timer thread as daemon... - mTimer = new Timer(true); - mTimerEntries = new HashMap(); + timer = new Timer(true); + timerEntries = new HashMap(); } /** @@ -95,12 +95,12 @@ public abstract class ResourceMonitor { Object resourceId = getResourceId(pResourceId, pListener); // Remove the old task for this Id, if any, and register the new one - synchronized (mTimerEntries) { + synchronized (timerEntries) { removeListenerInternal(resourceId); - mTimerEntries.put(resourceId, task); + timerEntries.put(resourceId, task); } - mTimer.schedule(task, pPeriod, pPeriod); + timer.schedule(task, pPeriod, pPeriod); } /** @@ -110,13 +110,13 @@ public abstract class ResourceMonitor { * @param pResourceId name of the resource to monitor. */ public void removeResourceChangeListener(ResourceChangeListener pListener, Object pResourceId) { - synchronized (mTimerEntries) { + synchronized (timerEntries) { removeListenerInternal(getResourceId(pResourceId, pListener)); } } private void removeListenerInternal(Object pResourceId) { - ResourceMonitorTask task = (ResourceMonitorTask) mTimerEntries.remove(pResourceId); + ResourceMonitorTask task = timerEntries.remove(pResourceId); if (task != null) { task.cancel(); @@ -135,13 +135,13 @@ public abstract class ResourceMonitor { * */ private class ResourceMonitorTask extends TimerTask { - ResourceChangeListener mListener; - Resource mMonitoredResource; - long mLastModified; + ResourceChangeListener listener; + Resource monitoredResource; + long lastModified; public ResourceMonitorTask(ResourceChangeListener pListener, Object pResourceId) throws IOException { - mListener = pListener; - mLastModified = 0; + listener = pListener; + lastModified = 0; String resourceId = null; File file = null; @@ -159,13 +159,13 @@ public abstract class ResourceMonitor { } else if (pResourceId instanceof String) { resourceId = (String) pResourceId; // This one might be looked up - file = new File((String) resourceId); + file = new File(resourceId); } if (file != null && file.exists()) { // Easy, this is a file - mMonitoredResource = new FileResource(pResourceId, file); - //System.out.println("File: " + mMonitoredResource); + monitoredResource = new FileResource(pResourceId, file); + //System.out.println("File: " + monitoredResource); } else { // No file there, but is it on CLASSPATH? @@ -179,28 +179,28 @@ public abstract class ResourceMonitor { if (url != null && "file".equals(url.getProtocol()) && (file = new File(url.getFile())).exists()) { // It's a file in classpath, so try this as an optimization - mMonitoredResource = new FileResource(pResourceId, file); - //System.out.println("File: " + mMonitoredResource); + monitoredResource = new FileResource(pResourceId, file); + //System.out.println("File: " + monitoredResource); } else if (url != null) { // No, not a file, might even be an external resource - mMonitoredResource = new URLResource(pResourceId, url); - //System.out.println("URL: " + mMonitoredResource); + monitoredResource = new URLResource(pResourceId, url); + //System.out.println("URL: " + monitoredResource); } else { throw new FileNotFoundException(resourceId); } } - mLastModified = mMonitoredResource.lastModified(); + lastModified = monitoredResource.lastModified(); } public void run() { - long lastModified = mMonitoredResource.lastModified(); + long lastModified = monitoredResource.lastModified(); - if (lastModified != mLastModified) { - mLastModified = lastModified; - fireResourceChangeEvent(mListener, mMonitoredResource); + if (lastModified != this.lastModified) { + this.lastModified = lastModified; + fireResourceChangeEvent(listener, monitoredResource); } } } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/StringKey.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/StringKey.java similarity index 100% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/StringKey.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/StringKey.java diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/TypedMap.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/TypedMap.java similarity index 92% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/TypedMap.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/TypedMap.java index 6caf52e6..995e12d6 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/TypedMap.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/TypedMap.java @@ -49,14 +49,14 @@ public class TypedMap implements Map, Serializa /** * The wrapped map */ - protected Map mEntries; + protected Map entries; /** * Creates a {@code TypedMap}. * This {@code TypedMap} will be backed by a new {@code HashMap} instance. */ public TypedMap() { - mEntries = new HashMap(); + entries = new HashMap(); } /** @@ -104,14 +104,14 @@ public class TypedMap implements Map, Serializa // This is safe, as we re-insert all values later //noinspection unchecked - mEntries = (Map) pBacking; + entries = (Map) pBacking; // Re-insert all elements to avoid undeterministic ClassCastExceptions if (pUseElements) { putAll(pBacking); } - else if (mEntries.size() > 0) { - mEntries.clear(); + else if (entries.size() > 0) { + entries.clear(); } } @@ -123,7 +123,7 @@ public class TypedMap implements Map, Serializa * @return the number of key-value mappings in this map. */ public int size() { - return mEntries.size(); + return entries.size(); } /** @@ -132,7 +132,7 @@ public class TypedMap implements Map, Serializa * @return {@code true} if this map contains no key-value mappings. */ public boolean isEmpty() { - return mEntries.isEmpty(); + return entries.isEmpty(); } /** @@ -144,7 +144,7 @@ public class TypedMap implements Map, Serializa * key. */ public boolean containsKey(Object pKey) { - return mEntries.containsKey(pKey); + return entries.containsKey(pKey); } /** @@ -160,7 +160,7 @@ public class TypedMap implements Map, Serializa * specified value. */ public boolean containsValue(Object pValue) { - return mEntries.containsValue(pValue); + return entries.containsValue(pValue); } /** @@ -177,7 +177,7 @@ public class TypedMap implements Map, Serializa * @see #containsKey(java.lang.Object) */ public V get(Object pKey) { - return mEntries.get(pKey); + return entries.get(pKey); } /** @@ -203,7 +203,7 @@ public class TypedMap implements Map, Serializa if (!pKey.isCompatibleValue(pValue)) { throw new IllegalArgumentException("incompatible value for key"); } - return mEntries.put(pKey, pValue); + return entries.put(pKey, pValue); } /** @@ -218,7 +218,7 @@ public class TypedMap implements Map, Serializa * {@code null} values. */ public V remove(Object pKey) { - return mEntries.remove(pKey); + return entries.remove(pKey); } /** @@ -241,19 +241,19 @@ public class TypedMap implements Map, Serializa * Removes all mappings from this map (optional operation). */ public void clear() { - mEntries.clear(); + entries.clear(); } public Collection values() { - return mEntries.values(); + return entries.values(); } public Set> entrySet() { - return mEntries.entrySet(); + return entries.entrySet(); } public Set keySet() { - return mEntries.keySet(); + return entries.keySet(); } /** diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/URLResource.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/URLResource.java similarity index 91% rename from common/common-lang/src/main/java/com/twelvemonkeys/util/URLResource.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/URLResource.java index 890df112..3e0cb63a 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/URLResource.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/URLResource.java @@ -45,7 +45,7 @@ final class URLResource extends AbstractResource { // NOTE: For the time being, we rely on the URL class (and helpers) to do // some smart caching and reuse of connections... // TODO: Change the implementation if this is a problem - private long mLastModified = -1; + private long lastModified = -1; /** * Creates a {@code URLResource}. @@ -58,7 +58,7 @@ final class URLResource extends AbstractResource { } private URL getURL() { - return (URL) mWrappedResource; + return (URL) wrappedResource; } public URL asURL() { @@ -77,13 +77,13 @@ final class URLResource extends AbstractResource { URLConnection connection = getURL().openConnection(); connection.setAllowUserInteraction(false); connection.setUseCaches(true); - connection.setIfModifiedSince(mLastModified); + connection.setIfModifiedSince(lastModified); - mLastModified = connection.getLastModified(); + lastModified = connection.getLastModified(); } catch (IOException ignore) { } - return mLastModified; + return lastModified; } } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java new file mode 100644 index 00000000..2b3208fd --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util; + +import com.twelvemonkeys.lang.StringUtil; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.*; + +/** + * A factory for creating {@code UUID}s not directly supported by {@link java.util.UUID java.util.UUID}. + *

    + * This class can create version 1 time based {@code UUID}s, using either IEEE 802 (mac) address or random "node" value + * as well as version 5 SHA1 hash based {@code UUID}s. + *

    + *

    + * The timestamp value for version 1 {@code UUID}s will use a high precision clock, when available to the Java VM. + * If the Java system clock does not offer the needed precision, the timestamps will fall back to 100-nanosecond + * increments, to avoid duplicates. + *

    + *

    + * + * The node value for version 1 {@code UUID}s will, by default, reflect the IEEE 802 (mac) address of one of + * the network interfaces of the local computer. This node value can be manually overridden by setting + * the system property {@code "com.twelvemonkeys.util.UUID.node"} to a valid IEEE 802 address, on the form + * {@code "12:34:56:78:9a:bc"} or {@code "12-34-45-78-9a-bc"}. + *

    + *

    + * + * The node value for the random "node" based version 1 {@code UUID}s will be stable for the lifetime of the VM. + *

    + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: UUIDFactory.java,v 1.0 27.02.12 09:45 haraldk Exp$ + * + * @see RFC 4122 + * @see Wikipedia + * @see java.util.UUID + */ +public final class UUIDFactory { + private static final String NODE_PROPERTY = "com.twelvemonkeys.util.UUID.node"; + + /** + * The Nil UUID: {@code "00000000-0000-0000-0000-000000000000"}. + * + * The nil UUID is special form of UUID that is specified to have all + * 128 bits set to zero. Not particularly useful, unless as a placeholder or template. + * + * @see RFC 4122 4.1.7. Nil UUID + */ + public static final UUID NIL = new UUID(0l, 0l); + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + private static final Comparator COMPARATOR = new UUIDComparator(); + + // Assumes MAC address is constant, which it may not be if a network card is replaced + static final long MAC_ADDRESS_NODE = getMacAddressNode(); + + static final long SECURE_RANDOM_NODE = getSecureRandomNode(); + + private static long getSecureRandomNode() { + // Creates a completely random "node" value, with the unicast bit set to 1, as outlined in RFC 4122. + return 1l << 40 | SECURE_RANDOM.nextLong() & 0xffffffffffffl; + } + + private static long getMacAddressNode() { + long[] addressNodes; + + String nodeProperty = System.getProperty(NODE_PROPERTY); + + // Read mac address/node from system property, to allow user-specified node addresses. + if (!StringUtil.isEmpty(nodeProperty)) { + addressNodes = parseMacAddressNodes(nodeProperty); + } + else { + addressNodes = MacAddressFinder.getMacAddressNodes(); + } + + // TODO: The UUID spec allows us to use multiple nodes, when available, to create more UUIDs per time unit... + // For example in a round robin fashion? + return addressNodes != null && addressNodes.length > 0 ? addressNodes[0] : -1; + } + + static long[] parseMacAddressNodes(final String nodeProperty) { + // Parse comma-separated list mac addresses on format 00:11:22:33:44:55 / 00-11-22-33-44-55 + String[] nodesStrings = nodeProperty.trim().split(",\\W*"); + long[] addressNodes = new long[nodesStrings.length]; + + for (int i = 0, nodesStringsLength = nodesStrings.length; i < nodesStringsLength; i++) { + String nodesString = nodesStrings[i]; + + try { + String[] nodes = nodesString.split("(?<=(^|\\W)[0-9a-fA-F]{2})\\W(?=[0-9a-fA-F]{2}(\\W|$))", 6); + + long nodeAddress = 0; + + // Network byte order + nodeAddress |= (long) (Integer.parseInt(nodes[0], 16) & 0xff) << 40; + nodeAddress |= (long) (Integer.parseInt(nodes[1], 16) & 0xff) << 32; + nodeAddress |= (long) (Integer.parseInt(nodes[2], 16) & 0xff) << 24; + nodeAddress |= (long) (Integer.parseInt(nodes[3], 16) & 0xff) << 16; + nodeAddress |= (long) (Integer.parseInt(nodes[4], 16) & 0xff) << 8; + nodeAddress |= (long) (Integer.parseInt(nodes[5], 16) & 0xff); + + addressNodes[i] = nodeAddress; + } + catch (RuntimeException e) { + // May be NumberformatException from parseInt or ArrayIndexOutOfBounds from nodes array + NumberFormatException formatException = new NumberFormatException(String.format("Bad IEEE 802 node address: '%s' (from system property %s)", nodesString, NODE_PROPERTY)); + formatException.initCause(e); + throw formatException; + } + } + + return addressNodes; + } + + private UUIDFactory() {} + + /** + * Creates a type 5 (name based) {@code UUID} based on the specified byte array. + * This method is effectively identical to {@link UUID#nameUUIDFromBytes}, except that this method + * uses a SHA1 hash instead of the MD5 hash used in the type 3 {@code UUID}s. + * RFC 4122 states that "SHA-1 is preferred" over MD5, without giving a reason for why. + * + * @param name a byte array to be used to construct a {@code UUID} + * @return a {@code UUID} generated from the specified array. + * + * @see RFC 4122 4.3. Algorithm for Creating a Name-Based UUID + * @see RFC 4122 appendix A + * @see UUID#nameUUIDFromBytes(byte[]) + */ + public static UUID nameUUIDv5FromBytes(byte[] name) { + // Based on code from OpenJDK UUID#nameUUIDFromBytes + private byte[] constructor + MessageDigest md; + + try { + md = MessageDigest.getInstance("SHA1"); + } + catch (NoSuchAlgorithmException nsae) { + throw new InternalError("SHA1 not supported"); + } + + byte[] sha1Bytes = md.digest(name); + sha1Bytes[6] &= 0x0f; /* clear version */ + sha1Bytes[6] |= 0x50; /* set to version 5 */ + sha1Bytes[8] &= 0x3f; /* clear variant */ + sha1Bytes[8] |= 0x80; /* set to IETF variant */ + + long msb = 0; + long lsb = 0; + + // NOTE: According to RFC 4122, only first 16 bytes are used, meaning + // bytes 17-20 in the 160 bit SHA1 hash are simply discarded in this case... + for (int i=0; i<8; i++) { + msb = (msb << 8) | (sha1Bytes[i] & 0xff); + } + for (int i=8; i<16; i++) { + lsb = (lsb << 8) | (sha1Bytes[i] & 0xff); + } + + return new UUID(msb, lsb); + } + + /** + * Creates a version 1 time (and node) based {@code UUID}. + * The node part is by default the IEE 802 (mac) address of one of the network cards of the current machine. + * + * @return a {@code UUID} based on the current time and the node address of this computer. + * @see RFC 4122 4.2. Algorithms for Creating a Time-Based UUID + * @see IEEE 802 (mac) address + * @see Overriding the node address + * + * @throws IllegalStateException if the IEEE 802 (mac) address of the computer (node) cannot be found. + */ + public static UUID timeNodeBasedUUID() { + if (MAC_ADDRESS_NODE == -1) { + throw new IllegalStateException("Could not determine IEEE 802 (mac) address for node"); + } + + return createTimeBasedUUIDforNode(MAC_ADDRESS_NODE); + } + + /** + * Creates a version 1 time based {@code UUID} with the node part replaced by a random based value. + * The node part is computed using a 47 bit secure random number + lsb of first octet (unicast/multicast bit) set to 1. + * These {@code UUID}s can never clash with "real" node based version 1 {@code UUID}s due to the difference in + * the unicast/multicast bit, however, no uniqueness between multiple machines/vms/nodes can be guaranteed. + * + * @return a {@code UUID} based on the current time and a random generated "node" value. + * @see RFC 4122 4.5. Node IDs that Do Not Identify the Host + * @see RFC 4122 Appendix A + * @see Lifetime of random node value + * + * @throws IllegalStateException if the IEEE 802 (mac) address of the computer (node) cannot be found. + */ + public static UUID timeRandomBasedUUID() { + return createTimeBasedUUIDforNode(SECURE_RANDOM_NODE); + } + + private static UUID createTimeBasedUUIDforNode(final long node) { + return new UUID(createTimeAndVersion(), createClockSeqAndNode(node)); + } + + // TODO: Version 2 UUID? + /* + Version 2 UUIDs are similar to Version 1 UUIDs, with the upper byte of the clock sequence replaced by the + identifier for a "local domain" (typically either the "POSIX UID domain" or the "POSIX GID domain") + and the first 4 bytes of the timestamp replaced by the user's POSIX UID or GID (with the "local domain" + identifier indicating which it is).[2][3] + */ + + private static long createClockSeqAndNode(final long node) { + // Variant (2) + Clock seq high and low + node + return 0x8000000000000000l | (Clock.getClockSequence() << 48) | node & 0xffffffffffffl; + } + + private static long createTimeAndVersion() { + long clockTime = Clock.currentTimeHundredNanos(); + + long time = clockTime << 32; // Time low + time |= (clockTime & 0xFFFF00000000L) >> 16; // Time mid + time |= ((clockTime >> 48) & 0x0FFF); // Time high + time |= 0x1000; // Version (1) + + return time; + } + + /** + * Returns a comparator that compares UUIDs as 128 bit unsigned entities, as mentioned in RFC 4122. + * This is different than {@link UUID#compareTo(Object)} that compares the UUIDs as signed entities. + * + * @return a comparator that compares UUIDs as 128 bit unsigned entities. + * + * @see java.lang.UUID compareTo() does not do an unsigned compare + * @see RFC 4122 Appendix A + */ + public static Comparator comparator() { + return COMPARATOR; + } + + static final class UUIDComparator implements Comparator { + public int compare(UUID left, UUID right) { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two *UNSIGNED* numbers + if (left.getMostSignificantBits() >>> 32 < right.getMostSignificantBits() >>> 32) { + return -1; + } + else if (left.getMostSignificantBits() >>> 32 > right.getMostSignificantBits() >>> 32) { + return 1; + } + else if ((left.getMostSignificantBits() & 0xffffffffl) < (right.getMostSignificantBits() & 0xffffffffl)) { + return -1; + } + else if ((left.getMostSignificantBits() & 0xffffffffl) > (right.getMostSignificantBits() & 0xffffffffl)) { + return 1; + } + else if (left.getLeastSignificantBits() >>> 32 < right.getLeastSignificantBits() >>> 32) { + return -1; + } + else if (left.getLeastSignificantBits() >>> 32 > right.getLeastSignificantBits() >>> 32) { + return 1; + } + else if ((left.getLeastSignificantBits() & 0xffffffffl) < (right.getLeastSignificantBits() & 0xffffffffl)) { + return -1; + } + else if ((left.getLeastSignificantBits() & 0xffffffffl) > (right.getLeastSignificantBits() & 0xffffffffl)) { + return 1; + } + + return 0; + } + } + + /** + * A high-resolution timer for use in creating version 1 {@code UUID}s. + */ + static final class Clock { + // Java: 0:00, Jan. 1st, 1970 vs UUID: 0:00, Oct 15th, 1582 + private static final long JAVA_EPOCH_OFFSET_HUNDRED_NANOS = 122192928000000000L; + + private static int clockSeq = SECURE_RANDOM.nextInt(); + + private static long initialNanos; + private static long initialTime; + + private static long lastMeasuredTime; + private static long lastTime; + + static { + initClock(); + } + + private static void initClock() { + long millis = System.currentTimeMillis(); + long nanos = System.nanoTime(); + + initialTime = JAVA_EPOCH_OFFSET_HUNDRED_NANOS + millis * 10000 + (nanos / 100) % 10000; + initialNanos = nanos; + } + + public static synchronized long currentTimeHundredNanos() { + // Measure delta since init and compute accurate time + long time; + + while ((time = initialTime + (System.nanoTime() - initialNanos) / 100) < lastMeasuredTime) { + // Reset clock seq (should happen VERY rarely) + initClock(); + clockSeq++; + } + + lastMeasuredTime = time; + + if (time <= lastTime) { + // This typically means the clock isn't accurate enough, use auto-incremented time. + // It is possible that more timestamps than available are requested for + // each time unit in the system clock, but that is extremely unlikely. + // TODO: RFC 4122 says we should wait in the case of too many requests... + time = ++lastTime; + } + else { + lastTime = time; + } + + return time; + } + + public static synchronized long getClockSequence() { + return clockSeq & 0x3fff; + } + } + + /** + * Static inner class for 1.5 compatibility. + */ + static final class MacAddressFinder { + public static long[] getMacAddressNodes() { + List nodeAddresses = new ArrayList(); + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + NetworkInterface nic = interfaces.nextElement(); + + if (!nic.isVirtual()) { + long nodeAddress = 0; + + byte[] hardware = nic.getHardwareAddress(); // 1.6 method + + if (hardware != null && hardware.length == 6 && hardware[1] != (byte) 0xff) { + // Network byte order + nodeAddress |= (long) (hardware[0] & 0xff) << 40; + nodeAddress |= (long) (hardware[1] & 0xff) << 32; + nodeAddress |= (long) (hardware[2] & 0xff) << 24; + nodeAddress |= (long) (hardware[3] & 0xff) << 16; + nodeAddress |= (long) (hardware[4] & 0xff) << 8; + nodeAddress |= (long) (hardware[5] & 0xff); + + nodeAddresses.add(nodeAddress); + } + } + } + } + } + catch (SocketException ex) { + return null; + } + + long[] unwrapped = new long[nodeAddresses.size()]; + for (int i = 0, nodeAddressesSize = nodeAddresses.size(); i < nodeAddressesSize; i++) { + unwrapped[i] = nodeAddresses.get(i); + } + + return unwrapped; + } + } +} diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/util/XMLProperties.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/XMLProperties.java similarity index 97% rename from sandbox/common/src/main/java/com/twelvemonkeys/util/XMLProperties.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/XMLProperties.java index 261790e8..40b464cb 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/util/XMLProperties.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/XMLProperties.java @@ -1,1287 +1,1287 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. - */ - -package com.twelvemonkeys.util; - -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.xml.XMLSerializer; -import org.w3c.dom.*; -import org.xml.sax.*; -import org.xml.sax.helpers.DefaultHandler; -import org.xml.sax.helpers.XMLReaderFactory; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.*; -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * Properties subclass, that reads and writes files in a simple XML format. - * Can be used in-place where ever {@link java.util.Properties} - * is used. The major differences are that it reads - * and writes XML files, instead of ".properties" format files, and has - * support for typed values (where normal Properties only supports Strings). - *

    - * The greatest advantage of the XML format, is that it - * supports hierarchial structures or grouping of properties, in addtition to - * be a more standard way of storing data. The XML format also opens up for - * allowing for more metadata on - * the properties, such as type and the format information, specifying how to - * read and write them. - *

    - * This class relies on SAX for reading and parsing XML, in - * addition, it requires DOM for outputting XML. It is possible - * to configure what (SAX implementing) parser to use, by setting the system - * property {@code org.xml.sax.driver} to your favourite SAX parser. The - * default is the {@code org.apache.xerces.parsers.SAXParser}. - *

    - * XML Format (DTD):
    - *

    - * <!ELEMENT properties (property)*>
    - * <!ELEMENT property (value?, property*)>
    - * <!ATTLIST property
    - *                    name   CDATA #REQUIRED
    - *                    value  CDATA #IMPLIED
    - *                    type   CDATA "String"
    - *                    format CDATA #IMPLIED
    - * >
    - * <!ELEMENT value (PCDATA)>
    - * <!ATTLIST value
    - *                    type   CDATA "String"
    - *                    format CDATA #IMPLIED
    - * >
    - * 
    - * See {@link #SYSTEM_DTD_URI}, {@link #DTD}. - *

    - * XML Format eample:
    - *
    - * <?xml version="1.0" encoding="UTF-8"?>
    - * <!DOCTYPE properties SYSTEM "http://www.twelvemonkeys.com/xml/XMLProperties.dtd">
    - * <!-- A simple XMLProperties example -->
    - * <!-- Sat Jan 05 00:16:55 CET 2002 -->
    - * <properties>
    - *    <property name="one" value="1" type="Integer" />
    - *    <property name="two">
    - *       <property name="a" value="A" />
    - *       <property name="b">
    - *           <value>B is a very long value, that can span several
    - * lines
    - * <![CDATA[<this><doesn't ---> really
    - * matter<
    - * ]]>
    - * as it is escaped using CDATA.</value>
    - *       </property>
    - *       <property name="c" value="C">
    - *          <property name="i" value="I"/>
    - *       </property>
    - *    </property>
    - *    <property name="date" value="16. Mar 2002"
    - *              type="java.util.Date" format="dd. MMM yyyy"/>
    - *    <property name="none" value="" />
    - * </properties>
    - * 
    - * Results in the properties {@code one=1, two.a=A, two.b=B is a very long..., - * two.c=C, two.c.i=I, date=Sat Mar 16 00:00:00 CET 2002 - * } and {@code none=}. Note that there is no property named - * {@code two}. - * - * @see java.util.Properties - * @see #setPropertyValue(String,Object) - * @see #getPropertyValue(String) - * @see #load(InputStream) - * @see #store(OutputStream,String) - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/XMLProperties.java#1 $ - * - */ -// TODO: Consider deleting this code.. Look at Properties XML format. -public class XMLProperties extends Properties { - - /** {@code "UTF-8"} */ - public final static String UTF_8_ENCODING = "UTF-8"; - - /** {@code "xmlns"} */ - public final static String XMLNS = "xmlns"; - - /** {@code "properties"} */ - public final static String PROPERTIES = "properties"; - - /** {@code "property"} */ - public final static String PROPERTY = "property"; - - /** {@code "name"} */ - public final static String PROPERTY_NAME = "name"; - - /** {@code "value"} */ - public final static String PROPERTY_VALUE = "value"; - - /** {@code "type"} */ - public final static String PROPERTY_TYPE = "type"; - - /** {@code "format"} */ - public final static String PROPERTY_FORMAT = "format"; - - /** {@code "String"} ({@link java.lang.String}) */ - public final static String DEFAULT_TYPE = "String"; - - /** {@code "yyyy-MM-dd hh:mm:ss.SSS"} - * ({@link java.sql.Timestamp} format, excpet nanos) */ - public final static String DEFAULT_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss.SSS"; - - /** This is the DTD */ - public final static String DTD = - "\n\n\n\n"; - - /** {@code "http://www.twelvemonkeys.com/xml/XMLProperties.dtd"} */ - public final static String SYSTEM_DTD_URI = "http://www.twelvemonkeys.com/xml/XMLProperties.dtd"; - - /** {@code "http://www.twelvemonkeys.com/xml/XMLProperties"} */ - public final static String NAMESPACE_URI = "http://www.twelvemonkeys.com/xml/XMLProperties"; - - /** {@code "http://xml.org/sax/features/validation"} */ - public final static String SAX_VALIDATION_URI = "http://xml.org/sax/features/validation"; - - /** debug */ - private boolean mValidation = true; - protected Vector mErrors = null; - protected Vector mWarnings = null; - - // protected Hashtable mTypes = new Hashtable(); - protected Hashtable mFormats = new Hashtable(); - protected static DateFormat sDefaultFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT); - - /** - * Creates an empty property list with no default values. - */ - public XMLProperties() {} - - /** - * Creates an empty property list with the specified defaults. - * - * @param pDefaults the defaults. - */ - public XMLProperties(Properties pDefaults) { - - // Sets the protected defaults variable - super(pDefaults); - } - - void addXMLError(SAXParseException pException) { - - if (mErrors == null) { - mErrors = new Vector(); - } - mErrors.add(pException); - } - - /** - * Gets the non-fatal XML errors (SAXParseExceptions) resulting from a - * load. - * - * @return An array of SAXParseExceptions, or null if none occured. - * - * @see XMLProperties.PropertiesHandler - * @see #load(InputStream) - */ - public SAXParseException[] getXMLErrors() { - - if (mErrors == null) { - return null; - } - return (SAXParseException[]) mErrors.toArray(new SAXParseException[mErrors.size()]); - } - - void addXMLWarning(SAXParseException pException) { - - if (mWarnings == null) { - mWarnings = new Vector(); - } - mWarnings.add(pException); - } - - /** - * Gets the XML warnings (SAXParseExceptions) resulting from a load. - * - * @return An array of SAXParseExceptions, or null if none occured. - * - * @see XMLProperties.PropertiesHandler - * @see #load(InputStream) - */ - public SAXParseException[] getXMLWarnings() { - - if (mWarnings == null) { - return null; - } - return (SAXParseException[]) mWarnings.toArray(new SAXParseException[mWarnings.size()]); - } - - /** - * Reads a property list (key and element pairs) from the input stream. The - * stream is assumed to be using the UFT-8 character encoding, and be in - * valid, well-formed XML format. - *

    - * After loading, any errors or warnings from the SAX parser, are available - * as array of SAXParseExceptions from the getXMLErrors and getXMLWarnings - * methods. - * - * @param pInput the input stream to load from. - * - * @exception IOException if an error occurred when reading from the input - * stream. Any SAXExceptions are also wrapped in IOExceptions. - * - * @see Properties#load(InputStream) - * @see #DTD - * @see #SYSTEM_DTD_URI - * @see XMLProperties.PropertiesHandler - * @see #getXMLErrors - * @see #getXMLWarnings - */ - public synchronized void load(InputStream pInput) throws IOException { - // Get parser instance - XMLReader parser; - - // Try to instantiate System default parser - String driver = System.getProperty("org.xml.sax.driver"); - - if (driver == null) { - - // Instantiate the org.apache.xerces.parsers.SAXParser as default - driver = "org.apache.xerces.parsers.SAXParser"; - } - try { - parser = XMLReaderFactory.createXMLReader(driver); - parser.setFeature(SAX_VALIDATION_URI, mValidation); - } catch (SAXNotRecognizedException saxnre) { - - // It should be okay to throw RuntimeExptions, as you will need an - // XML parser - throw new RuntimeException("Error configuring XML parser \"" + driver + "\": " + saxnre.getClass().getName() + ": " - + saxnre.getMessage()); - } catch (SAXException saxe) { - throw new RuntimeException("Error creating XML parser \"" + driver + "\": " + saxe.getClass().getName() + ": " + saxe.getMessage()); - } - - // Register handler - PropertiesHandler handler = new PropertiesHandler(this); - - parser.setContentHandler(handler); - parser.setErrorHandler(handler); - parser.setDTDHandler(handler); - parser.setEntityResolver(handler); - - // Read and parse XML - try { - parser.parse(new InputSource(pInput)); - } catch (SAXParseException saxpe) { - - // Wrap SAXException in IOException to be consistent - throw new IOException("Error parsing XML: " + saxpe.getClass().getName() + ": " + saxpe.getMessage() + " Line: " - + saxpe.getLineNumber() + " Column: " + saxpe.getColumnNumber()); - } catch (SAXException saxe) { - - // Wrap SAXException in IOException to be consistent - // Doesn't realy matter, as the SAXExceptions seems to be pretty - // meaningless themselves... - throw new IOException("Error parsing XML: " + saxe.getClass().getName() + ": " + saxe.getMessage()); - } - } - - /** - * Initializes the value of a property. - * - * @todo move init code to the parser? - * - * @throws ClassNotFoundException if there is no class found for the given - * type - * @throws IllegalArgumentException if the value given, is not parseable - * as the given type - */ - protected Object initPropertyValue(String pValue, String pType, String pFormat) throws ClassNotFoundException { - - // System.out.println("pValue=" + pValue + " pType=" + pType - // + " pFormat=" + pFormat); - // No value to convert - if (pValue == null) { - return null; - } - - // No conversion needed for Strings - if ((pType == null) || pType.equals("String") || pType.equals("java.lang.String")) { - return pValue; - } - Object value; - - if (pType.equals("Date") || pType.equals("java.util.Date")) { - - // Special parser needed - try { - - // Parse date through StringUtil - if (pFormat == null) { - value = StringUtil.toDate(pValue, sDefaultFormat); - } else { - value = StringUtil.toDate(pValue, new SimpleDateFormat(pFormat)); - } - } catch (IllegalArgumentException e) { - - // Not parseable... - throw e; - } - - // Return - return value; - } else if (pType.equals("java.sql.Timestamp")) { - - // Special parser needed - try { - - // Parse timestamp through StringUtil - value = StringUtil.toTimestamp(pValue); - } catch (IllegalArgumentException e) { - - // Not parseable... - throw new RuntimeException(e.getMessage()); - } - - // Return - return value; - } else { - int dot = pType.indexOf("."); - - if (dot < 0) { - pType = "java.lang." + pType; - } - - // Get class - Class cl = Class.forName(pType); - - // Try to create instance from (String) - value = createInstance(cl, pValue); - if (value == null) { - - // createInstance failed for some reason - // Try to invoke the static method valueof(String) - value = invokeStaticMethod(cl, "valueOf", pValue); - - // If the value is still null, well, then I cannot help... - } - } - - // Return - return value; - } - - /** - * Creates an object from the given class' single argument constructor. - * - * @return The object created from the constructor. - * If the constructor could not be invoked for any reason, null is - * returned. - */ - private Object createInstance(Class pClass, Object pParam) { - Object value; - - try { - - // Create param and argument arrays - Class[] param = { pParam.getClass() }; - Object[] arg = { pParam }; - - // Get constructor - Constructor constructor = pClass.getDeclaredConstructor(param); - - // Invoke and create instance - value = constructor.newInstance(arg); - } catch (Exception e) { - return null; - } - return value; - } - - /** - * Creates an object from any given static method, given the parameter - * - * @return The object returned by the static method. - * If the return type of the method is a primitive type, it is wrapped in - * the corresponding wrapper object (int is wrapped in an Integer). - * If the return type of the method is void, null is returned. - * If the method could not be invoked for any reason, null is returned. - */ - private Object invokeStaticMethod(Class pClass, String pMethod, Object pParam) { - Object value = null; - - try { - - // Create param and argument arrays - Class[] param = { pParam.getClass() }; - Object[] arg = { pParam }; - - // Get method - // *** If more than one such method is found in the class, and one - // of these methods has a RETURN TYPE that is more specific than - // any of the others, that method is reflected; otherwise one of - // the methods is chosen ARBITRARILY. - // java/lang/Class.html#getMethod(java.lang.String, java.lang.Class[]) - java.lang.reflect.Method method = pClass.getMethod(pMethod, param); - - // Invoke public static method - if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { - value = method.invoke(null, arg); - } - } catch (Exception e) { - return null; - } - return value; - } - - /** - * Gets the format of a property. This value is used for formatting the - * value before it is stored as xml. - * - * @param pKey a key in this hashtable. - * - * @return the format for the specified key or null if it does not - * have one. - */ - public String getPropertyFormat(String pKey) { - - // Get format - return StringUtil.valueOf(mFormats.get(pKey)); - } - - /** - * Sets the format of a property. This value is used for formatting the - * value before it is stored as xml. - * - * @param pKey a key in this hashtable. - * @param pFormat a string representation of the format. - * - * @return the previous format for the specified key or null if it did not - * have one. - */ - public synchronized String setPropertyFormat(String pKey, String pFormat) { - - // Insert format - return StringUtil.valueOf(mFormats.put(pKey, pFormat)); - } - - /** - * Calls the Hashtable method put. Provided for parallelism with the - * getPropertyValue method. Enforces use of strings for property keys. - * The value returned is the result of the Hashtable call to put. - * - * @param pKey the key to be placed into this property list. - * @param pValue the value corresponding to key. - * - * @return the previous value of the specified key in this property list, - * or null if it did not have one. - * - * @see #getPropertyValue(String) - */ - public synchronized Object setPropertyValue(String pKey, Object pValue) { - - // Insert value - return put(pKey, pValue); - } - - /** - * Searches for the property with the specified key in this property list. - * If the key is not found in this property list, the default property - * list, and its defaults, recursively, are then checked. The method - * returns null if the property is not found. - * - * @param pKey the property key. - * - * @return the value in this property list with the specified key value. - * - * @see #setPropertyValue(String, Object) - * @see #getPropertyValue(String, Object) - * @see Properties#defaults - */ - public synchronized Object getPropertyValue(String pKey) { - return getPropertyValue(pKey, null); - } - - /** - * Searches for the property with the specified key in this property list. - * If the key is not found in this property list, the default property - * list, and its defaults, recursively, are then checked. The method - * returns the default value argument if the property is not found. - * - * @param pKey the property key. - * @param pDefaultValue the default value. - * - * @return the value in this property list with the specified key value. - * - * @see #getPropertyValue(String) - * @see Properties#defaults - */ - public Object getPropertyValue(String pKey, Object pDefaultValue) { - - Object value = super.get(pKey); // super.get() is EXTREMELEY IMPORTANT - - if (value != null) { - return value; - } - if (defaults instanceof XMLProperties) { - return (((XMLProperties) defaults).getPropertyValue(pKey)); - } - return ((defaults != null) ? defaults.getProperty(pKey) : pDefaultValue); - } - - /** - * Overloaded get method, that always returns Strings. - * Due to the way the store and list methods of - * java.util.Properties works (it calls get and casts to String, instead - * of calling getProperty), this methods returns - * StringUtil.valueOf(super.get), to avoid ClassCastExcpetions. - *

    - * If you need the old functionality back, - * getPropertyValue returns super.get directly. - * A cleaner approach would be to override the list and store - * methods, but it's too much work for nothing... - * - * @param pKey a key in this hashtable - * - * @return the value to which the key is mapped in this hashtable, - * converted to a string; null if the key is not mapped to any value in - * this hashtable. - * - * @see #getPropertyValue(String) - * @see Properties#getProperty(String) - * @see Hashtable#get(Object) - * @see StringUtil#valueOf(Object) - */ - public Object get(Object pKey) { - - //System.out.println("get(" + pKey + "): " + super.get(pKey)); - Object value = super.get(pKey); - - // --- - if ((value != null) && (value instanceof Date)) { // Hmm.. This is true for subclasses too... - - // Special threatment of Date - String format = getPropertyFormat(StringUtil.valueOf(pKey)); - - if (format != null) { - value = new SimpleDateFormat(format).format(value); - } else { - value = sDefaultFormat.format(value); - } - return value; - } - - // --- - // Simply return value - return StringUtil.valueOf(value); - } - - /** - * Searches for the property with the specified key in this property list. - * If the key is not found in this property list, the default property list, - * and its defaults, recursively, are then checked. The method returns the - * default value argument if the property is not found. - * - * @param pKey the hashtable key. - * @param pDefaultValue a default value. - * - * @return the value in this property list with the specified key value. - * @see #setProperty - * @see #defaults - */ - public String getProperty(String pKey, String pDefaultValue) { - - // Had to override this because Properties uses super.get()... - String value = (String) get(pKey); // Safe cast, see get(Object) - - if (value != null) { - return value; - } - return ((defaults != null) - ? defaults.getProperty(pKey) - : pDefaultValue); - } - - /** - * Searches for the property with the specified key in this property list. - * If the key is not found in this property list, the default property list, - * and its defaults, recursively, are then checked. The method returns - * {@code null} if the property is not found. - * - * @param pKey the property key. - * @return the value in this property list with the specified key value. - * @see #setProperty - * @see #defaults - */ - public String getProperty(String pKey) { - - // Had to override this because Properties uses super.get()... - return getProperty(pKey, null); - } - - /** - * Writes this property list (key and element pairs) in this - * {@code Properties} - * table to the output stream in a format suitable for loading into a - * Properties table using the load method. This implementation writes - * the list in XML format. The stream is written using the UTF-8 character - * encoding. - * - * @param pOutput the output stream to write to. - * @param pHeader a description of the property list. - * - * @exception IOException if writing this property list to the specified - * output stream throws an IOException. - * - * @see java.util.Properties#store(OutputStream,String) - */ - public synchronized void store(OutputStream pOutput, String pHeader) throws IOException { - storeXML(this, pOutput, pHeader); - } - - /** - * Utility method that stores the property list in normal properties - * format. This method writes the list of Properties (key and element - * pairs) in the given {@code Properties} - * table to the output stream in a format suitable for loading into a - * Properties table using the load method. The stream is written using the - * ISO 8859-1 character encoding. - * - * @param pProperties the Properties table to store - * @param pOutput the output stream to write to. - * @param pHeader a description of the property list. - * - * @exception IOException if writing this property list to the specified - * output stream throws an IOException. - * - * @see java.util.Properties#store(OutputStream,String) - */ - public static void storeProperties(Map pProperties, OutputStream pOutput, String pHeader) throws IOException { - // Create new properties - Properties props = new Properties(); - - // Copy all elements from the pProperties (shallow) - Iterator iterator = pProperties.entrySet().iterator(); - - while (iterator.hasNext()) { - Map.Entry entry = (Map.Entry) iterator.next(); - - props.setProperty((String) entry.getKey(), StringUtil.valueOf(entry.getValue())); - } - - // Store normal properties - props.store(pOutput, pHeader); - } - - /** - * Utility method that stores the property list in XML format. This method - * writes the list of Properties (key and element pairs) in the given - * {@code Properties} - * table to the output stream in a format suitable for loading into a - * XMLProperties table using the load method. Useful for converting - * Properties into XMLProperties. - * The stream is written using the UTF-8 character - * encoding. - * - * @param pProperties the Properties table to store. - * @param pOutput the output stream to write to. - * @param pHeader a description of the property list. - * - * @exception IOException if writing this property list to the specified - * output stream throws an IOException. - * - * @see #store(OutputStream,String) - * @see java.util.Properties#store(OutputStream,String) - * - * @todo Find correct way of setting namespace URI's - * @todo Store type and format information - */ - public static void storeXML(Map pProperties, OutputStream pOutput, String pHeader) throws IOException { - // Build XML tree (Document) and write - // Find the implementation - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - DocumentBuilder builder; - try { - builder = factory.newDocumentBuilder(); - } - catch (ParserConfigurationException e) { - throw (IOException) new IOException(e.getMessage()).initCause(e); - } - DOMImplementation dom = builder.getDOMImplementation(); - - Document document = dom.createDocument(null, PROPERTIES, dom.createDocumentType(PROPERTIES, null, SYSTEM_DTD_URI)); - Element root = document.getDocumentElement(); - - // This is probably not the correct way of setting a default namespace - root.setAttribute(XMLNS, NAMESPACE_URI); - - // Create and insert the normal Properties headers as XML comments - if (pHeader != null) { - document.insertBefore(document.createComment(" " + pHeader + " "), root); - } - document.insertBefore(document.createComment(" " + new Date() + " "), root); - - // Insert elements from the Properties - Iterator iterator = pProperties.entrySet().iterator(); - - while (iterator.hasNext()) { - Map.Entry entry = (Map.Entry) iterator.next(); - String key = (String) entry.getKey(); - Object value = entry.getValue(); - String format = null; - - if (pProperties instanceof XMLProperties) { - format = ((XMLProperties) pProperties).getPropertyFormat(key); - } - insertElement(document, key, value, format); - } - - // Create serializer and output document - //XMLSerializer serializer = new XMLSerializer(pOutput, new OutputFormat(document, UTF_8_ENCODING, true)); - XMLSerializer serializer = new XMLSerializer(pOutput, UTF_8_ENCODING); - - serializer.serialize(document); - } - - /** - * Inserts elements to the given document one by one, and creates all its - * parents if needed. - * - * @param pDocument the document to insert to. - * @param pName the name of the property element. - * @param pValue the value of the property element. - * @param pFormat - * - * @todo I guess this implementation could use some optimisaztion, as - * we do a lot of unneccessary looping. - */ - private static void insertElement(Document pDocument, String pName, Object pValue, String pFormat) { - - // Get names of all elements we need - String[] names = StringUtil.toStringArray(pName, "."); - - // Get value formatted as string - String value = null; - - if (pValue != null) { - // --- - if (pValue instanceof Date) { - - // Special threatment of Date - if (pFormat != null) { - value = new SimpleDateFormat(pFormat).format(pValue); - } - else { - value = sDefaultFormat.format(pValue); - } - } - else { - value = String.valueOf(pValue); - } - - // --- - } - - // Loop through document from root, and insert parents as needed - Element element = pDocument.getDocumentElement(); - - for (int i = 0; i < names.length; i++) { - boolean found = false; - - // Get children - NodeList children = element.getElementsByTagName(PROPERTY); - Element child = null; - - // Search for correct name - for (int j = 0; j < children.getLength(); j++) { - child = (Element) children.item(j); - if (names[i].equals(child.getAttribute(PROPERTY_NAME))) { - // Found - found = true; - element = child; - break; // Next name - } - } - - // Test if the node was not found, otherwise we need to insert - if (!found) { - // Not found - child = pDocument.createElement(PROPERTY); - child.setAttribute(PROPERTY_NAME, names[i]); - - // Insert it - element.appendChild(child); - element = child; - } - - // If it's the destination node, set the value - if ((i + 1) == names.length) { - - // If the value string contains special data, - // use a CDATA block instead of the "value" attribute - if (StringUtil.contains(value, "\n") || StringUtil.contains(value, "\t") || StringUtil.contains(value, "\"") - || StringUtil.contains(value, "&") || StringUtil.contains(value, "<") || StringUtil.contains(value, ">")) { - - // Create value element - Element valueElement = pDocument.createElement(PROPERTY_VALUE); - - // Set type attribute - String className = pValue.getClass().getName(); - - className = StringUtil.replace(className, "java.lang.", ""); - if (!DEFAULT_TYPE.equals(className)) { - valueElement.setAttribute(PROPERTY_TYPE, className); - } - - // Set format attribute - if (pFormat != null) { - valueElement.setAttribute(PROPERTY_FORMAT, pFormat); - } - - // Crate cdata section - CDATASection cdata = pDocument.createCDATASection(value); - - // Append to document tree - valueElement.appendChild(cdata); - child.appendChild(valueElement); - } - else { - // Just set normal attribute value - child.setAttribute(PROPERTY_VALUE, value); - - // Set type attribute - String className = pValue.getClass().getName(); - - className = StringUtil.replace(className, "java.lang.", ""); - if (!DEFAULT_TYPE.equals(className)) { - child.setAttribute(PROPERTY_TYPE, className); - } - - // If format is set, store in attribute - if (pFormat != null) { - child.setAttribute(PROPERTY_FORMAT, pFormat); - } - } - } - } - } - - /** - * Gets all properties in a properties group. - * If no properties exists in the specified group, {@code null} is - * returned. - * - * @param pGroupKey the group key - * - * @return a new Properties continaing all properties in the group. Keys in - * the new Properties wil not contain the group key. - * If no properties exists in the specified group, {@code null} is - * returned. - */ - public Properties getProperties(String pGroupKey) { - // Stupid impl... - XMLProperties props = new XMLProperties(); - String groupKey = pGroupKey; - - if (groupKey.charAt(groupKey.length()) != '.') { - groupKey += "."; - } - - Iterator iterator = entrySet().iterator(); - - while (iterator.hasNext()) { - Map.Entry entry = (Map.Entry) iterator.next(); - String key = (String) entry.getKey(); - - if (key.startsWith(groupKey)) { - String subKey = key.substring(key.indexOf(groupKey)); - - props.setPropertyValue(subKey, entry.getValue()); - } - } - - return ((props.size() > 0) ? props : null); - } - - /** - * Sets the properties in the given properties group. - * Existing properties in the same group, will not be removed, unless they - * are replaced by new values. - * Any existing properties in the same group that was replaced, are - * returned. If no properties are replaced, null is - * returned. - * - * @param pGroupKey the group key - * @param pProperties the properties to set into this group - * - * @return Any existing properties in the same group that was replaced. - * If no properties are replaced, null is - * returned. - */ - public Properties setProperties(String pGroupKey, Properties pProperties) { - XMLProperties old = new XMLProperties(); - String groupKey = pGroupKey; - - if (groupKey.charAt(groupKey.length()) != '.') { - groupKey += "."; - } - Iterator iterator = pProperties.entrySet().iterator(); - - while (iterator.hasNext()) { - Map.Entry entry = (Map.Entry) iterator.next(); - String key = (String) entry.getKey(); - Object obj = setPropertyValue(groupKey + key, entry.getValue()); - - // Return removed entries - if (obj != null) { - old.setPropertyValue(groupKey + key, entry.getValue()); - } - } - return ((old.size() > 0) ? old : null); - } - - /** - * For testing only. - */ - public static void main(String[] pArgs) throws Exception { - // -- Print DTD - System.out.println("DTD: \n" + DTD); - System.out.println("--"); - - // -- Test load - System.out.println("Reading properties from \"" + pArgs[0] + "\"..."); - XMLProperties props = new XMLProperties(); - - props.load(new FileInputStream(new File(pArgs[0]))); - props.list(System.out); - System.out.println("--"); - - // -- Test recursion - String key = "key"; - Object old = props.setProperty(key, "AAA"); - Properties p1 = new XMLProperties(new XMLProperties(props)); - Properties p2 = new Properties(new Properties(props)); - - System.out.println("XMLProperties: " + p1.getProperty(key) + " ==" + " Properties: " + p2.getProperty(key)); - if (old == null) { - props.remove("key"); - } else { - props.put("key", old); // Put old value back, to avoid confusion... - } - System.out.println("--"); - - // -- Test store - //props.store(System.out, "XML Properties file written by XMLProperties."); - File out = new File("copy_of_" + pArgs[0]); - - System.out.println("Writing properties to \"" + out.getName() + "\""); - if (!out.exists()) { - props.store(new FileOutputStream(out), "XML Properties file written by XMLProperties."); - } else { - System.err.println("File \"" + out.getName() + "\" allready exists, cannot write!"); - } - - // -- Test utility methods - // Write normal properties from XMLProperties - out = new File("copy_of_" + pArgs[0].substring(0, pArgs[0].lastIndexOf(".")) + ".properties"); - System.out.println("Writing properties to \"" + out.getName() + "\""); - if (!out.exists()) { - storeProperties(props, new FileOutputStream(out), "Properties file written by XMLProperties."); - } else { - System.err.println("File \"" + out.getName() + "\" allready exists, cannot write!"); - } - System.out.println("--"); - - // -- Test type attribute - System.out.println("getPropertyValue(\"one\"): " + props.getPropertyValue("one") + " class: " - + props.getPropertyValue("one").getClass()); - System.out.println("setPropertyValue(\"now\", " + new Date() + "): " + props.setPropertyValue("now", new Date()) + " class: " - + props.getPropertyValue("now").getClass()); - System.out.println("getPropertyValue(\"date\"): " + props.getPropertyValue("date") + " class: " - + props.getPropertyValue("date").getClass()); - System.out.println("getPropertyValue(\"time\"): " + props.getPropertyValue("time") + " class: " - + props.getPropertyValue("time").getClass()); - } - - /** - * ContentHandler, ErrorHandler and EntityResolver implementation for the - * SAX Parser. - */ - protected class PropertiesHandler extends DefaultHandler { - protected Stack mStack = null; - - /** Stores the characters read so far, from the characters callback */ - protected char[] mReadSoFar = null; - protected boolean mIsValue = false; - protected String mType = null; - protected String mFormat = null; - protected XMLProperties mProperties = null; - protected Locator mLocator = null; - - /** - * Creates a PropertiesHandler for the given XMLProperties. - */ - PropertiesHandler(XMLProperties pProperties) { - mProperties = pProperties; - mStack = new Stack(); - } - - /** - * setDocumentLocator implementation. - */ - public void setDocumentLocator(Locator pLocator) { - - // System.out.println("Setting locator: " + pLocator); - mLocator = pLocator; - } - - /** - * Calls XMLProperties.addXMLError with the given SAXParseException - * as the argument. - */ - public void error(SAXParseException pException) throws SAXParseException { - - //throw pException; - mProperties.addXMLError(pException); - - /* - System.err.println("error: " + pException.getMessage()); - System.err.println("line: " + mLocator.getLineNumber()); - System.err.println("column: " + mLocator.getColumnNumber()); - */ - } - - /** - * Throws the given SAXParseException (and stops the parsing). - */ - public void fatalError(SAXParseException pException) throws SAXParseException { - - throw pException; - - /* - System.err.println("fatal error: " + pException.getMessage()); - System.err.println("line: " + mLocator.getLineNumber()); - System.err.println("column: " + mLocator.getColumnNumber()); - */ - } - - /** - * Calls XMLProperties.addXMLWarning with the given SAXParseException - * as the argument. - */ - public void warning(SAXParseException pException) throws SAXParseException { - - // throw pException; - mProperties.addXMLWarning(pException); - - /* - System.err.println("warning: " + pException.getMessage()); - System.err.println("line: " + mLocator.getLineNumber()); - System.err.println("column: " + mLocator.getColumnNumber()); - */ - } - - /** - * startElement implementation. - */ - public void startElement(String pNamespaceURI, String pLocalName, String pQualifiedName, Attributes pAttributes) throws SAXException { - - /* - - String attributes = ""; - for (int i = 0; i < pAttributes.getLength(); i++) { - attributes += pAttributes.getQName(i) + "=" + pAttributes.getValue(i) + (i < pAttributes.getLength() ? ", " : ""); - } - - System.out.println("startElement: " + pNamespaceURI - + "." + pLocalName - + " (" + pQualifiedName + ") " - + attributes); - */ - if (XMLProperties.PROPERTY.equals(pLocalName)) { - - // Get attibute values - String name = pAttributes.getValue(XMLProperties.PROPERTY_NAME); - String value = pAttributes.getValue(XMLProperties.PROPERTY_VALUE); - String type = pAttributes.getValue(XMLProperties.PROPERTY_TYPE); - String format = pAttributes.getValue(XMLProperties.PROPERTY_FORMAT); - - // Get the full name of the property - if (!mStack.isEmpty()) { - name = (String) mStack.peek() + "." + name; - } - - // Set the property - if (value != null) { - mProperties.setProperty(name, value); - - // Store type & format - if (!XMLProperties.DEFAULT_TYPE.equals(type)) { - mType = type; - mFormat = format; // Might be null (no format) - } - } - - // Push the last name on the stack - mStack.push(name); - } // /PROPERTY - else if (XMLProperties.PROPERTY_VALUE.equals(pLocalName)) { - - // Get attibute values - String name = (String) mStack.peek(); - String type = pAttributes.getValue(XMLProperties.PROPERTY_TYPE); - String format = pAttributes.getValue(XMLProperties.PROPERTY_FORMAT); - - // Store type & format - if (!XMLProperties.DEFAULT_TYPE.equals(type)) { - mType = type; - mFormat = format; - } - mIsValue = true; - } - } - - /** - * endElement implementation. - */ - public void endElement(String pNamespaceURI, String pLocalName, String pQualifiedName) throws SAXException { - - /* - System.out.println("endElement: " + pNamespaceURI - + "." + pLocalName + " (" + pQualifiedName + ")"); - */ - if (XMLProperties.PROPERTY.equals(pLocalName)) { - - // Just remove the last name - String name = (String) mStack.pop(); - - // Init typed values - try { - String prop = mProperties.getProperty(name); - - // Value may be null, if so just skip - if (prop != null) { - Object value = mProperties.initPropertyValue(prop, mType, mFormat); - - // Store format - if ((mFormat != null) &&!XMLProperties.DEFAULT_DATE_FORMAT.equals(mFormat)) { - mProperties.setPropertyFormat(name, mFormat); - } - - //System.out.println("-->" + prop + "-->" + value); - mProperties.setPropertyValue(name, value); - } - - // Clear type & format - mType = null; - mFormat = null; - } catch (Exception e) { - e.printStackTrace(System.err); - throw new SAXException(e); - } - } else if (XMLProperties.PROPERTY_VALUE.equals(pLocalName)) { - if (mStack.isEmpty()) { - - // There can't be any characters here, really - return; - } - - // Get the full name of the property - String name = (String) mStack.peek(); - - // Set the property - String value = new String(mReadSoFar); - - //System.err.println("characters: >" + value+ "<"); - if (!StringUtil.isEmpty(value)) { - - // If there is allready a value, both the value attribute - // and element have been specified, this is an error - if (mProperties.containsKey(name)) { - throw new SAXParseException( - "Value can only be specified either using the \"value\" attribute, OR the \"value\" element, not both.", mLocator); - } - - // Finally, set the property - mProperties.setProperty(name, value); - } - - // Done value processing - mIsValue = false; - } - } - - /** - * characters implementation - */ - public void characters(char[] pChars, int pStart, int pLength) throws SAXException { - // TODO: Use StringBuilder instead? - if (mIsValue) { - // If nothing read so far - if (mReadSoFar == null) { - // Create new array and copy into - mReadSoFar = new char[pLength]; - System.arraycopy(pChars, pStart, mReadSoFar, 0, pLength); - } - else { - // Merge arrays - mReadSoFar = (char[]) CollectionUtil.mergeArrays(mReadSoFar, 0, mReadSoFar.length, pChars, pStart, pLength); - } - } - } - - - /** - * Intercepts the entity - * "http://www.twelvemonkeys.com/xml/XMLProperties.dtd", and return - * an InputSource based on the internal DTD of XMLProperties instead. - * - * @todo Maybe intercept a PUBLIC DTD and be able to have SYSTEM DTD - * override? - */ - public InputSource resolveEntity(String pPublicId, String pSystemId) { - // If we are looking for the standard SYSTEM DTD, then - // Return an InputSource based on the internal DTD. - if (XMLProperties.SYSTEM_DTD_URI.equals(pSystemId)) { - return new InputSource(new StringReader(XMLProperties.DTD)); - } - - // use the default behaviour - return null; - } - } -} - +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.xml.XMLSerializer; +import org.w3c.dom.*; +import org.xml.sax.*; +import org.xml.sax.helpers.DefaultHandler; +import org.xml.sax.helpers.XMLReaderFactory; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Properties subclass, that reads and writes files in a simple XML format. + * Can be used in-place where ever {@link java.util.Properties} + * is used. The major differences are that it reads + * and writes XML files, instead of ".properties" format files, and has + * support for typed values (where normal Properties only supports Strings). + *

    + * The greatest advantage of the XML format, is that it + * supports hierarchial structures or grouping of properties, in addtition to + * be a more standard way of storing data. The XML format also opens up for + * allowing for more metadata on + * the properties, such as type and the format information, specifying how to + * read and write them. + *

    + * This class relies on SAX for reading and parsing XML, in + * addition, it requires DOM for outputting XML. It is possible + * to configure what (SAX implementing) parser to use, by setting the system + * property {@code org.xml.sax.driver} to your favourite SAX parser. The + * default is the {@code org.apache.xerces.parsers.SAXParser}. + *

    + * XML Format (DTD):
    + *

    + * <!ELEMENT properties (property)*>
    + * <!ELEMENT property (value?, property*)>
    + * <!ATTLIST property
    + *                    name   CDATA #REQUIRED
    + *                    value  CDATA #IMPLIED
    + *                    type   CDATA "String"
    + *                    format CDATA #IMPLIED
    + * >
    + * <!ELEMENT value (PCDATA)>
    + * <!ATTLIST value
    + *                    type   CDATA "String"
    + *                    format CDATA #IMPLIED
    + * >
    + * 
    + * See {@link #SYSTEM_DTD_URI}, {@link #DTD}. + *

    + * XML Format eample:
    + *
    + * <?xml version="1.0" encoding="UTF-8"?>
    + * <!DOCTYPE properties SYSTEM "http://www.twelvemonkeys.com/xml/XMLProperties.dtd">
    + * <!-- A simple XMLProperties example -->
    + * <!-- Sat Jan 05 00:16:55 CET 2002 -->
    + * <properties>
    + *    <property name="one" value="1" type="Integer" />
    + *    <property name="two">
    + *       <property name="a" value="A" />
    + *       <property name="b">
    + *           <value>B is a very long value, that can span several
    + * lines
    + * <![CDATA[<this><doesn't ---> really
    + * matter<
    + * ]]>
    + * as it is escaped using CDATA.</value>
    + *       </property>
    + *       <property name="c" value="C">
    + *          <property name="i" value="I"/>
    + *       </property>
    + *    </property>
    + *    <property name="date" value="16. Mar 2002"
    + *              type="java.util.Date" format="dd. MMM yyyy"/>
    + *    <property name="none" value="" />
    + * </properties>
    + * 
    + * Results in the properties {@code one=1, two.a=A, two.b=B is a very long..., + * two.c=C, two.c.i=I, date=Sat Mar 16 00:00:00 CET 2002 + * } and {@code none=}. Note that there is no property named + * {@code two}. + * + * @see java.util.Properties + * @see #setPropertyValue(String,Object) + * @see #getPropertyValue(String) + * @see #load(InputStream) + * @see #store(OutputStream,String) + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/XMLProperties.java#1 $ + * + */ +// TODO: Consider deleting this code.. Look at Properties XML format. +public class XMLProperties extends Properties { + + /** {@code "UTF-8"} */ + public final static String UTF_8_ENCODING = "UTF-8"; + + /** {@code "xmlns"} */ + public final static String XMLNS = "xmlns"; + + /** {@code "properties"} */ + public final static String PROPERTIES = "properties"; + + /** {@code "property"} */ + public final static String PROPERTY = "property"; + + /** {@code "name"} */ + public final static String PROPERTY_NAME = "name"; + + /** {@code "value"} */ + public final static String PROPERTY_VALUE = "value"; + + /** {@code "type"} */ + public final static String PROPERTY_TYPE = "type"; + + /** {@code "format"} */ + public final static String PROPERTY_FORMAT = "format"; + + /** {@code "String"} ({@link java.lang.String}) */ + public final static String DEFAULT_TYPE = "String"; + + /** {@code "yyyy-MM-dd hh:mm:ss.SSS"} + * ({@link java.sql.Timestamp} format, excpet nanos) */ + public final static String DEFAULT_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss.SSS"; + + /** This is the DTD */ + public final static String DTD = + "\n\n\n\n"; + + /** {@code "http://www.twelvemonkeys.com/xml/XMLProperties.dtd"} */ + public final static String SYSTEM_DTD_URI = "http://www.twelvemonkeys.com/xml/XMLProperties.dtd"; + + /** {@code "http://www.twelvemonkeys.com/xml/XMLProperties"} */ + public final static String NAMESPACE_URI = "http://www.twelvemonkeys.com/xml/XMLProperties"; + + /** {@code "http://xml.org/sax/features/validation"} */ + public final static String SAX_VALIDATION_URI = "http://xml.org/sax/features/validation"; + + /** debug */ + private boolean mValidation = true; + protected Vector mErrors = null; + protected Vector mWarnings = null; + + // protected Hashtable mTypes = new Hashtable(); + protected Hashtable mFormats = new Hashtable(); + protected static DateFormat sDefaultFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT); + + /** + * Creates an empty property list with no default values. + */ + public XMLProperties() {} + + /** + * Creates an empty property list with the specified defaults. + * + * @param pDefaults the defaults. + */ + public XMLProperties(Properties pDefaults) { + + // Sets the protected defaults variable + super(pDefaults); + } + + void addXMLError(SAXParseException pException) { + + if (mErrors == null) { + mErrors = new Vector(); + } + mErrors.add(pException); + } + + /** + * Gets the non-fatal XML errors (SAXParseExceptions) resulting from a + * load. + * + * @return An array of SAXParseExceptions, or null if none occured. + * + * @see XMLProperties.PropertiesHandler + * @see #load(InputStream) + */ + public SAXParseException[] getXMLErrors() { + + if (mErrors == null) { + return null; + } + return (SAXParseException[]) mErrors.toArray(new SAXParseException[mErrors.size()]); + } + + void addXMLWarning(SAXParseException pException) { + + if (mWarnings == null) { + mWarnings = new Vector(); + } + mWarnings.add(pException); + } + + /** + * Gets the XML warnings (SAXParseExceptions) resulting from a load. + * + * @return An array of SAXParseExceptions, or null if none occured. + * + * @see XMLProperties.PropertiesHandler + * @see #load(InputStream) + */ + public SAXParseException[] getXMLWarnings() { + + if (mWarnings == null) { + return null; + } + return (SAXParseException[]) mWarnings.toArray(new SAXParseException[mWarnings.size()]); + } + + /** + * Reads a property list (key and element pairs) from the input stream. The + * stream is assumed to be using the UFT-8 character encoding, and be in + * valid, well-formed XML format. + *

    + * After loading, any errors or warnings from the SAX parser, are available + * as array of SAXParseExceptions from the getXMLErrors and getXMLWarnings + * methods. + * + * @param pInput the input stream to load from. + * + * @exception IOException if an error occurred when reading from the input + * stream. Any SAXExceptions are also wrapped in IOExceptions. + * + * @see Properties#load(InputStream) + * @see #DTD + * @see #SYSTEM_DTD_URI + * @see XMLProperties.PropertiesHandler + * @see #getXMLErrors + * @see #getXMLWarnings + */ + public synchronized void load(InputStream pInput) throws IOException { + // Get parser instance + XMLReader parser; + + // Try to instantiate System default parser + String driver = System.getProperty("org.xml.sax.driver"); + + if (driver == null) { + + // Instantiate the org.apache.xerces.parsers.SAXParser as default + driver = "org.apache.xerces.parsers.SAXParser"; + } + try { + parser = XMLReaderFactory.createXMLReader(driver); + parser.setFeature(SAX_VALIDATION_URI, mValidation); + } catch (SAXNotRecognizedException saxnre) { + + // It should be okay to throw RuntimeExptions, as you will need an + // XML parser + throw new RuntimeException("Error configuring XML parser \"" + driver + "\": " + saxnre.getClass().getName() + ": " + + saxnre.getMessage()); + } catch (SAXException saxe) { + throw new RuntimeException("Error creating XML parser \"" + driver + "\": " + saxe.getClass().getName() + ": " + saxe.getMessage()); + } + + // Register handler + PropertiesHandler handler = new PropertiesHandler(this); + + parser.setContentHandler(handler); + parser.setErrorHandler(handler); + parser.setDTDHandler(handler); + parser.setEntityResolver(handler); + + // Read and parse XML + try { + parser.parse(new InputSource(pInput)); + } catch (SAXParseException saxpe) { + + // Wrap SAXException in IOException to be consistent + throw new IOException("Error parsing XML: " + saxpe.getClass().getName() + ": " + saxpe.getMessage() + " Line: " + + saxpe.getLineNumber() + " Column: " + saxpe.getColumnNumber()); + } catch (SAXException saxe) { + + // Wrap SAXException in IOException to be consistent + // Doesn't realy matter, as the SAXExceptions seems to be pretty + // meaningless themselves... + throw new IOException("Error parsing XML: " + saxe.getClass().getName() + ": " + saxe.getMessage()); + } + } + + /** + * Initializes the value of a property. + * + * @todo move init code to the parser? + * + * @throws ClassNotFoundException if there is no class found for the given + * type + * @throws IllegalArgumentException if the value given, is not parseable + * as the given type + */ + protected Object initPropertyValue(String pValue, String pType, String pFormat) throws ClassNotFoundException { + + // System.out.println("pValue=" + pValue + " pType=" + pType + // + " pFormat=" + pFormat); + // No value to convert + if (pValue == null) { + return null; + } + + // No conversion needed for Strings + if ((pType == null) || pType.equals("String") || pType.equals("java.lang.String")) { + return pValue; + } + Object value; + + if (pType.equals("Date") || pType.equals("java.util.Date")) { + + // Special parser needed + try { + + // Parse date through StringUtil + if (pFormat == null) { + value = StringUtil.toDate(pValue, sDefaultFormat); + } else { + value = StringUtil.toDate(pValue, new SimpleDateFormat(pFormat)); + } + } catch (IllegalArgumentException e) { + + // Not parseable... + throw e; + } + + // Return + return value; + } else if (pType.equals("java.sql.Timestamp")) { + + // Special parser needed + try { + + // Parse timestamp through StringUtil + value = StringUtil.toTimestamp(pValue); + } catch (IllegalArgumentException e) { + + // Not parseable... + throw new RuntimeException(e.getMessage()); + } + + // Return + return value; + } else { + int dot = pType.indexOf("."); + + if (dot < 0) { + pType = "java.lang." + pType; + } + + // Get class + Class cl = Class.forName(pType); + + // Try to create instance from (String) + value = createInstance(cl, pValue); + if (value == null) { + + // createInstance failed for some reason + // Try to invoke the static method valueof(String) + value = invokeStaticMethod(cl, "valueOf", pValue); + + // If the value is still null, well, then I cannot help... + } + } + + // Return + return value; + } + + /** + * Creates an object from the given class' single argument constructor. + * + * @return The object created from the constructor. + * If the constructor could not be invoked for any reason, null is + * returned. + */ + private Object createInstance(Class pClass, Object pParam) { + Object value; + + try { + + // Create param and argument arrays + Class[] param = { pParam.getClass() }; + Object[] arg = { pParam }; + + // Get constructor + Constructor constructor = pClass.getDeclaredConstructor(param); + + // Invoke and create instance + value = constructor.newInstance(arg); + } catch (Exception e) { + return null; + } + return value; + } + + /** + * Creates an object from any given static method, given the parameter + * + * @return The object returned by the static method. + * If the return type of the method is a primitive type, it is wrapped in + * the corresponding wrapper object (int is wrapped in an Integer). + * If the return type of the method is void, null is returned. + * If the method could not be invoked for any reason, null is returned. + */ + private Object invokeStaticMethod(Class pClass, String pMethod, Object pParam) { + Object value = null; + + try { + + // Create param and argument arrays + Class[] param = { pParam.getClass() }; + Object[] arg = { pParam }; + + // Get method + // *** If more than one such method is found in the class, and one + // of these methods has a RETURN TYPE that is more specific than + // any of the others, that method is reflected; otherwise one of + // the methods is chosen ARBITRARILY. + // java/lang/Class.html#getMethod(java.lang.String, java.lang.Class[]) + java.lang.reflect.Method method = pClass.getMethod(pMethod, param); + + // Invoke public static method + if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { + value = method.invoke(null, arg); + } + } catch (Exception e) { + return null; + } + return value; + } + + /** + * Gets the format of a property. This value is used for formatting the + * value before it is stored as xml. + * + * @param pKey a key in this hashtable. + * + * @return the format for the specified key or null if it does not + * have one. + */ + public String getPropertyFormat(String pKey) { + + // Get format + return StringUtil.valueOf(mFormats.get(pKey)); + } + + /** + * Sets the format of a property. This value is used for formatting the + * value before it is stored as xml. + * + * @param pKey a key in this hashtable. + * @param pFormat a string representation of the format. + * + * @return the previous format for the specified key or null if it did not + * have one. + */ + public synchronized String setPropertyFormat(String pKey, String pFormat) { + + // Insert format + return StringUtil.valueOf(mFormats.put(pKey, pFormat)); + } + + /** + * Calls the Hashtable method put. Provided for parallelism with the + * getPropertyValue method. Enforces use of strings for property keys. + * The value returned is the result of the Hashtable call to put. + * + * @param pKey the key to be placed into this property list. + * @param pValue the value corresponding to key. + * + * @return the previous value of the specified key in this property list, + * or null if it did not have one. + * + * @see #getPropertyValue(String) + */ + public synchronized Object setPropertyValue(String pKey, Object pValue) { + + // Insert value + return put(pKey, pValue); + } + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property + * list, and its defaults, recursively, are then checked. The method + * returns null if the property is not found. + * + * @param pKey the property key. + * + * @return the value in this property list with the specified key value. + * + * @see #setPropertyValue(String, Object) + * @see #getPropertyValue(String, Object) + * @see Properties#defaults + */ + public synchronized Object getPropertyValue(String pKey) { + return getPropertyValue(pKey, null); + } + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property + * list, and its defaults, recursively, are then checked. The method + * returns the default value argument if the property is not found. + * + * @param pKey the property key. + * @param pDefaultValue the default value. + * + * @return the value in this property list with the specified key value. + * + * @see #getPropertyValue(String) + * @see Properties#defaults + */ + public Object getPropertyValue(String pKey, Object pDefaultValue) { + + Object value = super.get(pKey); // super.get() is EXTREMELEY IMPORTANT + + if (value != null) { + return value; + } + if (defaults instanceof XMLProperties) { + return (((XMLProperties) defaults).getPropertyValue(pKey)); + } + return ((defaults != null) ? defaults.getProperty(pKey) : pDefaultValue); + } + + /** + * Overloaded get method, that always returns Strings. + * Due to the way the store and list methods of + * java.util.Properties works (it calls get and casts to String, instead + * of calling getProperty), this methods returns + * StringUtil.valueOf(super.get), to avoid ClassCastExcpetions. + *

    + * If you need the old functionality back, + * getPropertyValue returns super.get directly. + * A cleaner approach would be to override the list and store + * methods, but it's too much work for nothing... + * + * @param pKey a key in this hashtable + * + * @return the value to which the key is mapped in this hashtable, + * converted to a string; null if the key is not mapped to any value in + * this hashtable. + * + * @see #getPropertyValue(String) + * @see Properties#getProperty(String) + * @see Hashtable#get(Object) + * @see StringUtil#valueOf(Object) + */ + public Object get(Object pKey) { + + //System.out.println("get(" + pKey + "): " + super.get(pKey)); + Object value = super.get(pKey); + + // --- + if ((value != null) && (value instanceof Date)) { // Hmm.. This is true for subclasses too... + + // Special threatment of Date + String format = getPropertyFormat(StringUtil.valueOf(pKey)); + + if (format != null) { + value = new SimpleDateFormat(format).format(value); + } else { + value = sDefaultFormat.format(value); + } + return value; + } + + // --- + // Simply return value + return StringUtil.valueOf(value); + } + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property list, + * and its defaults, recursively, are then checked. The method returns the + * default value argument if the property is not found. + * + * @param pKey the hashtable key. + * @param pDefaultValue a default value. + * + * @return the value in this property list with the specified key value. + * @see #setProperty + * @see #defaults + */ + public String getProperty(String pKey, String pDefaultValue) { + + // Had to override this because Properties uses super.get()... + String value = (String) get(pKey); // Safe cast, see get(Object) + + if (value != null) { + return value; + } + return ((defaults != null) + ? defaults.getProperty(pKey) + : pDefaultValue); + } + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property list, + * and its defaults, recursively, are then checked. The method returns + * {@code null} if the property is not found. + * + * @param pKey the property key. + * @return the value in this property list with the specified key value. + * @see #setProperty + * @see #defaults + */ + public String getProperty(String pKey) { + + // Had to override this because Properties uses super.get()... + return getProperty(pKey, null); + } + + /** + * Writes this property list (key and element pairs) in this + * {@code Properties} + * table to the output stream in a format suitable for loading into a + * Properties table using the load method. This implementation writes + * the list in XML format. The stream is written using the UTF-8 character + * encoding. + * + * @param pOutput the output stream to write to. + * @param pHeader a description of the property list. + * + * @exception IOException if writing this property list to the specified + * output stream throws an IOException. + * + * @see java.util.Properties#store(OutputStream,String) + */ + public synchronized void store(OutputStream pOutput, String pHeader) throws IOException { + storeXML(this, pOutput, pHeader); + } + + /** + * Utility method that stores the property list in normal properties + * format. This method writes the list of Properties (key and element + * pairs) in the given {@code Properties} + * table to the output stream in a format suitable for loading into a + * Properties table using the load method. The stream is written using the + * ISO 8859-1 character encoding. + * + * @param pProperties the Properties table to store + * @param pOutput the output stream to write to. + * @param pHeader a description of the property list. + * + * @exception IOException if writing this property list to the specified + * output stream throws an IOException. + * + * @see java.util.Properties#store(OutputStream,String) + */ + public static void storeProperties(Map pProperties, OutputStream pOutput, String pHeader) throws IOException { + // Create new properties + Properties props = new Properties(); + + // Copy all elements from the pProperties (shallow) + Iterator iterator = pProperties.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + + props.setProperty((String) entry.getKey(), StringUtil.valueOf(entry.getValue())); + } + + // Store normal properties + props.store(pOutput, pHeader); + } + + /** + * Utility method that stores the property list in XML format. This method + * writes the list of Properties (key and element pairs) in the given + * {@code Properties} + * table to the output stream in a format suitable for loading into a + * XMLProperties table using the load method. Useful for converting + * Properties into XMLProperties. + * The stream is written using the UTF-8 character + * encoding. + * + * @param pProperties the Properties table to store. + * @param pOutput the output stream to write to. + * @param pHeader a description of the property list. + * + * @exception IOException if writing this property list to the specified + * output stream throws an IOException. + * + * @see #store(OutputStream,String) + * @see java.util.Properties#store(OutputStream,String) + * + * @todo Find correct way of setting namespace URI's + * @todo Store type and format information + */ + public static void storeXML(Map pProperties, OutputStream pOutput, String pHeader) throws IOException { + // Build XML tree (Document) and write + // Find the implementation + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + } + catch (ParserConfigurationException e) { + throw (IOException) new IOException(e.getMessage()).initCause(e); + } + DOMImplementation dom = builder.getDOMImplementation(); + + Document document = dom.createDocument(null, PROPERTIES, dom.createDocumentType(PROPERTIES, null, SYSTEM_DTD_URI)); + Element root = document.getDocumentElement(); + + // This is probably not the correct way of setting a default namespace + root.setAttribute(XMLNS, NAMESPACE_URI); + + // Create and insert the normal Properties headers as XML comments + if (pHeader != null) { + document.insertBefore(document.createComment(" " + pHeader + " "), root); + } + document.insertBefore(document.createComment(" " + new Date() + " "), root); + + // Insert elements from the Properties + Iterator iterator = pProperties.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + Object value = entry.getValue(); + String format = null; + + if (pProperties instanceof XMLProperties) { + format = ((XMLProperties) pProperties).getPropertyFormat(key); + } + insertElement(document, key, value, format); + } + + // Create serializer and output document + //XMLSerializer serializer = new XMLSerializer(pOutput, new OutputFormat(document, UTF_8_ENCODING, true)); + XMLSerializer serializer = new XMLSerializer(pOutput, UTF_8_ENCODING); + + serializer.serialize(document); + } + + /** + * Inserts elements to the given document one by one, and creates all its + * parents if needed. + * + * @param pDocument the document to insert to. + * @param pName the name of the property element. + * @param pValue the value of the property element. + * @param pFormat + * + * @todo I guess this implementation could use some optimisaztion, as + * we do a lot of unneccessary looping. + */ + private static void insertElement(Document pDocument, String pName, Object pValue, String pFormat) { + + // Get names of all elements we need + String[] names = StringUtil.toStringArray(pName, "."); + + // Get value formatted as string + String value = null; + + if (pValue != null) { + // --- + if (pValue instanceof Date) { + + // Special threatment of Date + if (pFormat != null) { + value = new SimpleDateFormat(pFormat).format(pValue); + } + else { + value = sDefaultFormat.format(pValue); + } + } + else { + value = String.valueOf(pValue); + } + + // --- + } + + // Loop through document from root, and insert parents as needed + Element element = pDocument.getDocumentElement(); + + for (int i = 0; i < names.length; i++) { + boolean found = false; + + // Get children + NodeList children = element.getElementsByTagName(PROPERTY); + Element child = null; + + // Search for correct name + for (int j = 0; j < children.getLength(); j++) { + child = (Element) children.item(j); + if (names[i].equals(child.getAttribute(PROPERTY_NAME))) { + // Found + found = true; + element = child; + break; // Next name + } + } + + // Test if the node was not found, otherwise we need to insert + if (!found) { + // Not found + child = pDocument.createElement(PROPERTY); + child.setAttribute(PROPERTY_NAME, names[i]); + + // Insert it + element.appendChild(child); + element = child; + } + + // If it's the destination node, set the value + if ((i + 1) == names.length) { + + // If the value string contains special data, + // use a CDATA block instead of the "value" attribute + if (StringUtil.contains(value, "\n") || StringUtil.contains(value, "\t") || StringUtil.contains(value, "\"") + || StringUtil.contains(value, "&") || StringUtil.contains(value, "<") || StringUtil.contains(value, ">")) { + + // Create value element + Element valueElement = pDocument.createElement(PROPERTY_VALUE); + + // Set type attribute + String className = pValue.getClass().getName(); + + className = StringUtil.replace(className, "java.lang.", ""); + if (!DEFAULT_TYPE.equals(className)) { + valueElement.setAttribute(PROPERTY_TYPE, className); + } + + // Set format attribute + if (pFormat != null) { + valueElement.setAttribute(PROPERTY_FORMAT, pFormat); + } + + // Crate cdata section + CDATASection cdata = pDocument.createCDATASection(value); + + // Append to document tree + valueElement.appendChild(cdata); + child.appendChild(valueElement); + } + else { + // Just set normal attribute value + child.setAttribute(PROPERTY_VALUE, value); + + // Set type attribute + String className = pValue.getClass().getName(); + + className = StringUtil.replace(className, "java.lang.", ""); + if (!DEFAULT_TYPE.equals(className)) { + child.setAttribute(PROPERTY_TYPE, className); + } + + // If format is set, store in attribute + if (pFormat != null) { + child.setAttribute(PROPERTY_FORMAT, pFormat); + } + } + } + } + } + + /** + * Gets all properties in a properties group. + * If no properties exists in the specified group, {@code null} is + * returned. + * + * @param pGroupKey the group key + * + * @return a new Properties continaing all properties in the group. Keys in + * the new Properties wil not contain the group key. + * If no properties exists in the specified group, {@code null} is + * returned. + */ + public Properties getProperties(String pGroupKey) { + // Stupid impl... + XMLProperties props = new XMLProperties(); + String groupKey = pGroupKey; + + if (groupKey.charAt(groupKey.length()) != '.') { + groupKey += "."; + } + + Iterator iterator = entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + + if (key.startsWith(groupKey)) { + String subKey = key.substring(key.indexOf(groupKey)); + + props.setPropertyValue(subKey, entry.getValue()); + } + } + + return ((props.size() > 0) ? props : null); + } + + /** + * Sets the properties in the given properties group. + * Existing properties in the same group, will not be removed, unless they + * are replaced by new values. + * Any existing properties in the same group that was replaced, are + * returned. If no properties are replaced, null is + * returned. + * + * @param pGroupKey the group key + * @param pProperties the properties to set into this group + * + * @return Any existing properties in the same group that was replaced. + * If no properties are replaced, null is + * returned. + */ + public Properties setProperties(String pGroupKey, Properties pProperties) { + XMLProperties old = new XMLProperties(); + String groupKey = pGroupKey; + + if (groupKey.charAt(groupKey.length()) != '.') { + groupKey += "."; + } + Iterator iterator = pProperties.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + Object obj = setPropertyValue(groupKey + key, entry.getValue()); + + // Return removed entries + if (obj != null) { + old.setPropertyValue(groupKey + key, entry.getValue()); + } + } + return ((old.size() > 0) ? old : null); + } + + /** + * For testing only. + */ + public static void main(String[] pArgs) throws Exception { + // -- Print DTD + System.out.println("DTD: \n" + DTD); + System.out.println("--"); + + // -- Test load + System.out.println("Reading properties from \"" + pArgs[0] + "\"..."); + XMLProperties props = new XMLProperties(); + + props.load(new FileInputStream(new File(pArgs[0]))); + props.list(System.out); + System.out.println("--"); + + // -- Test recursion + String key = "key"; + Object old = props.setProperty(key, "AAA"); + Properties p1 = new XMLProperties(new XMLProperties(props)); + Properties p2 = new Properties(new Properties(props)); + + System.out.println("XMLProperties: " + p1.getProperty(key) + " ==" + " Properties: " + p2.getProperty(key)); + if (old == null) { + props.remove("key"); + } else { + props.put("key", old); // Put old value back, to avoid confusion... + } + System.out.println("--"); + + // -- Test store + //props.store(System.out, "XML Properties file written by XMLProperties."); + File out = new File("copy_of_" + pArgs[0]); + + System.out.println("Writing properties to \"" + out.getName() + "\""); + if (!out.exists()) { + props.store(new FileOutputStream(out), "XML Properties file written by XMLProperties."); + } else { + System.err.println("File \"" + out.getName() + "\" allready exists, cannot write!"); + } + + // -- Test utility methods + // Write normal properties from XMLProperties + out = new File("copy_of_" + pArgs[0].substring(0, pArgs[0].lastIndexOf(".")) + ".properties"); + System.out.println("Writing properties to \"" + out.getName() + "\""); + if (!out.exists()) { + storeProperties(props, new FileOutputStream(out), "Properties file written by XMLProperties."); + } else { + System.err.println("File \"" + out.getName() + "\" allready exists, cannot write!"); + } + System.out.println("--"); + + // -- Test type attribute + System.out.println("getPropertyValue(\"one\"): " + props.getPropertyValue("one") + " class: " + + props.getPropertyValue("one").getClass()); + System.out.println("setPropertyValue(\"now\", " + new Date() + "): " + props.setPropertyValue("now", new Date()) + " class: " + + props.getPropertyValue("now").getClass()); + System.out.println("getPropertyValue(\"date\"): " + props.getPropertyValue("date") + " class: " + + props.getPropertyValue("date").getClass()); + System.out.println("getPropertyValue(\"time\"): " + props.getPropertyValue("time") + " class: " + + props.getPropertyValue("time").getClass()); + } + + /** + * ContentHandler, ErrorHandler and EntityResolver implementation for the + * SAX Parser. + */ + protected class PropertiesHandler extends DefaultHandler { + protected Stack mStack = null; + + /** Stores the characters read so far, from the characters callback */ + protected char[] mReadSoFar = null; + protected boolean mIsValue = false; + protected String mType = null; + protected String mFormat = null; + protected XMLProperties mProperties = null; + protected Locator mLocator = null; + + /** + * Creates a PropertiesHandler for the given XMLProperties. + */ + PropertiesHandler(XMLProperties pProperties) { + mProperties = pProperties; + mStack = new Stack(); + } + + /** + * setDocumentLocator implementation. + */ + public void setDocumentLocator(Locator pLocator) { + + // System.out.println("Setting locator: " + pLocator); + mLocator = pLocator; + } + + /** + * Calls XMLProperties.addXMLError with the given SAXParseException + * as the argument. + */ + public void error(SAXParseException pException) throws SAXParseException { + + //throw pException; + mProperties.addXMLError(pException); + + /* + System.err.println("error: " + pException.getMessage()); + System.err.println("line: " + mLocator.getLineNumber()); + System.err.println("column: " + mLocator.getColumnNumber()); + */ + } + + /** + * Throws the given SAXParseException (and stops the parsing). + */ + public void fatalError(SAXParseException pException) throws SAXParseException { + + throw pException; + + /* + System.err.println("fatal error: " + pException.getMessage()); + System.err.println("line: " + mLocator.getLineNumber()); + System.err.println("column: " + mLocator.getColumnNumber()); + */ + } + + /** + * Calls XMLProperties.addXMLWarning with the given SAXParseException + * as the argument. + */ + public void warning(SAXParseException pException) throws SAXParseException { + + // throw pException; + mProperties.addXMLWarning(pException); + + /* + System.err.println("warning: " + pException.getMessage()); + System.err.println("line: " + mLocator.getLineNumber()); + System.err.println("column: " + mLocator.getColumnNumber()); + */ + } + + /** + * startElement implementation. + */ + public void startElement(String pNamespaceURI, String pLocalName, String pQualifiedName, Attributes pAttributes) throws SAXException { + + /* + + String attributes = ""; + for (int i = 0; i < pAttributes.getLength(); i++) { + attributes += pAttributes.getQName(i) + "=" + pAttributes.getValue(i) + (i < pAttributes.getLength() ? ", " : ""); + } + + System.out.println("startElement: " + pNamespaceURI + + "." + pLocalName + + " (" + pQualifiedName + ") " + + attributes); + */ + if (XMLProperties.PROPERTY.equals(pLocalName)) { + + // Get attibute values + String name = pAttributes.getValue(XMLProperties.PROPERTY_NAME); + String value = pAttributes.getValue(XMLProperties.PROPERTY_VALUE); + String type = pAttributes.getValue(XMLProperties.PROPERTY_TYPE); + String format = pAttributes.getValue(XMLProperties.PROPERTY_FORMAT); + + // Get the full name of the property + if (!mStack.isEmpty()) { + name = (String) mStack.peek() + "." + name; + } + + // Set the property + if (value != null) { + mProperties.setProperty(name, value); + + // Store type & format + if (!XMLProperties.DEFAULT_TYPE.equals(type)) { + mType = type; + mFormat = format; // Might be null (no format) + } + } + + // Push the last name on the stack + mStack.push(name); + } // /PROPERTY + else if (XMLProperties.PROPERTY_VALUE.equals(pLocalName)) { + + // Get attibute values + String name = (String) mStack.peek(); + String type = pAttributes.getValue(XMLProperties.PROPERTY_TYPE); + String format = pAttributes.getValue(XMLProperties.PROPERTY_FORMAT); + + // Store type & format + if (!XMLProperties.DEFAULT_TYPE.equals(type)) { + mType = type; + mFormat = format; + } + mIsValue = true; + } + } + + /** + * endElement implementation. + */ + public void endElement(String pNamespaceURI, String pLocalName, String pQualifiedName) throws SAXException { + + /* + System.out.println("endElement: " + pNamespaceURI + + "." + pLocalName + " (" + pQualifiedName + ")"); + */ + if (XMLProperties.PROPERTY.equals(pLocalName)) { + + // Just remove the last name + String name = (String) mStack.pop(); + + // Init typed values + try { + String prop = mProperties.getProperty(name); + + // Value may be null, if so just skip + if (prop != null) { + Object value = mProperties.initPropertyValue(prop, mType, mFormat); + + // Store format + if ((mFormat != null) &&!XMLProperties.DEFAULT_DATE_FORMAT.equals(mFormat)) { + mProperties.setPropertyFormat(name, mFormat); + } + + //System.out.println("-->" + prop + "-->" + value); + mProperties.setPropertyValue(name, value); + } + + // Clear type & format + mType = null; + mFormat = null; + } catch (Exception e) { + e.printStackTrace(System.err); + throw new SAXException(e); + } + } else if (XMLProperties.PROPERTY_VALUE.equals(pLocalName)) { + if (mStack.isEmpty()) { + + // There can't be any characters here, really + return; + } + + // Get the full name of the property + String name = (String) mStack.peek(); + + // Set the property + String value = new String(mReadSoFar); + + //System.err.println("characters: >" + value+ "<"); + if (!StringUtil.isEmpty(value)) { + + // If there is allready a value, both the value attribute + // and element have been specified, this is an error + if (mProperties.containsKey(name)) { + throw new SAXParseException( + "Value can only be specified either using the \"value\" attribute, OR the \"value\" element, not both.", mLocator); + } + + // Finally, set the property + mProperties.setProperty(name, value); + } + + // Done value processing + mIsValue = false; + } + } + + /** + * characters implementation + */ + public void characters(char[] pChars, int pStart, int pLength) throws SAXException { + // TODO: Use StringBuilder instead? + if (mIsValue) { + // If nothing read so far + if (mReadSoFar == null) { + // Create new array and copy into + mReadSoFar = new char[pLength]; + System.arraycopy(pChars, pStart, mReadSoFar, 0, pLength); + } + else { + // Merge arrays + mReadSoFar = (char[]) CollectionUtil.mergeArrays(mReadSoFar, 0, mReadSoFar.length, pChars, pStart, pLength); + } + } + } + + + /** + * Intercepts the entity + * "http://www.twelvemonkeys.com/xml/XMLProperties.dtd", and return + * an InputSource based on the internal DTD of XMLProperties instead. + * + * @todo Maybe intercept a PUBLIC DTD and be able to have SYSTEM DTD + * override? + */ + public InputSource resolveEntity(String pPublicId, String pSystemId) { + // If we are looking for the standard SYSTEM DTD, then + // Return an InputSource based on the internal DTD. + if (XMLProperties.SYSTEM_DTD_URI.equals(pSystemId)) { + return new InputSource(new StringReader(XMLProperties.DTD)); + } + + // use the default behaviour + return null; + } + } +} + diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/util/regex/REWildcardStringParser.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java similarity index 95% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/util/regex/REWildcardStringParser.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java index d2bbb8de..fa2f2879 100755 --- a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/util/regex/REWildcardStringParser.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java @@ -82,9 +82,9 @@ public class REWildcardStringParser /*extends EntityObject*/ { /** Field ALPHABET */ public static final char[] ALPHABET = { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'æ', - 'ø', 'å', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'N', 'M', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', - 'Z', 'Æ', 'Ø', 'Å', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-' + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\u00e6', + '\u00f8', '\u00e5', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'N', 'M', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', '\u00c6', '\u00d8', '\u00c5', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-' }; /** Field FREE_RANGE_CHARACTER */ @@ -165,12 +165,12 @@ public class REWildcardStringParser /*extends EntityObject*/ { char stringMaskChar = pWildcardExpression.charAt(i); if (isFreeRangeCharacter(stringMaskChar)) { - regexpBuffer.append("(([a-åA-Å0-9]|.|_|-)*)"); + regexpBuffer.append("(([a-�A-�0-9]|.|_|-)*)"); } // Free-pass character '?' else if (isFreePassCharacter(stringMaskChar)) { - regexpBuffer.append("([a-åA_Å0-9]|.|_|-)"); + regexpBuffer.append("([a-�A_�0-9]|.|_|-)"); } // Valid characters diff --git a/sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/xml/XMLReader.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/xml/XMLReader.java similarity index 100% rename from sandbox/common/src/main/java/com/twelvemonkeys/twelvemonkeys/xml/XMLReader.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/xml/XMLReader.java diff --git a/sandbox/common/src/test/java/com/twelvemonkeys/io/enc/DeflateEncoderTestCase.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/enc/DeflateEncoderTestCase.java similarity index 96% rename from sandbox/common/src/test/java/com/twelvemonkeys/io/enc/DeflateEncoderTestCase.java rename to sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/enc/DeflateEncoderTestCase.java index a1666ff4..d2e501ff 100644 --- a/sandbox/common/src/test/java/com/twelvemonkeys/io/enc/DeflateEncoderTestCase.java +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/enc/DeflateEncoderTestCase.java @@ -1,18 +1,18 @@ -package com.twelvemonkeys.io.enc; - -/** - * DeflateEncoderTest - *

    - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java#1 $ - */ -public class DeflateEncoderTestCase extends EncoderAbstractTestCase { - protected Encoder createEncoder() { - return new DeflateEncoder(); - } - - protected Decoder createCompatibleDecoder() { - return new InflateDecoder(); - } -} +package com.twelvemonkeys.io.enc; + +/** + * DeflateEncoderTest + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java#1 $ + */ +public class DeflateEncoderTestCase extends EncoderAbstractTestCase { + protected Encoder createEncoder() { + return new DeflateEncoder(); + } + + protected Decoder createCompatibleDecoder() { + return new InflateDecoder(); + } +} diff --git a/sandbox/common/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java similarity index 96% rename from sandbox/common/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java rename to sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java index fcde90b3..70a6ad08 100644 --- a/sandbox/common/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java @@ -1,18 +1,18 @@ -package com.twelvemonkeys.io.enc; - -/** - * InflateEncoderTest - *

    - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java#1 $ - */ -public class InflateDecoderTestCase extends DecoderAbstractTestCase { - public Decoder createDecoder() { - return new InflateDecoder(); - } - - public Encoder createCompatibleEncoder() { - return new DeflateEncoder(); - } -} +package com.twelvemonkeys.io.enc; + +/** + * InflateEncoderTest + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java#1 $ + */ +public class InflateDecoderTestCase extends DecoderAbstractTestCase { + public Decoder createDecoder() { + return new InflateDecoder(); + } + + public Encoder createCompatibleEncoder() { + return new DeflateEncoder(); + } +} diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/lang/DuckTypeTestCase.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/lang/DuckTypeTestCase.java new file mode 100755 index 00000000..303db743 --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/lang/DuckTypeTestCase.java @@ -0,0 +1,246 @@ +package com.twelvemonkeys.lang; + +import junit.framework.TestCase; +import junit.framework.AssertionFailedError; + +/** + * “If it walks like a duck, looks like a duck, quacks like a duck, it must be…” + *

    + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/test/java/com/twelvemonkeys/lang/DuckTypeTestCase.java#1 $ + */ +public class DuckTypeTestCase extends TestCase { + + static public interface Eatable { + } + + static public interface Vegetable extends Eatable { + } + + static public interface Meat extends Eatable { + } + + static public interface Animal { + void walk(); + boolean canEat(Eatable pFood); + void eat(Eatable pFood); + } + + static public interface Bird extends Animal { + void fly(); + } + + static public interface Duck extends Bird { + void quack(); + } + + static public class DuckLookALike { + private boolean mWalking; + private boolean mFlying; + private boolean mQuacking; + + public void walk() { + mWalking = true; + } + + public void fly() { + mFlying = true; + } + + public void quack() { + mQuacking = true; + } + + void reset() { + mWalking = mFlying = mQuacking = false; + } + + boolean verify() { + return mWalking && mFlying && mQuacking; + } + } + + static public class Swan extends DuckLookALike { + } + + static public class VeggieEater { + private boolean mHappy; + + public boolean canEat(Eatable pFood) { + return pFood instanceof Vegetable; + } + + public void eat(Eatable pFood) { + if (pFood == this) { + throw new IllegalArgumentException("CantEatMyselfException: duh"); + } + if (!canEat(pFood)) { + throw new NotVegetableException("yuck"); + } + + mHappy = true; + } + + void reset() { + mHappy = false; + } + + boolean verify() { + return mHappy; + } + } + + static class NotVegetableException extends RuntimeException { + public NotVegetableException(String message) { + super(message); + } + } + + public static void testTooManyThingsAtOnce() { + DuckLookALike lookALike = new DuckLookALike(); + VeggieEater veggieEater = new VeggieEater(); + + Object obj = DuckType.implement(new Class[]{Duck.class, Meat.class}, + new Object[]{lookALike, veggieEater}); + assertTrue(obj instanceof Duck); + assertTrue(obj instanceof Meat); + Duck duck = (Duck) obj; + + Bird another = (Bird) DuckType.implement(new Class[]{Duck.class, Meat.class}, + new Object[]{lookALike, veggieEater}); + + Duck uglyDuckling = (Duck) DuckType.implement(new Class[] {Duck.class, Meat.class}, + new Object[] {new Swan(), new VeggieEater()}); + + assertNotNull(duck.toString()); + + assertTrue("Duck is supposed to equal itself (identity crisis)", duck.equals(duck)); + + assertEquals("Duck is supposed to equal other duck with same stuffing", duck, another); + + assertFalse("Some ducks are more equal than others", duck.equals(uglyDuckling)); + + duck.walk(); + duck.quack(); + duck.quack(); + duck.fly(); + + assertTrue("Duck is supposed to quack", lookALike.verify()); + + Vegetable cabbage = new Vegetable() {}; + assertTrue("Duck is supposed to like cabbage", duck.canEat(cabbage)); + duck.eat(cabbage); + assertTrue("Duck is supposed to eat vegetables", veggieEater.verify()); + + veggieEater.reset(); + + Throwable exception = null; + try { + duck.eat((Meat) uglyDuckling); + fail("Duck ate distant cousin"); + } + catch (AssertionFailedError e) { + throw e; + } + catch (Throwable t) { + exception = t; + } + assertTrue("Incorrect quack: " + exception, exception instanceof NotVegetableException); + + + // TODO: There's a flaw in the design here.. + // The "this" keyword don't work well with proxies.. + + // Something that could possibly work, is: + // All proxy-aware delegates need a method getThis() / getSelf()... + // (using a field won't work for delegates that are part of multiple + // proxies). + // The default implementation should only return "this".. + // TBD: How do we know which proxy the current delegate is part of? + + exception = null; + try { + duck.eat((Meat) duck); + fail("Duck ate itself"); + } + catch (AssertionFailedError e) { + throw e; + } + catch (Throwable t) { + exception = t; + } + assertTrue("Duck tried to eat itself: " + exception, exception instanceof IllegalArgumentException); + } + + public void testExpandedArgs() { + Object walker = new Object() { + public void walk() { + } + }; + Object eater = new Object() { + // Assignable, but not direct match + public boolean canEat(Object pFood) { + return true; + } + + // Assignable, but not direct match + public void eat(Object pFood) { + } + }; + + Animal rat = (Animal) DuckType.implement(new Class[]{Animal.class, Meat.class}, + new Object[]{walker, eater}); + + assertNotNull(rat); + assertTrue(rat instanceof Meat); + + // Rats eat everything + Eatable smellyFood = new Eatable() {boolean tastesVeryBad = true;}; + assertTrue("Rat did not eat smelly food", rat.canEat(smellyFood)); + } + + public void testExpandedArgsFail() { + try { + Object walker = new Object() { + public void walk() { + } + }; + Object eater = new Object() { + // Not assignable return type + public int canEat(Eatable pFood) { + return 1; + } + + // Assignable, but not direct match + public void eat(Object pFood) { + } + }; + DuckType.implement(new Class[]{Animal.class}, + new Object[]{walker, eater}); + + fail("This kind of animal won't live long"); + } + catch (DuckType.NoMatchingMethodException e) { + } + } + + public void testStubAbstract() { + Object obj = DuckType.implement(new Class[]{Animal.class}, + new Object[]{new Object()}, true); + assertTrue(obj instanceof Animal); + Animal unicorn = (Animal) obj; + assertNotNull(unicorn); + + // Should create a meaningful string representation + assertNotNull(unicorn.toString()); + + // Unicorns don't fly, as they are only an abstract idea.. + try { + unicorn.walk(); + fail("Unicorns should not fly, as they are only an abstract idea"); + } + catch (AbstractMethodError e) { + } + } +} diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/lang/ExceptionUtilTest.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/lang/ExceptionUtilTest.java new file mode 100644 index 00000000..90b5aed5 --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/lang/ExceptionUtilTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.lang; + +import org.junit.Ignore; +import org.junit.Test; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.sql.SQLException; + +/** + * ExceptionUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ExceptionUtilTest.java,v 1.0 11.04.12 16:07 haraldk Exp$ + */ +@Ignore("Under development") +public class ExceptionUtilTest { + @Test(expected = BadException.class) + @SuppressWarnings({"InfiniteLoopStatement"}) + public void test() { + while (true) { + foo(); + } + } + + @SuppressWarnings({"unchecked", "varargs"}) + private static void foo() { + try { + bar(); + } + catch (Throwable t) { + ExceptionUtil.handle(t, + new ExceptionUtil.ThrowableHandler(IOException.class) { + public void handle(final IOException pThrowable) { + System.out.println("IOException: " + pThrowable + " handled"); + } + }, + new ExceptionUtil.ThrowableHandler(SQLException.class, NumberFormatException.class) { + public void handle(final Exception pThrowable) { + System.out.println("Exception: " + pThrowable + " handled"); + } + } + ); + } + } + + private static void bar() { + baz(); + } + + @SuppressWarnings({"ThrowableInstanceNeverThrown"}) + private static void baz() { + double random = Math.random(); + if (random < (2.0 / 3.0)) { + ExceptionUtil.throwUnchecked(new FileNotFoundException("FNF Boo")); + } + if (random < (5.0 / 6.0)) { + ExceptionUtil.throwUnchecked(new SQLException("SQL Boo")); + } + else { + ExceptionUtil.throwUnchecked(new BadException("Some Boo")); + } + } + + static final class BadException extends Exception { + public BadException(String s) { + super(s); + } + } +} diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/MappedBeanFactoryTestCase.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/MappedBeanFactoryTestCase.java new file mode 100755 index 00000000..bf313abd --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/MappedBeanFactoryTestCase.java @@ -0,0 +1,578 @@ +/* + * Copyright (c) 2009, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.util; + +import com.twelvemonkeys.util.MappedBeanFactory; +import junit.framework.AssertionFailedError; +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +import java.awt.*; +import java.awt.event.ActionListener; +import java.beans.IntrospectionException; +import java.beans.PropertyChangeListener; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.io.*; + +/** + * MappedBeanFactoryTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-sandbox/src/test/java/com/twelvemonkeys/sandbox/MappedBeanFactoryTestCase.java#1 $ + */ +public class MappedBeanFactoryTestCase { + + public static interface Foo { + boolean isFoo(); + + int getBar(); + void setBar(int bar); + + Rectangle getBounds(); + void setBounds(Rectangle bounds); + } + + public static interface DefaultFoo extends Foo { +// @MappedBeanFactory.DefaultBooleanValue + @MappedBeanFactory.DefaultValue(booleanValue = false) + boolean isFoo(); + + @MappedBeanFactory.DefaultIntValue + int getBar(); + void setBar(int bar); + + Rectangle getBounds(); + void setBounds(Rectangle bounds); + + @SuppressWarnings({"CloneDoesntDeclareCloneNotSupportedException"}) + DefaultFoo clone(); + } + + static interface ObservableFoo extends DefaultFoo { + @MappedBeanFactory.DefaultBooleanValue(true) + boolean isFoo(); + + @MappedBeanFactory.DefaultIntValue(1) + int getBar(); + + @MappedBeanFactory.NotNull + Rectangle getBounds(); + + @MappedBeanFactory.Observable + void setBounds(@MappedBeanFactory.NotNull Rectangle bounds); + + // TODO: This method should be implicitly supported, and throw IllegalArgument, if NoSuchProperty + // TODO: An observable interface to extend? + void addPropertyChangeListener(String property, PropertyChangeListener listener); + } + + @Test + public void testToString() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + assertNotNull(foo); + assertNotNull(foo.toString()); + assertTrue(foo.toString().contains(DefaultFoo.class.getName())); + + // TODO: Consider this: +// assertTrue(foo.toString().contains("foo=false")); +// assertTrue(foo.toString().contains("bar=0")); +// assertTrue(foo.toString().contains("bounds=null")); + } + + @Test + public void testClone() { + DefaultFoo foo = MappedBeanFactory.as(DefaultFoo.class, Collections.singletonMap("foo", true)); + DefaultFoo clone = foo.clone(); + assertNotSame(foo, clone); + assertEquals(foo, clone); + assertEquals(foo.hashCode(), clone.hashCode()); + assertEquals(foo.isFoo(), clone.isFoo()); + } + + @Test + public void testSerializable() throws IOException, ClassNotFoundException { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, Collections.singletonMap("foo", true)); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ObjectOutputStream outputStream = new ObjectOutputStream(bytes); + outputStream.writeObject(foo); + + ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray())); + Foo bar = (Foo) inputStream.readObject(); + + assertNotSame(foo, bar); + assertEquals(foo, bar); + assertEquals(foo.hashCode(), bar.hashCode()); + assertEquals(foo.isFoo(), bar.isFoo()); + } + + @Test + public void testNotEqualsNull() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + + @SuppressWarnings({"ObjectEqualsNull"}) + boolean equalsNull = foo.equals(null); + + assertFalse(equalsNull); + } + + @Test + public void testEqualsSelf() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap() { + @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass"}) + @Override + public boolean equals(Object o) { + throw new AssertionFailedError("Don't need to test map for equals if same object"); + } + }); + + assertTrue(foo.equals(foo)); + } + + @Test + public void testEqualsOther() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + Foo other = MappedBeanFactory.as(DefaultFoo.class); + + assertEquals(foo, other); + } + + @Test + public void testEqualsOtherModifiedSameValue() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap()); + Foo other = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bar", 0))); + + assertEquals(foo, other); + + // No real change + other.setBar(foo.getBar()); + assertTrue(foo.equals(other)); + } + + @Test + public void testNotEqualsOtherModified() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + Foo other = MappedBeanFactory.as(DefaultFoo.class); + + assertEquals(foo, other); + + // Real change + other.setBar(42); + assertFalse(foo.equals(other)); + } + + @Test + public void testEqualsSubclass() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + + Foo sub = MappedBeanFactory.as(ObservableFoo.class); + assertEquals(foo, sub); + } + + @Test + public void testNotEqualsDifferentValues() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + Foo bar = MappedBeanFactory.as(DefaultFoo.class, Collections.singletonMap("bar", true)); + assertFalse(foo.equals(bar)); + } + + @Test + public void testNotEqualsDifferentClass() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class); + ActionListener actionListener = MappedBeanFactory.as(ActionListener.class); + assertFalse(foo.equals(actionListener)); + } + + @Test + public void testBooleanReadOnly() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, Collections.singletonMap("foo", true)); + + assertNotNull(foo); + assertEquals(true, foo.isFoo()); + } + + @Test + public void testBooleanEmpty() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap()); + + assertNotNull(foo); + + try { + foo.isFoo(); + fail("Expected NullPointerException"); + } + catch (NullPointerException expected) { + } + } + + @Test + public void testBooleanEmptyWithConverter() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(), new NullBooleanConverter(true)); + + assertNotNull(foo); + + assertEquals(true, foo.isFoo()); + } + + @Test + public void testIntReadOnly() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, Collections.singletonMap("bar", 1)); + + assertNotNull(foo); + assertEquals(1, foo.getBar()); + + try { + foo.setBar(42); + fail("Expected UnsupportedOperationException"); + } + catch (UnsupportedOperationException expected) { + } + } + + @Test + public void testIntReadWrite() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bar", 1))); + + assertNotNull(foo); + assertEquals(1, foo.getBar()); + + foo.setBar(42); + assertEquals(42, foo.getBar()); + } + + @Test + public void testIntNull() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bar", null))); + + assertNotNull(foo); + + // TODO: Handle null-values smarter, maybe throw a better exception? + // TODO: Consider allowing custom initializers? + try { + foo.getBar(); + fail("Expected NullPointerException"); + } + catch (NullPointerException expected) { + } + + foo.setBar(42); + assertEquals(42, foo.getBar()); + } + + @Test + public void testIntNullWithConverter() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bar", null)), new NullIntConverter(1)); + + assertNotNull(foo); + + assertEquals(1, foo.getBar()); + + foo.setBar(42); + assertEquals(42, foo.getBar()); + } + + @Test + public void testIntWrongType() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bar", "1"))); + + assertNotNull(foo); + + // TODO: Handle conversion smarter, maybe throw a better exception? + try { + foo.getBar(); + fail("Expected ClassCastException"); + } + catch (ClassCastException expected) { + } + + // TODO: Should we allow changing type? + try { + foo.setBar(42); + fail("Expected ClassCastException"); + } + catch (ClassCastException expected) { + } + } + + @Test + public void testBounds() { + Rectangle rectangle = new Rectangle(2, 2, 4, 4); + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bounds", rectangle))); + + assertNotNull(foo); + assertEquals(rectangle, foo.getBounds()); + + foo.setBounds(new Rectangle()); + assertEquals(new Rectangle(), foo.getBounds()); + } + + @Test + public void testBoundsNull() { + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("bounds", null))); + + assertNotNull(foo); + assertNull(foo.getBounds()); + + Rectangle rectangle = new Rectangle(2, 2, 4, 4); + foo.setBounds(rectangle); + assertEquals(rectangle, foo.getBounds()); + + foo.setBounds(null); + assertEquals(null, foo.getBounds()); + } + + @Test + public void testBoundsNullWithConverter() { + // TODO: Allow @NotNull annotations, to say that null is not a valid return value/paramter? + Foo foo = MappedBeanFactory.as(ObservableFoo.class, new HashMap(Collections.singletonMap("bounds", null)), new MappedBeanFactory.Converter() { + public Class getFromType() { + return Void.class; + } + + public Class getToType() { + return Rectangle.class; + } + + public Rectangle convert(Void value, Rectangle old) { + return new Rectangle(10, 10, 10, 10); + } + }); + + assertNotNull(foo); + // TODO: The current problem is that null is okay as return value, even if not specified for interface... + assertEquals(new Rectangle(10, 10, 10, 10), foo.getBounds()); + + Rectangle rectangle = new Rectangle(2, 2, 4, 4); + foo.setBounds(rectangle); + assertEquals(rectangle, foo.getBounds()); + } + + @Test + public void testBoundsAsMapWithConverter() throws IntrospectionException { + Rectangle rectangle = new Rectangle(2, 2, 4, 4); + Map recAsMap = new HashMap(); + recAsMap.put("x", 2); + recAsMap.put("y", 2); + recAsMap.put("width", 4); + recAsMap.put("height", 4); + + HashMap map = new HashMap(Collections.singletonMap("bounds", recAsMap)); + + // TODO: Allow for registering superclasses/interfaces like Map... + Foo foo = MappedBeanFactory.as(DefaultFoo.class, map, new MapRectangleConverter(), new RectangleMapConverter()); + + assertNotNull(foo); + + assertEquals(rectangle, foo.getBounds()); + + foo.setBounds(new Rectangle()); + assertEquals(new Rectangle(), foo.getBounds()); + assertEquals(recAsMap, map.get("bounds")); + assertSame(recAsMap, map.get("bounds")); + + // TODO: The converter should maybe not have to handle this + foo.setBounds(null); + assertNull(foo.getBounds()); + assertEquals(recAsMap, map.get("bounds")); + assertSame(recAsMap, map.get("bounds")); + + Rectangle bounds = new Rectangle(1, 1, 1, 1); + foo.setBounds(bounds); + assertEquals(bounds, foo.getBounds()); + assertEquals(1, foo.getBounds().x); + assertEquals(1, foo.getBounds().y); + assertEquals(1, foo.getBounds().width); + assertEquals(1, foo.getBounds().height); + assertEquals(recAsMap, map.get("bounds")); + assertSame(recAsMap, map.get("bounds")); + } + + @Test + public void testSpeed() { + // How many times faster may the direct access be, before we declare failure? + final int threshold = 50; + + Foo foo = MappedBeanFactory.as(DefaultFoo.class, new HashMap(Collections.singletonMap("foo", false))); + + Foo bar = new Foo() { + public boolean isFoo() { + return false; + } + + public int getBar() { + throw new UnsupportedOperationException("Method getBar not implemented"); + } + + public void setBar(int bar) { + throw new UnsupportedOperationException("Method setBar not implemented"); + } + + public Rectangle getBounds() { + throw new UnsupportedOperationException("Method getBounds not implemented"); // TODO: Implement + } + + public void setBounds(Rectangle bounds) { + throw new UnsupportedOperationException("Method setBounds not implemented"); // TODO: Implement + } + }; + + final int warmup = 50005; + final int iter = 2000000; + for (int i = 0; i < warmup; i++) { + if (foo.isFoo()) { + fail(); + } + if (bar.isFoo()) { + fail(); + } + } + + long startProxy = System.nanoTime(); + for (int i = 0; i < iter; i++) { + if (foo.isFoo()) { + fail(); + } + } + long proxyTime = System.nanoTime() - startProxy; + + long startJava = System.nanoTime(); + for (int i = 0; i < iter; i++) { + if (bar.isFoo()) { + fail(); + } + } + long javaTime = System.nanoTime() - startJava; + + assertTrue( + String.format( + "Proxy time (%1$,d ms) greater than %3$d times direct invocation (%2$,d ms)", + proxyTime / 1000, javaTime / 1000, threshold + ), + proxyTime < threshold * javaTime); + } + + private static class MapRectangleConverter implements MappedBeanFactory.Converter { + public Class getFromType() { + return HashMap.class; + } + + public Class getToType() { + return Rectangle.class; + } + + public Rectangle convert(final HashMap pMap, Rectangle pOldValue) { + if (pMap == null || pMap.isEmpty()) { + return null; + } + + Rectangle rectangle = pOldValue != null ? pOldValue : new Rectangle(); + + rectangle.x = (Integer) pMap.get("x"); + rectangle.y = (Integer) pMap.get("y"); + rectangle.width = (Integer) pMap.get("width"); + rectangle.height = (Integer) pMap.get("height"); + + return rectangle; + } + } + + private static class RectangleMapConverter implements MappedBeanFactory.Converter { + public Class getToType() { + return HashMap.class; + } + + public Class getFromType() { + return Rectangle.class; + } + + public HashMap convert(final Rectangle pRectangle, HashMap pOldValue) { + @SuppressWarnings("unchecked") + HashMap map = pOldValue != null ? pOldValue : new HashMap(); + + if (pRectangle != null) { + map.put("x", pRectangle.x); + map.put("y", pRectangle.y); + map.put("width", pRectangle.width); + map.put("height", pRectangle.height); + } + else { + map.remove("x"); + map.remove("y"); + map.remove("width"); + map.remove("height"); + } + + return map; + } + } + + private static class NullIntConverter implements MappedBeanFactory.Converter { + private Integer mInitialValue; + + public NullIntConverter(int pValue) { + mInitialValue = pValue; + } + + public Class getFromType() { + return Void.class; + } + + public Class getToType() { + return Integer.class; + } + + public Integer convert(Void value, Integer old) { + return mInitialValue; + } + } + + private static class NullBooleanConverter implements MappedBeanFactory.Converter { + private Boolean mInitialValue; + + public NullBooleanConverter(boolean pValue) { + mInitialValue = pValue; + } + + public Class getFromType() { + return Void.class; + } + + public Class getToType() { + return Boolean.class; + } + + public Boolean convert(Void value, Boolean old) { + return mInitialValue; + } + } +} diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java new file mode 100644 index 00000000..10397d13 --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java @@ -0,0 +1,419 @@ +package com.twelvemonkeys.util; + +import org.junit.Ignore; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class UUIDFactoryTest { + private static final String EXAMPLE_COM_UUID = "http://www.example.com/uuid/"; + + // Nil UUID + + @Test + public void testNilUUIDVariant() { + assertEquals(0, UUIDFactory.NIL.variant()); + } + @Test + public void testNilUUIDVersion() { + assertEquals(0, UUIDFactory.NIL.version()); + } + + @Test + public void testNilUUIDFromStringRep() { + assertEquals(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUIDFactory.NIL); + } + + @Test + public void testNilUUIDFromLong() { + assertEquals(new UUID(0l, 0l), UUIDFactory.NIL); + } + + // Version 3 UUIDs (for comparison with v5) + + @Test + public void testVersion3NameBasedMD5Variant() throws UnsupportedEncodingException { + assertEquals(2, UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")).variant()); + } + @Test + public void testVersion3NameBasedMD5Version() throws UnsupportedEncodingException { + assertEquals(3, UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")).version()); + } + + @Test + public void testVersion3NameBasedMD5Equals() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + UUID b = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertEquals(a, b); + } + + @Test + public void testVersion3NameBasedMD5NotEqualSHA1() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + UUID b = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertFalse(a.equals(b)); + } + + @Test + public void testVersion3NameBasedMD5FromStringRep() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertEquals(a, UUID.fromString(a.toString())); + } + + // Version 5 UUIDs + + @Test + public void testVersion5NameBasedSHA1Variant() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertEquals(2, a.variant()); + } + @Test + public void testVersion5NameBasedSHA1Version() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertEquals(5, a.version()); + } + + @Test + public void testVersion5NameBasedSHA1Equals() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + UUID b = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertEquals(a, b); + } + + @Test + public void testVersion5NameBasedSHA1Different() throws UnsupportedEncodingException { + Random random = new Random(); + byte[] data = new byte[128]; + random.nextBytes(data); + + UUID a = UUIDFactory.nameUUIDv5FromBytes(data); + + // Swap a random byte with its "opposite" + int i; + while (data[i = random.nextInt(data.length)] == data[data.length - 1 - i]) {} + data[i] = data[data.length - 1 - i]; + + UUID b = UUIDFactory.nameUUIDv5FromBytes(data); + + assertFalse(a.equals(b)); + } + + @Test + public void testVersion5NameBasedSHA1NotEqualMD5() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + UUID b = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertFalse(a.equals(b)); + } + + @Test + public void testVersion5NameBasedSHA1FromStringRep() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDv5FromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertEquals(a, UUID.fromString(a.toString())); + } + + // Version 1 UUIDs + + @Test + public void testVersion1NodeBasedVariant() { + assertEquals(2, UUIDFactory.timeNodeBasedUUID().variant()); + } + + @Test + public void testVersion1NodeBasedVersion() { + assertEquals(1, UUIDFactory.timeNodeBasedUUID().version()); + } + + @Test + public void testVersion1NodeBasedFromStringRep() { + UUID uuid = UUIDFactory.timeNodeBasedUUID(); + assertEquals(uuid, UUID.fromString(uuid.toString())); + } + + @Test + public void testVersion1NodeBasedMacAddress() { + UUID uuid = UUIDFactory.timeNodeBasedUUID(); + assertEquals(UUIDFactory.MAC_ADDRESS_NODE, uuid.node()); + // TODO: Test that this is actually a Mac address from the local computer, or specified through system property? + } + + @Test + public void testVersion1NodeBasedClockSeq() { + UUID uuid = UUIDFactory.timeNodeBasedUUID(); + assertEquals(UUIDFactory.Clock.getClockSequence(), uuid.clockSequence()); + + // Test time fields (within reasonable limits +/- 100 ms or so?) + assertEquals(UUIDFactory.Clock.currentTimeHundredNanos(), uuid.timestamp(), 1e6); + } + + @Test + public void testVersion1NodeBasedTimestamp() { + UUID uuid = UUIDFactory.timeNodeBasedUUID(); + // Test time fields (within reasonable limits +/- 100 ms or so?) + assertEquals(UUIDFactory.Clock.currentTimeHundredNanos(), uuid.timestamp(), 1e6); + } + + @Test + public void testVersion1NodeBasedUniMulticastBitUnset() { + // Do it a couple of times, to avoid accidentally have correct bit + for (int i = 0; i < 100; i++) { + UUID uuid = UUIDFactory.timeNodeBasedUUID(); + assertEquals(0, (uuid.node() >> 40) & 1); + } + } + + @Test + public void testVersion1NodeBasedUnique() { + for (int i = 0; i < 100; i++) { + UUID a = UUIDFactory.timeNodeBasedUUID(); + UUID b = UUIDFactory.timeNodeBasedUUID(); + assertFalse(a.equals(b)); + } + } + + @Test + public void testVersion1SecureRandomVariant() { + assertEquals(2, UUIDFactory.timeRandomBasedUUID().variant()); + } + + @Test + public void testVersion1SecureRandomVersion() { + assertEquals(1, UUIDFactory.timeRandomBasedUUID().version()); + } + + @Test + public void testVersion1SecureRandomFromStringRep() { + UUID uuid = UUIDFactory.timeRandomBasedUUID(); + assertEquals(uuid, UUID.fromString(uuid.toString())); + } + + @Test + public void testVersion1SecureRandomNode() { + UUID uuid = UUIDFactory.timeRandomBasedUUID(); + assertEquals(UUIDFactory.SECURE_RANDOM_NODE, uuid.node()); + } + + @Test + public void testVersion1SecureRandomClockSeq() { + UUID uuid = UUIDFactory.timeRandomBasedUUID(); + assertEquals(UUIDFactory.Clock.getClockSequence(), uuid.clockSequence()); + } + + @Test + public void testVersion1SecureRandomTimestamp() { + UUID uuid = UUIDFactory.timeRandomBasedUUID(); + + // Test time fields (within reasonable limits +/- 100 ms or so?) + assertEquals(UUIDFactory.Clock.currentTimeHundredNanos(), uuid.timestamp(), 1e6); + } + + @Test + public void testVersion1SecureRandomUniMulticastBit() { + // Do it a couple of times, to avoid accidentally have correct bit + for (int i = 0; i < 100; i++) { + UUID uuid = UUIDFactory.timeRandomBasedUUID(); + assertEquals(1, (uuid.node() >> 40) & 1); + } + } + + @Test + public void testVersion1SecureRandomUnique() { + for (int i = 0; i < 100; i++) { + UUID a = UUIDFactory.timeRandomBasedUUID(); + UUID b = UUIDFactory.timeRandomBasedUUID(); + assertFalse(a.equals(b)); + } + } + + // Clock tests + + @Test(timeout = 10000l) + public void testClock() throws InterruptedException { + final long[] times = new long[100000]; + + ExecutorService service = Executors.newFixedThreadPool(20); + for (int i = 0; i < times.length; i++) { + final int index = i; + + service.submit(new Runnable() { + public void run() { + times[index] = UUIDFactory.Clock.currentTimeHundredNanos(); + } + }); + } + + service.shutdown(); + assertTrue("Execution timed out", service.awaitTermination(10, TimeUnit.SECONDS)); + + Arrays.sort(times); // This is what really takes time... + + for (int i = 0, timesLength = times.length; i < timesLength; i++) { + if (i == 0) { + continue; + } + + assertFalse(String.format("times[%d] == times[%d]: 0x%016x", i - 1, i, times[i]), times[i - 1] == times[i]); + } + } + + @Test(timeout = 10000l) + public void testClockSkew() throws InterruptedException { + long clockSequence = UUIDFactory.Clock.getClockSequence(); + + ExecutorService service = Executors.newFixedThreadPool(10); + for (int i = 0; i < 100000; i++) { + service.submit(new Runnable() { + public void run() { + UUIDFactory.Clock.currentTimeHundredNanos(); + } + }); + } + + service.shutdown(); + assertTrue("Execution timed out", service.awaitTermination(10, TimeUnit.SECONDS)); + + assertEquals(clockSequence, UUIDFactory.Clock.getClockSequence(), 1); // Verify that clock skew doesn't happen "often" + } + + // Tests for node address system property + + @Test + public void testParseNodeAddressesSingle() { + long[] nodes = UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55"); + + assertEquals(1, nodes.length); + assertEquals(0x001122334455l, nodes[0]); + } + + @Test + public void testParseNodeAddressesSingleWhitespace() { + long[] nodes = UUIDFactory.parseMacAddressNodes(" 00:11:22:33:44:55\r\n"); + + assertEquals(1, nodes.length); + assertEquals(0x001122334455l, nodes[0]); + } + + @Test + public void testParseNodeAddressesMulti() { + long[] nodes = UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55, aa:bb:cc:dd:ee:ff, \n\t 0a-1b-2c-3d-4e-5f,"); + + assertEquals(3, nodes.length); + assertEquals(0x001122334455l, nodes[0]); + assertEquals(0xaabbccddeeffl, nodes[1]); + assertEquals(0x0a1b2c3d4e5fl, nodes[2]); + } + + @Test(expected = NullPointerException.class) + public void testParseNodeAddressesNull() { + UUIDFactory.parseMacAddressNodes(null); + } + + @Test(expected = NumberFormatException.class) + public void testParseNodeAddressesEmpty() { + UUIDFactory.parseMacAddressNodes(""); + } + + @Test(expected = NumberFormatException.class) + public void testParseNodeAddressesNonAddress() { + UUIDFactory.parseMacAddressNodes("127.0.0.1"); + } + + @Test(expected = NumberFormatException.class) + public void testParseNodeAddressesBadAddress() { + UUIDFactory.parseMacAddressNodes("00a:11:22:33:44:55"); + } + + @Test(expected = NumberFormatException.class) + public void testParseNodeAddressesBadAddress4() { + UUIDFactory.parseMacAddressNodes("00:11:22:33:44:550"); + } + + @Test(expected = NumberFormatException.class) + public void testParseNodeAddressesBadAddress2() { + UUIDFactory.parseMacAddressNodes("0x:11:22:33:44:55"); + } + + @Test(expected = NumberFormatException.class) + public void testParseNodeAddressesBadAddress3() { + UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55:99"); + } + + // Comparator test + + @Test + public void testComparator() { + UUID min = new UUID(0, 0); + // Long.MAX_VALUE and MIN_VALUE are really adjacent values when comparing unsigned... + UUID midLow = new UUID(Long.MAX_VALUE, Long.MAX_VALUE); + UUID midHigh = new UUID(Long.MIN_VALUE, Long.MIN_VALUE); + UUID max = new UUID(-1l, -1l); + + Comparator comparator = UUIDFactory.comparator(); + + assertEquals(0, comparator.compare(min, min)); + assertEquals(-1, comparator.compare(min, midLow)); + assertEquals(-1, comparator.compare(min, midHigh)); + assertEquals(-1, comparator.compare(min, max)); + + assertEquals(1, comparator.compare(midLow, min)); + assertEquals(0, comparator.compare(midLow, midLow)); + assertEquals(-1, comparator.compare(midLow, midHigh)); + assertEquals(-1, comparator.compare(midLow, max)); + + assertEquals(1, comparator.compare(midHigh, min)); + assertEquals(1, comparator.compare(midHigh, midLow)); + assertEquals(0, comparator.compare(midHigh, midHigh)); + assertEquals(-1, comparator.compare(midHigh, max)); + + assertEquals(1, comparator.compare(max, min)); + assertEquals(1, comparator.compare(max, midLow)); + assertEquals(1, comparator.compare(max, midHigh)); + assertEquals(0, comparator.compare(max, max)); + } + + @Test + public void testComparatorRandom() { + final Comparator comparator = UUIDFactory.comparator(); + + for (int i = 0; i < 10000; i++) { + UUID one = UUID.randomUUID(); + UUID two = UUID.randomUUID(); + + if (one.getMostSignificantBits() < 0 && two.getMostSignificantBits() >= 0 + || one.getMostSignificantBits() >= 0 && two.getMostSignificantBits() < 0 + || one.getLeastSignificantBits() < 0 && two.getLeastSignificantBits() >= 0 + || one.getLeastSignificantBits() >= 0 && two.getLeastSignificantBits() < 0) { + // These will differ due to the differing signs + assertEquals(-one.compareTo(two), comparator.compare(one, two)); + } + else { + assertEquals(one.compareTo(two), comparator.compare(one, two)); + } + } + } + + // Various testing + + @Ignore("Development testing only") + @Test + public void testOracleSYS_GUID() { + // TODO: Consider including this as a "fromCompactString" or similar... + String str = "AEB87F28E222D08AE043803BD559D08A"; + BigInteger bigInteger = new BigInteger(str, 16); // ALT: Create byte array of every 2 chars. + long msb = bigInteger.shiftRight(64).longValue(); + long lsb = bigInteger.longValue(); + UUID uuid = new UUID(msb, lsb); + System.err.println("uuid: " + uuid); + System.err.println("uuid.variant(): " + uuid.variant()); + System.err.println("uuid.version(): " + uuid.version()); + } +} \ No newline at end of file diff --git a/sandbox/sandbox-common/src/test/java/com/twlevemonkeys/image/MappedFileBufferTest.java b/sandbox/sandbox-common/src/test/java/com/twlevemonkeys/image/MappedFileBufferTest.java new file mode 100644 index 00000000..776a69d4 --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twlevemonkeys/image/MappedFileBufferTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twlevemonkeys.image; + +import com.twelvemonkeys.image.MappedFileBuffer; +import org.junit.Test; + +import java.awt.image.DataBuffer; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * MappedFileBufferTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: MappedFileBufferTest.java,v 1.0 01.06.12 14:23 haraldk Exp$ + */ +public class MappedFileBufferTest { + @Test(expected = IllegalArgumentException.class) + public void testCreateInvalidType() throws IOException { + MappedFileBuffer.create(-1, 1, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateInvalidSize() throws IOException { + MappedFileBuffer.create(DataBuffer.TYPE_USHORT, -1, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateInvalidBands() throws IOException { + MappedFileBuffer.create(DataBuffer.TYPE_BYTE, 1, -1); + } + + @Test + public void testCreateByte() throws IOException { + DataBuffer buffer = MappedFileBuffer.create(DataBuffer.TYPE_BYTE, 256, 3); + assertNotNull(buffer); + + assertEquals(DataBuffer.TYPE_BYTE, buffer.getDataType()); + assertEquals(256, buffer.getSize()); + assertEquals(3, buffer.getNumBanks()); + } + + @Test + public void testSetGetElemByte() throws IOException { + final int size = 256; + DataBuffer buffer = MappedFileBuffer.create(DataBuffer.TYPE_BYTE, size, 3); + assertNotNull(buffer); + + for (int b = 0; b < 3; b++) { + for (int i = 0; i < size; i++) { + buffer.setElem(b, i, i); + + assertEquals(i, buffer.getElem(b, i)); + } + } + } + + @Test + public void testCreateUShort() throws IOException { + DataBuffer buffer = MappedFileBuffer.create(DataBuffer.TYPE_USHORT, 256, 3); + assertNotNull(buffer); + + assertEquals(DataBuffer.TYPE_USHORT, buffer.getDataType()); + assertEquals(256, buffer.getSize()); + assertEquals(3, buffer.getNumBanks()); + } + + @Test + public void testSetGetElemUShort() throws IOException { + final int size = (Short.MAX_VALUE + 1) * 2; + DataBuffer buffer = MappedFileBuffer.create(DataBuffer.TYPE_USHORT, size, 3); + assertNotNull(buffer); + + for (int b = 0; b < 3; b++) { + for (int i = 0; i < size; i++) { + buffer.setElem(b, i, i); + + assertEquals(i, buffer.getElem(b, i)); + } + } + } + + @Test + public void testCreateInt() throws IOException { + DataBuffer buffer = MappedFileBuffer.create(DataBuffer.TYPE_INT, 256, 3); + assertNotNull(buffer); + + assertEquals(DataBuffer.TYPE_INT, buffer.getDataType()); + assertEquals(256, buffer.getSize()); + assertEquals(3, buffer.getNumBanks()); + } + + @Test + public void testSetGetElemInt() throws IOException { + final int size = (Short.MAX_VALUE + 1) * 2; + DataBuffer buffer = MappedFileBuffer.create(DataBuffer.TYPE_INT, size, 3); + assertNotNull(buffer); + + for (int b = 0; b < 3; b++) { + for (int i = 0; i < size; i++) { + buffer.setElem(b, i, i * i); + + assertEquals(i * i, buffer.getElem(b, i)); + } + } + } +} diff --git a/sandbox/sandbox-imageio/pom.xml b/sandbox/sandbox-imageio/pom.xml new file mode 100644 index 00000000..c574bd69 --- /dev/null +++ b/sandbox/sandbox-imageio/pom.xml @@ -0,0 +1,80 @@ + + + + + 4.0.0 + + com.twelvemonkeys.sandbox + sandbox + 3.0-SNAPSHOT + + sandbox-imageio + jar + TwelveMonkeys :: Sandbox :: ImageIO + + The TwelveMonkeys ImageIO Sandbox. Experimental stuff. Old retired stuff. + + + + + com.twelvemonkeys.common + common-io + compile + + + + com.twelvemonkeys.common + common-image + compile + + + + com.twelvemonkeys.imageio + imageio-core + compile + + + + com.twelvemonkeys.common + common-io + test + tests + + + + com.twelvemonkeys.common + common-lang + test + tests + + + + diff --git a/sandbox/sandbox-servlet/pom.xml b/sandbox/sandbox-servlet/pom.xml new file mode 100644 index 00000000..87077d1c --- /dev/null +++ b/sandbox/sandbox-servlet/pom.xml @@ -0,0 +1,90 @@ + + + + + 4.0.0 + + com.twelvemonkeys.sandbox + sandbox + 3.0-SNAPSHOT + + sandbox-servlet + jar + TwelveMonkeys :: Sandbox :: Servlet + + The TwelveMonkeys Servlet Sandbox. Experimental stuff. Old stuff that didn't cut it. + + + + + com.twelvemonkeys.common + common-lang + compile + + + com.twelvemonkeys.common + common-io + compile + + + com.twelvemonkeys.common + common-image + compile + + + com.twelvemonkeys.servlet + servlet + compile + + + + + javax.servlet + servlet-api + 2.4 + provided + + + + javax.servlet + jsp-api + 2.0 + provided + + + + log4j + log4j + 1.2.14 + provided + + + diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java similarity index 88% rename from servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java index ebe41d97..1bfda5ff 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java @@ -28,7 +28,6 @@ package com.twelvemonkeys.servlet.image; -import com.twelvemonkeys.lang.MathUtil; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.servlet.ServletUtil; @@ -144,11 +143,11 @@ import java.awt.geom.Rectangle2D; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java#2 $ + * @version $Id: TextRenderer.java#2 $ */ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { - // TODO: Create something useable out of this piece of old junk.. ;-) + // TODO: Create something usable out of this piece of old junk.. ;-) // It just needs a graphics object to write onto // Alternatively, defer, and compute the size needed // Or, make it a filter... @@ -195,8 +194,7 @@ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { /** * Renders the text string for this servlet request. */ - private void paint(ServletRequest pReq, Graphics2D pRes, - int pWidth, int pHeight) + private void paint(ServletRequest pReq, Graphics2D pRes, int pWidth, int pHeight) throws ImageServletException { // Get parameters @@ -227,10 +225,11 @@ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { } // Create and set font - Font font = new Font((fontFamily != null ? fontFamily : "Helvetica"), - getFontStyle(fontStyle), - (fontSize != null ? Integer.parseInt(fontSize) - : 12)); + Font font = new Font( + fontFamily != null ? fontFamily : "Helvetica", + getFontStyle(fontStyle), + fontSize != null ? Integer.parseInt(fontSize) : 12 + ); pRes.setFont(font); // Set rotation @@ -238,18 +237,15 @@ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { pRes.rotate(angle, pWidth / 2.0, pHeight / 2.0); // Draw string in fgcolor - pRes.setColor(fgcolor != null ? StringUtil.toColor(fgcolor) - : Color.black); + pRes.setColor(fgcolor != null ? StringUtil.toColor(fgcolor) : Color.black); - float x = ServletUtil.getFloatParameter(pReq, PARAM_MARGIN_LEFT, - Float.MIN_VALUE); + float x = ServletUtil.getFloatParameter(pReq, PARAM_MARGIN_LEFT, Float.MIN_VALUE); Rectangle2D[] bounds = new Rectangle2D[lines.length]; if (x <= Float.MIN_VALUE) { // Center float longest = 0f; for (int i = 0; i < lines.length; i++) { - bounds[i] = font.getStringBounds(lines[i], - pRes.getFontRenderContext()); + bounds[i] = font.getStringBounds(lines[i], pRes.getFontRenderContext()); if (bounds[i].getWidth() > longest) { longest = (float) bounds[i].getWidth(); } @@ -264,11 +260,9 @@ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { //System.out.println("marginLeft (from param): " + x); //} - float y = ServletUtil.getFloatParameter(pReq, PARAM_MARGIN_TOP, - Float.MIN_VALUE); - float lineHeight = (float) (bounds[0] != null ? bounds[0].getHeight() : - font.getStringBounds(lines[0], - pRes.getFontRenderContext()).getHeight()); + float y = ServletUtil.getFloatParameter(pReq, PARAM_MARGIN_TOP, Float.MIN_VALUE); + float lineHeight = (float) (bounds[0] != null ? + bounds[0].getHeight() : font.getStringBounds(lines[0], pRes.getFontRenderContext()).getHeight()); if (y <= Float.MIN_VALUE) { // Center @@ -305,8 +299,7 @@ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { * @see Font#ITALIC */ private int getFontStyle(String pStyle) { - if (pStyle == null - || StringUtil.containsIgnoreCase(pStyle, FONT_STYLE_PLAIN)) { + if (pStyle == null || StringUtil.containsIgnoreCase(pStyle, FONT_STYLE_PLAIN)) { return Font.PLAIN; } @@ -330,19 +323,14 @@ class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { */ private double getAngle(ServletRequest pRequest) { // Get angle - double angle = - ServletUtil.getDoubleParameter(pRequest, PARAM_TEXT_ROTATION, 0.0); + double angle = ServletUtil.getDoubleParameter(pRequest, PARAM_TEXT_ROTATION, 0.0); // Convert to radians, if needed String units = pRequest.getParameter(PARAM_TEXT_ROTATION_UNITS); - if (!StringUtil.isEmpty(units) - && ROTATION_DEGREES.equalsIgnoreCase(units)) { - angle = MathUtil.toRadians(angle); + if (!StringUtil.isEmpty(units) && ROTATION_DEGREES.equalsIgnoreCase(units)) { + angle = Math.toRadians(angle); } return angle; } - -} - - +} \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java similarity index 74% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java index 5f30f57f..fb068c41 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java @@ -17,13 +17,14 @@ package com.twelvemonkeys.servlet.jsp.droplet; -import java.io.*; +import com.twelvemonkeys.servlet.jsp.droplet.taglib.IncludeTag; -import javax.servlet.*; -import javax.servlet.http.*; -import javax.servlet.jsp.*; - -import com.twelvemonkeys.servlet.jsp.droplet.taglib.*; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.PageContext; +import java.io.IOException; /** * Dynamo Droplet like Servlet. @@ -36,7 +37,6 @@ import com.twelvemonkeys.servlet.jsp.droplet.taglib.*; */ public abstract class Droplet extends HttpServlet implements JspFragment { - // Copy doc public abstract void service(PageContext pPageContext) throws ServletException, IOException; @@ -44,8 +44,7 @@ public abstract class Droplet extends HttpServlet implements JspFragment { * Services a parameter. Programatically equivalent to the * JSP tag. */ - public void serviceParameter(String pParameter, PageContext pPageContext) - throws ServletException, IOException { + public void serviceParameter(String pParameter, PageContext pPageContext) throws ServletException, IOException { Object param = pPageContext.getRequest().getAttribute(pParameter); if (param != null) { @@ -68,11 +67,8 @@ public abstract class Droplet extends HttpServlet implements JspFragment { /** * "There's no need to override this method." :-) */ - final public void service(HttpServletRequest pRequest, - HttpServletResponse pResponse) - throws ServletException, IOException { - PageContext pageContext = - (PageContext) pRequest.getAttribute(IncludeTag.PAGE_CONTEXT); + final public void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException { + PageContext pageContext = (PageContext) pRequest.getAttribute(IncludeTag.PAGE_CONTEXT); // TODO: What if pageContext == null service(pageContext); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java similarity index 83% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java index acdf86e7..344dab31 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java @@ -14,10 +14,9 @@ package com.twelvemonkeys.servlet.jsp.droplet; -import java.io.*; - -import javax.servlet.*; -import javax.servlet.jsp.*; +import javax.servlet.ServletException; +import javax.servlet.jsp.PageContext; +import java.io.IOException; /** * Interface for JSP sub pages or page fragments to implement. @@ -39,6 +38,5 @@ public interface JspFragment { * subpage's normal operation * @throws IOException if an input or output exception occurs */ - public void service(PageContext pContext) - throws ServletException, IOException; + public void service(PageContext pContext) throws ServletException, IOException; } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java similarity index 65% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java index e87f79ea..dfc49f89 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java @@ -1,10 +1,8 @@ - package com.twelvemonkeys.servlet.jsp.droplet; -import java.io.*; - -import javax.servlet.*; -import javax.servlet.jsp.*; +import javax.servlet.ServletException; +import javax.servlet.jsp.PageContext; +import java.io.IOException; /** * Oparam (Open parameter) @@ -19,11 +17,10 @@ public class Oparam extends Param implements JspFragment { super(pValue); } - public void service(PageContext pContext) - throws ServletException, IOException { - pContext.getServletContext().log("Service subpage " + pContext.getServletContext().getRealPath(mValue)); + public void service(PageContext pContext) throws ServletException, IOException { + pContext.getServletContext().log("Service subpage " + pContext.getServletContext().getRealPath(value)); - pContext.include(mValue); + pContext.include(value); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java similarity index 72% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java index 6517de11..bfee7a5e 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java @@ -1,10 +1,9 @@ - package com.twelvemonkeys.servlet.jsp.droplet; -import java.io.*; - -import javax.servlet.*; -import javax.servlet.jsp.*; +import javax.servlet.ServletException; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.PageContext; +import java.io.IOException; /** * Param @@ -12,7 +11,7 @@ import javax.servlet.jsp.*; public class Param implements JspFragment { /** The value member field. */ - protected String mValue = null; + protected String value = null; /** * Creates a Param. @@ -20,14 +19,14 @@ public class Param implements JspFragment { * @param pValue the value of the parameter */ public Param(String pValue) { - mValue = pValue; + value = pValue; } /** * Gets the value of the parameter. */ public String getValue() { - return mValue; + return value; } /** @@ -37,6 +36,6 @@ public class Param implements JspFragment { public void service(PageContext pContext) throws ServletException, IOException { JspWriter writer = pContext.getOut(); - writer.print(mValue); + writer.print(value); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package_info.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package-info.java similarity index 87% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package_info.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package-info.java index 69dfcbc8..0f10f187 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package_info.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package-info.java @@ -5,6 +5,5 @@ * Read: The interfaces and classes in this package (and subpackages) will be * developed and modified for a while. * - * TODO: Insert taglib-descriptor here? */ package com.twelvemonkeys.servlet.jsp.droplet; \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java similarity index 86% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java index 5fce6abf..a9c097d1 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java @@ -41,20 +41,20 @@ public class IncludeTag extends ExTagSupport { * This will contain the names of all the parameters that have been * added to the PageContext.REQUEST_SCOPE scope by this tag. */ - private ArrayList mParameterNames = null; + private ArrayList parameterNames = null; /** * If any of the parameters we insert for this tag already exist, then * we back up the older parameter in this {@code HashMap} and * restore them when the tag is finished. */ - private HashMap mOldParameters = null; + private HashMap oldParameters = null; /** * This is the URL for the JSP page that the parameters contained in this * tag are to be inserted into. */ - private String mPage; + private String page; /** * The name of the PageContext attribute @@ -68,7 +68,7 @@ public class IncludeTag extends ExTagSupport { * @param pPage The URL for the JSP page to insert parameters into. */ public void setPage(String pPage) { - mPage = pPage; + page = pPage; } /** @@ -85,13 +85,13 @@ public class IncludeTag extends ExTagSupport { */ public void addParameter(String pName, Object pValue) { // Check that we haven't already saved this parameter - if (!mParameterNames.contains(pName)) { - mParameterNames.add(pName); + if (!parameterNames.contains(pName)) { + parameterNames.add(pName); // Now check if this parameter already exists in the page. Object obj = getRequest().getAttribute(pName); if (obj != null) { - mOldParameters.put(pName, obj); + oldParameters.put(pName, obj); } } @@ -110,8 +110,8 @@ public class IncludeTag extends ExTagSupport { * @exception JspException */ public int doStartTag() throws JspException { - mOldParameters = new HashMap(); - mParameterNames = new ArrayList(); + oldParameters = new HashMap(); + parameterNames = new ArrayList(); return EVAL_BODY_INCLUDE; } @@ -129,44 +129,44 @@ public class IncludeTag extends ExTagSupport { String msg; try { - Iterator iterator; + Iterator iterator; String parameterName; // -- Harald K 20020726 // Include the page, in place //getDispatcher().include(getRequest(), getResponse()); addParameter(PAGE_CONTEXT, pageContext); // Will be cleared later - pageContext.include(mPage); + pageContext.include(page); // Remove all the parameters that were added to the request scope // for this insert tag. - iterator = mParameterNames.iterator(); + iterator = parameterNames.iterator(); while (iterator.hasNext()) { - parameterName = (String) iterator.next(); + parameterName = iterator.next(); getRequest().removeAttribute(parameterName); } - iterator = mOldParameters.keySet().iterator(); + iterator = oldParameters.keySet().iterator(); // Restore the parameters we temporarily replaced (if any). while (iterator.hasNext()) { - parameterName = (String) iterator.next(); + parameterName = iterator.next(); - getRequest().setAttribute(parameterName, mOldParameters.get(parameterName)); + getRequest().setAttribute(parameterName, oldParameters.get(parameterName)); } return super.doEndTag(); } catch (IOException ioe) { - msg = "Caught an IOException while including " + mPage + msg = "Caught an IOException while including " + page + "\n" + ioe.toString(); log(msg, ioe); throw new JspException(msg); } catch (ServletException se) { - msg = "Caught a ServletException while including " + mPage + msg = "Caught a ServletException while including " + page + "\n" + se.toString(); log(msg, se); throw new JspException(msg); @@ -177,8 +177,8 @@ public class IncludeTag extends ExTagSupport { * Free up the member variables that we've used throughout this tag. */ protected void clearServiceState() { - mOldParameters = null; - mParameterNames = null; + oldParameters = null; + parameterNames = null; } /** @@ -190,7 +190,7 @@ public class IncludeTag extends ExTagSupport { */ /* private RequestDispatcher getDispatcher() { - return getRequest().getRequestDispatcher(mPage); + return getRequest().getRequestDispatcher(page); } */ diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java similarity index 58% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java index ef472a9b..8382464b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java @@ -41,27 +41,26 @@ import org.xml.sax.helpers.DefaultHandler; * * @version $Revision: #1 $, ($Date: 2008/05/05 $) */ - public class NestingHandler extends DefaultHandler { - private String mIncludeTagName = "include"; - private String mParamTagName = "param"; - private String mOpenParamTagName = "oparam"; + private String includeTagName = "include"; + private String paramTagName = "param"; + private String openParamTagName = "oparam"; //private Stack mParents = new Stack(); - private boolean mInIncludeTag = false; + private boolean inIncludeTag = false; - private String mNamespacePrefix = null; - private String mNamespaceURI = null; + private String namespacePrefix = null; + private String namespaceURI = null; - private NestingValidator mValidator = null; + private NestingValidator validator = null; public NestingHandler(String pNamespacePrefix, String pNameSpaceURI, NestingValidator pValidator) { - mNamespacePrefix = pNamespacePrefix; - mNamespaceURI = pNameSpaceURI; + namespacePrefix = pNamespacePrefix; + namespaceURI = pNameSpaceURI; - mValidator = pValidator; + validator = pValidator; } public void startElement(String pNamespaceURI, String pLocalName, @@ -74,7 +73,7 @@ public class NestingHandler extends DefaultHandler { String localName = !StringUtil.isEmpty(pLocalName) ? pLocalName : getLocalName(pQualifiedName); /* - if (namespacePrefix.equals(mNamespacePrefix)) { + if (namespacePrefix.equals(namespacePrefix)) { System.out.println("startElement:\nnamespaceURI=" + pNamespaceURI + " namespacePrefix=" + namespacePrefix + " localName=" + localName @@ -82,48 +81,48 @@ public class NestingHandler extends DefaultHandler { + " attributes=" + pAttributes); } */ - if (localName.equals(mIncludeTagName)) { + if (localName.equals(includeTagName)) { // include - //System.out.println("<" + mNamespacePrefix + ":" - // + mIncludeTagName + ">"); - if (mInIncludeTag) { - mValidator.reportError("Cannot nest " + namespacePrefix + ":" - + mIncludeTagName); + //System.out.println("<" + namespacePrefix + ":" + // + includeTagName + ">"); + if (inIncludeTag) { + validator.reportError("Cannot nest " + namespacePrefix + ":" + + includeTagName); } - mInIncludeTag = true; + inIncludeTag = true; } - else if (localName.equals(mParamTagName)) { + else if (localName.equals(paramTagName)) { // param - //System.out.println("<" + mNamespacePrefix + ":" - // + mParamTagName + "/>"); - if (!mInIncludeTag) { - mValidator.reportError(mNamespacePrefix + ":" - + mParamTagName + //System.out.println("<" + namespacePrefix + ":" + // + paramTagName + "/>"); + if (!inIncludeTag) { + validator.reportError(this.namespacePrefix + ":" + + paramTagName + " can only appear within " - + mNamespacePrefix + ":" - + mIncludeTagName); + + this.namespacePrefix + ":" + + includeTagName); } } - else if (localName.equals(mOpenParamTagName)) { + else if (localName.equals(openParamTagName)) { // oparam - //System.out.println("<" + mNamespacePrefix + ":" - // + mOpenParamTagName + ">"); - if (!mInIncludeTag) { - mValidator.reportError(mNamespacePrefix + ":" - + mOpenParamTagName + //System.out.println("<" + namespacePrefix + ":" + // + openParamTagName + ">"); + if (!inIncludeTag) { + validator.reportError(this.namespacePrefix + ":" + + openParamTagName + " can only appear within " - + mNamespacePrefix + ":" - + mIncludeTagName); + + this.namespacePrefix + ":" + + includeTagName); } - mInIncludeTag = false; + inIncludeTag = false; } else { // Only jsp:text allowed inside include! - if (mInIncludeTag && !localName.equals("text")) { - mValidator.reportError(namespacePrefix + ":" + localName + if (inIncludeTag && !localName.equals("text")) { + validator.reportError(namespacePrefix + ":" + localName + " can not appear within " - + mNamespacePrefix + ":" - + mIncludeTagName); + + this.namespacePrefix + ":" + + includeTagName); } } } @@ -139,28 +138,28 @@ public class NestingHandler extends DefaultHandler { String localName = !StringUtil.isEmpty(pLocalName) ? pLocalName : getLocalName(pQualifiedName); /* - if (namespacePrefix.equals(mNamespacePrefix)) { + if (namespacePrefix.equals(namespacePrefix)) { System.out.println("endElement:\nnamespaceURI=" + pNamespaceURI + " namespacePrefix=" + namespacePrefix + " localName=" + localName + " qName=" + pQualifiedName); } */ - if (namespacePrefix.equals(mNamespacePrefix) - && localName.equals(mIncludeTagName)) { + if (namespacePrefix.equals(this.namespacePrefix) + && localName.equals(includeTagName)) { - //System.out.println(""); + //System.out.println(""); - mInIncludeTag = false; + inIncludeTag = false; } - else if (namespacePrefix.equals(mNamespacePrefix) - && localName.equals(mOpenParamTagName)) { + else if (namespacePrefix.equals(this.namespacePrefix) + && localName.equals(openParamTagName)) { - //System.out.println(""); + //System.out.println(""); - mInIncludeTag = true; // assuming no errors before this... + inIncludeTag = true; // assuming no errors before this... } } @@ -169,8 +168,8 @@ public class NestingHandler extends DefaultHandler { */ private String getNSPrefixFromURI(String pNamespaceURI) { - return (pNamespaceURI.equals(mNamespaceURI) - ? mNamespacePrefix : ""); + return (pNamespaceURI.equals(namespaceURI) + ? namespacePrefix : ""); } private String getNamespacePrefix(String pQualifiedName) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java similarity index 80% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java index 37080286..6c2b2708 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java @@ -20,16 +20,16 @@ package com.twelvemonkeys.servlet.jsp.droplet.taglib; +import org.xml.sax.InputSource; +import org.xml.sax.helpers.DefaultHandler; -import java.util.*; - -import javax.servlet.jsp.tagext.*; -import javax.xml.parsers.*; - -import org.xml.sax.*; -import org.xml.sax.helpers.*; - -import com.twelvemonkeys.util.*; +import javax.servlet.jsp.tagext.PageData; +import javax.servlet.jsp.tagext.TagLibraryValidator; +import javax.servlet.jsp.tagext.ValidationMessage; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.util.ArrayList; +import java.util.List; /** * A validator that verifies that tags follow @@ -47,18 +47,14 @@ import com.twelvemonkeys.util.*; * @version $Revision: #1 $, ($Date: 2008/05/05 $) * */ - public class NestingValidator extends TagLibraryValidator { - private Vector errors = new Vector(); + private List errors = new ArrayList(); /** * */ - - public ValidationMessage[] validate(String pPrefix, - String pURI, - PageData pPage) { + public ValidationMessage[] validate(String pPrefix, String pURI, PageData pPage) { //System.out.println("Validating " + pPrefix + " (" + pURI + ") for " // + pPage + "."); @@ -88,14 +84,12 @@ public class NestingValidator extends TagLibraryValidator { } // Return any errors and exceptions, empty array means okay - return (ValidationMessage[]) - errors.toArray(new ValidationMessage[errors.size()]); + return errors.toArray(new ValidationMessage[errors.size()]); } /** * Callback method for the handler to report errors */ - public void reportError(String pMessage) { // The first argument to the ValidationMessage // constructor can be a tag ID. Since tag IDs diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java similarity index 76% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java index 98ca02db..08a42ae5 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java @@ -27,73 +27,63 @@ import com.twelvemonkeys.servlet.jsp.taglib.BodyReaderTag; import javax.servlet.http.HttpServletRequest; import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; -import javax.servlet.jsp.tagext.Tag; import java.io.File; import java.io.IOException; - /** * Open parameter tag that emulates ATG Dynamo JHTML behaviour for JSP. * * @author Thomas Purcell (CSC Australia) * @author Harald Kuhr * @author last modified by $Author: haku $ - * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java#1 $ + * @version $Id: jsp/droplet/taglib/OparamTag.java#1 $ */ - public class OparamTag extends BodyReaderTag { protected final static String COUNTER = "com.twelvemonkeys.servlet.jsp.taglib.OparamTag.counter"; - - private File mSubpage = null; + private File subpage = null; /** * This is the name of the parameter to be inserted into the {@code * PageContext.REQUEST_SCOPE} scope. */ + private String parameterName = null; - private String mParameterName = null; + private String language = null; - private String mLanguage = null; - - private String mPrefix = null; + private String prefix = null; /** * This method allows the JSP page to set the name for the parameter by * using the {@code name} tag attribute. * * @param pName The name for the parameter to insert into the {@code - * PageContext.REQUEST_SCOPE} scope. + * PageContext.REQUEST_SCOPE} scope. */ - public void setName(String pName) { - mParameterName = pName; + parameterName = pName; } public void setLanguage(String pLanguage) { //System.out.println("setLanguage:"+pLanguage); - mLanguage = pLanguage; + language = pLanguage; } public void setPrefix(String pPrefix) { //System.out.println("setPrefix:"+pPrefix); - mPrefix = pPrefix; + prefix = pPrefix; } /** - * Ensure that the tag implemented by this class is enclosed by an {@code - * IncludeTag}. If the tag is not enclosed by an - * {@code IncludeTag} then a {@code JspException} is thrown. + * Ensure that the tag implemented by this class is enclosed by an {@code IncludeTag}. + * If the tag is not enclosed by an {@code IncludeTag} then a {@code JspException} is thrown. * - * @return If this tag is enclosed within an {@code IncludeTag}, then - * the default return value from this method is the {@code - * TagSupport.EVAL_BODY_TAG} value. - * @exception JspException + * @return If this tag is enclosed within an {@code IncludeTag}, then the default return value + * from this method is the {@code TagSupport.EVAL_BODY_TAG} value. + * + * @throws JspException */ - public int doStartTag() throws JspException { //checkEnclosedInIncludeTag(); // Moved to TagLibValidator @@ -101,22 +91,21 @@ public class OparamTag extends BodyReaderTag { HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); // Get filename - mSubpage = createFileNameFromRequest(request); + subpage = createFileNameFromRequest(request); // Get include tag, and add to parameters IncludeTag includeTag = (IncludeTag) getParent(); - includeTag.addParameter(mParameterName, new Oparam(mSubpage.getName())); + includeTag.addParameter(parameterName, new Oparam(subpage.getName())); // if ! subpage.exist || jsp newer than subpage, write new - File jsp = new File(pageContext.getServletContext() - .getRealPath(request.getServletPath())); + File jsp = new File(pageContext.getServletContext().getRealPath(request.getServletPath())); - if (!mSubpage.exists() || jsp.lastModified() > mSubpage.lastModified()) { - return BodyTag.EVAL_BODY_BUFFERED; + if (!subpage.exists() || jsp.lastModified() > subpage.lastModified()) { + return EVAL_BODY_BUFFERED; } // No need to evaluate body again! - return Tag.SKIP_BODY; + return SKIP_BODY; } /** @@ -148,9 +137,8 @@ public class OparamTag extends BodyReaderTag { * life cycle just in case a JspException was thrown during the tag * execution. */ - protected void clearServiceState() { - mParameterName = null; + parameterName = null; } /** @@ -160,36 +148,31 @@ public class OparamTag extends BodyReaderTag { * into the session scope then a {@code JspException} will be thrown. * * @param pContent The body of the tag as a String. - * - * @exception JspException + * @throws JspException */ - protected void processBody(String pContent) throws JspException { // Okay, we have the content, we need to write it to disk somewhere String content = pContent; - if (!StringUtil.isEmpty(mLanguage)) { - content = "<%@page language=\"" + mLanguage + "\" %>" + content; + if (!StringUtil.isEmpty(language)) { + content = "<%@page language=\"" + language + "\" %>" + content; } - if (!StringUtil.isEmpty(mPrefix)) { - content = "<%@taglib uri=\"/twelvemonkeys-common\" prefix=\"" + mPrefix + "\" %>" + content; + if (!StringUtil.isEmpty(prefix)) { + content = "<%@taglib uri=\"/twelvemonkeys-common\" prefix=\"" + prefix + "\" %>" + content; } // Write the content of the oparam to disk try { - log("Processing subpage " + mSubpage.getPath()); - FileUtil.write(mSubpage, content.getBytes()); - + log("Processing subpage " + subpage.getPath()); + FileUtil.write(subpage, content.getBytes()); } catch (IOException ioe) { throw new JspException(ioe); } } - /** - * Creates a unique filename for each (nested) oparam - */ + /** Creates a unique filename for each (nested) oparam */ private File createFileNameFromRequest(HttpServletRequest pRequest) { //System.out.println("ServletPath" + pRequest.getServletPath()); String path = pRequest.getServletPath(); @@ -203,7 +186,7 @@ public class OparamTag extends BodyReaderTag { // Replace special chars in name with '_' name = name.replace('.', '_'); - String param = mParameterName.replace('.', '_'); + String param = parameterName.replace('.', '_'); param = param.replace('/', '_'); param = param.replace('\\', '_'); param = param.replace(':', '_'); @@ -218,21 +201,20 @@ public class OparamTag extends BodyReaderTag { return new File(new File(pageContext.getServletContext().getRealPath(path)), name + "_oparam_" + count + "_" + param + ".jsp"); } - /** - * Gets the current oparam count for this request - */ + /** Gets the current oparam count for this request */ private int getOparamCountFromRequest(HttpServletRequest pRequest) { // Use request.attribute for incrementing oparam counter Integer count = (Integer) pRequest.getAttribute(COUNTER); - if (count == null) + if (count == null) { count = new Integer(0); - else + } + else { count = new Integer(count.intValue() + 1); + } // ... and set it back pRequest.setAttribute(COUNTER, count); return count.intValue(); } - } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java similarity index 87% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java index 047ab084..a08e73e5 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java @@ -14,14 +14,10 @@ package com.twelvemonkeys.servlet.jsp.droplet.taglib; -import java.io.IOException; +import com.twelvemonkeys.servlet.jsp.droplet.Param; +import com.twelvemonkeys.servlet.jsp.taglib.ExTagSupport; -import javax.servlet.*; -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; - -import com.twelvemonkeys.servlet.jsp.droplet.*; -import com.twelvemonkeys.servlet.jsp.taglib.*; +import javax.servlet.jsp.JspException; /** * Parameter tag that emulates ATG Dynamo JHTML behaviour for JSP. @@ -33,22 +29,19 @@ import com.twelvemonkeys.servlet.jsp.taglib.*; * @version $Revision: #1 $, ($Date: 2008/05/05 $) * */ - public class ParamTag extends ExTagSupport { /** * This is the name of the parameter to be inserted into the {@code * PageContext.REQUEST_SCOPE} scope. */ - - private String mParameterName; + private String parameterName; /** * This is the value for the parameter to be inserted into the {@code * PageContext.REQUEST_SCOPE} scope. */ - - private Object mParameterValue; + private Object parameterValue; /** * This method allows the JSP page to set the name for the parameter by @@ -57,9 +50,8 @@ public class ParamTag extends ExTagSupport { * @param pName The name for the parameter to insert into the {@code * PageContext.REQUEST_SCOPE} scope. */ - public void setName(String pName) { - mParameterName = pName; + parameterName = pName; } /** @@ -69,9 +61,8 @@ public class ParamTag extends ExTagSupport { * @param pValue The value for the parameter to insert into the * PageContext.REQUEST_SCOPE scope. */ - public void setValue(String pValue) { - mParameterValue = new Param(pValue); + parameterValue = new Param(pValue); } /** @@ -84,7 +75,6 @@ public class ParamTag extends ExTagSupport { * TagSupport.SKIP_BODY} value. * @exception JspException */ - public int doStartTag() throws JspException { //checkEnclosedInIncludeTag(); @@ -118,11 +108,10 @@ public class ParamTag extends ExTagSupport { * This method adds the parameter whose name and value were passed to this * object via the tag attributes to the parent {@code Include} tag. */ - private void addParameter() { IncludeTag includeTag = (IncludeTag) getParent(); - includeTag.addParameter(mParameterName, mParameterValue); + includeTag.addParameter(parameterName, parameterValue); } /** @@ -133,9 +122,8 @@ public class ParamTag extends ExTagSupport { * life cycle just in case a JspException was thrown during the tag * execution. */ - protected void clearServiceState() { - mParameterName = null; - mParameterValue = null; + parameterName = null; + parameterValue = null; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java similarity index 83% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java index 9ecafd8d..d7a658fe 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java @@ -17,10 +17,8 @@ package com.twelvemonkeys.servlet.jsp.droplet.taglib; -import java.io.IOException; - -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; +import javax.servlet.jsp.tagext.TagData; +import javax.servlet.jsp.tagext.TagExtraInfo; /** * TagExtraInfo for ValueOf. @@ -39,8 +37,7 @@ public class ValueOfTEI extends TagExtraInfo { Object nameAttr = pTagData.getAttribute("name"); Object paramAttr = pTagData.getAttribute("param"); - if ((nameAttr != null && paramAttr == null) || - (nameAttr == null && paramAttr != null)) { + if ((nameAttr != null && paramAttr == null) || (nameAttr == null && paramAttr != null)) { return true; // Exactly one of name or param set } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java similarity index 80% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java index 180abe75..7989b082 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java @@ -14,13 +14,14 @@ package com.twelvemonkeys.servlet.jsp.droplet.taglib; -import java.io.*; +import com.twelvemonkeys.servlet.jsp.droplet.JspFragment; +import com.twelvemonkeys.servlet.jsp.taglib.ExTagSupport; -import javax.servlet.*; -import javax.servlet.jsp.*; - -import com.twelvemonkeys.servlet.jsp.droplet.*; -import com.twelvemonkeys.servlet.jsp.taglib.*; +import javax.servlet.ServletException; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.PageContext; +import java.io.IOException; /** * ValueOf tag that emulates ATG Dynamo JHTML behaviour for JSP. @@ -38,14 +39,14 @@ public class ValueOfTag extends ExTagSupport { * the current JSP page. This value will be set via the {@code name} * attribute. */ - private String mParameterName; + private String parameterName; /** * This is the value of the parameter read from the {@code * PageContext.REQUEST_SCOPE} scope. If the parameter doesn't exist, * then this will be null. */ - private Object mParameterValue; + private Object parameterValue; /** * This method is called as part of the initialisation phase of the tag @@ -56,7 +57,7 @@ public class ValueOfTag extends ExTagSupport { * PageContext.REQUEST_SCOPE} scope. */ public void setName(String pName) { - mParameterName = pName; + parameterName = pName; } /** @@ -69,7 +70,7 @@ public class ValueOfTag extends ExTagSupport { * PageContext.REQUEST_SCOPE} scope. */ public void setParam(String pName) { - mParameterName = pName; + parameterName = pName; } /** @@ -88,19 +89,19 @@ public class ValueOfTag extends ExTagSupport { public int doStartTag() throws JspException { try { if (parameterExists()) { - if (mParameterValue instanceof JspFragment) { + if (parameterValue instanceof JspFragment) { // OPARAM or PARAM - ((JspFragment) mParameterValue).service(pageContext); + ((JspFragment) parameterValue).service(pageContext); /* - log("Service subpage " + pageContext.getServletContext().getRealPath(((Oparam) mParameterValue).getName())); + log("Service subpage " + pageContext.getServletContext().getRealPath(((Oparam) parameterValue).getName())); - pageContext.include(((Oparam) mParameterValue).getName()); + pageContext.include(((Oparam) parameterValue).getName()); */ } else { // Normal JSP parameter value JspWriter writer = pageContext.getOut(); - writer.print(mParameterValue); + writer.print(parameterValue); } return SKIP_BODY; @@ -135,13 +136,13 @@ public class ValueOfTag extends ExTagSupport { * } scope, {@code false} otherwise. */ private boolean parameterExists() { - mParameterValue = pageContext.getAttribute(mParameterName, PageContext.REQUEST_SCOPE); + parameterValue = pageContext.getAttribute(parameterName, PageContext.REQUEST_SCOPE); // -- Harald K 20020726 - if (mParameterValue == null) { - mParameterValue = pageContext.getRequest().getParameter(mParameterName); + if (parameterValue == null) { + parameterValue = pageContext.getRequest().getParameter(parameterName); } - return (mParameterValue != null); + return (parameterValue != null); } } diff --git a/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package-info.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package-info.java new file mode 100644 index 00000000..4aac8ff7 --- /dev/null +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +/** + * Dynamo Droplet-like functionality for JSP. + * + * This package is early beta, not for commercial use! :-) + * Read: The interfaces and classes in this package (and subpackages) will be + * developed and modified for a while. + * + * TODO: Insert taglib-descriptor here? + */ +package com.twelvemonkeys.servlet.jsp.droplet.taglib; \ No newline at end of file diff --git a/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package-info.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package-info.java new file mode 100644 index 00000000..6681cfbc --- /dev/null +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package-info.java @@ -0,0 +1,4 @@ +/** + * JSP support. + */ +package com.twelvemonkeys.servlet.jsp; \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java similarity index 97% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java index 36d1f926..1d260c8d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java @@ -1,4 +1,3 @@ - package com.twelvemonkeys.servlet.jsp.taglib; import javax.servlet.jsp.JspException; @@ -10,7 +9,6 @@ import javax.servlet.jsp.JspException; * * @version 1.0 */ - public abstract class BodyReaderTag extends ExBodyTagSupport { /** * This is the method called by the JSP engine when the body for a tag @@ -23,7 +21,6 @@ public abstract class BodyReaderTag extends ExBodyTagSupport { * processed the one time. * @exception JspException */ - public int doAfterBody() throws JspException { processBody(bodyContent.getString()); return SKIP_BODY; @@ -36,8 +33,7 @@ public abstract class BodyReaderTag extends ExBodyTagSupport { * this method is called. * * @param pContent The body for the custom tag converted to a String. - * @exception JscException + * @exception JspException */ - protected abstract void processBody(String pContent) throws JspException; } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java similarity index 79% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java index bbc0003a..e698f2ad 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java @@ -20,11 +20,16 @@ package com.twelvemonkeys.servlet.jsp.taglib; -import java.util.*; -import java.io.*; - -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.tagext.BodyContent; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; /** * Creates a table from a string of "comma-separated values" (CSV). @@ -65,31 +70,29 @@ import javax.servlet.jsp.tagext.*; * * @author Harald Kuhr * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java#1 $ + * @version $Id: jsp/taglib/CSVToTableTag.java#1 $ */ - public class CSVToTableTag extends ExBodyTagSupport { - public final static String TAB = "\t"; - protected String mDelimiter = null; - protected boolean mFirstRowIsHeader = false; - protected boolean mFirstColIsHeader = false; + protected String delimiter = null; + protected boolean firstRowIsHeader = false; + protected boolean firstColIsHeader = false; public void setDelimiter(String pDelimiter) { - mDelimiter = pDelimiter; + delimiter = pDelimiter; } public String getDelimiter() { - return mDelimiter != null ? mDelimiter : TAB; + return delimiter != null ? delimiter : TAB; } public void setFirstRowIsHeader(String pBoolean) { - mFirstRowIsHeader = Boolean.valueOf(pBoolean).booleanValue(); + firstRowIsHeader = Boolean.valueOf(pBoolean); } public void setFirstColIsHeader(String pBoolean) { - mFirstColIsHeader = Boolean.valueOf(pBoolean).booleanValue(); + firstColIsHeader = Boolean.valueOf(pBoolean); } @@ -114,14 +117,11 @@ public class CSVToTableTag extends ExBodyTagSupport { // Loop over cells in each row for (int col = 0; col < table.getCols(); col++) { // Test if we are using headers, else normal cell - if (mFirstRowIsHeader && row == 0 - || mFirstColIsHeader && col == 0) { - out.println("" + table.get(row, col) - + " "); + if (firstRowIsHeader && row == 0 || firstColIsHeader && col == 0) { + out.println("" + table.get(row, col) + " "); } else { - out.println("" + table.get(row, col) - + " "); + out.println("" + table.get(row, col) + " "); } } @@ -139,29 +139,29 @@ public class CSVToTableTag extends ExBodyTagSupport { } static class Table { - List mRows = null; - int mCols = 0; + List rows = null; + int cols = 0; private Table(List pRows, int pCols) { - mRows = pRows; - mCols = pCols; + rows = pRows; + cols = pCols; } int getRows() { - return mRows != null ? mRows.size() : 0; + return rows != null ? rows.size() : 0; } int getCols() { - return mCols; + return cols; } List getTableRows() { - return mRows; + return rows; } List getTableRow(int pRow) { - return mRows != null - ? (List) mRows.get(pRow) + return rows != null + ? (List) rows.get(pRow) : Collections.EMPTY_LIST; } @@ -175,25 +175,22 @@ public class CSVToTableTag extends ExBodyTagSupport { * Parses a BodyContent to a table. * */ - - static Table parseContent(Reader pContent, String pDelim) - throws IOException { - ArrayList tableRows = new ArrayList(); + static Table parseContent(Reader pContent, String pDelim) throws IOException { + List> tableRows = new ArrayList>(); int tdsPerTR = 0; // Loop through TRs BufferedReader reader = new BufferedReader(pContent); - String tr = null; + String tr; while ((tr = reader.readLine()) != null) { // Discard blank lines - if (tr != null - && tr.trim().length() <= 0 && tr.indexOf(pDelim) < 0) { + if (tr.trim().length() <= 0 && tr.indexOf(pDelim) < 0) { continue; } //System.out.println("CSVToTable: read LINE=\"" + tr + "\""); - ArrayList tableDatas = new ArrayList(); + List tableDatas = new ArrayList(); StringTokenizer tableRow = new StringTokenizer(tr, pDelim, true); @@ -235,6 +232,4 @@ public class CSVToTableTag extends ExBodyTagSupport { return new Table(tableRows, tdsPerTR); } } - - } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java similarity index 94% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java index 45e215c5..78902c0a 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java @@ -14,14 +14,18 @@ package com.twelvemonkeys.servlet.jsp.taglib; -import java.io.*; -import java.net.*; -import java.util.*; - -import javax.servlet.*; -import javax.servlet.http.*; -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.tagext.BodyTagSupport; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.StringTokenizer; /** * This is the class that should be extended by all jsp pages that do use their @@ -30,7 +34,7 @@ import javax.servlet.jsp.tagext.*; * @author Thomas Purcell (CSC Australia) * @author Harald Kuhr * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java#1 $ + * @version $Id: jsp/taglib/ExBodyTagSupport.java#1 $ */ public class ExBodyTagSupport extends BodyTagSupport implements ExTag { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java similarity index 94% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java index 690819ec..7dee5cfd 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java @@ -15,12 +15,13 @@ package com.twelvemonkeys.servlet.jsp.taglib; -import java.io.*; -import java.util.*; - -import javax.servlet.*; -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.tagext.Tag; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; /** * This interface contains a lot of helper methods for simplifying common @@ -28,7 +29,7 @@ import javax.servlet.jsp.tagext.*; * * @author Harald Kuhr * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java#1 $ + * @version $Id: jsp/taglib/ExTag.java#1 $ */ public interface ExTag extends Tag { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java similarity index 94% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java index 0798976b..c356d237 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java @@ -15,14 +15,18 @@ package com.twelvemonkeys.servlet.jsp.taglib; -import java.io.*; -import java.net.*; -import java.util.*; - -import javax.servlet.*; -import javax.servlet.http.*; -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspWriter; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.tagext.TagSupport; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.StringTokenizer; /** * This is the class that should be extended by all jsp pages that don't use @@ -32,7 +36,7 @@ import javax.servlet.jsp.tagext.*; * @author Thomas Purcell (CSC Australia) * @author Harald Kuhr * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java#1 $ + * @version $Id: jsp/taglib/ExTagSupport.java#1 $ */ public class ExTagSupport extends TagSupport implements ExTag { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java similarity index 99% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java index 8dc90dcc..c61c4078 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java @@ -1,4 +1,3 @@ - package com.twelvemonkeys.servlet.jsp.taglib; import javax.servlet.jsp.*; diff --git a/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java new file mode 100755 index 00000000..7c8da4b3 --- /dev/null +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java @@ -0,0 +1,49 @@ +package com.twelvemonkeys.servlet.jsp.taglib; + +import com.twelvemonkeys.util.convert.Converter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.Tag; +import javax.servlet.jsp.tagext.TagSupport; +import java.io.File; +import java.util.Date; + +/** + * Prints the last modified + */ + +public class LastModifiedTag extends TagSupport { + private String fileName = null; + private String format = null; + + public void setFile(String pFileName) { + fileName = pFileName; + } + + public void setFormat(String pFormat) { + format = pFormat; + } + + public int doStartTag() throws JspException { + File file; + + if (fileName != null) { + file = new File(pageContext.getServletContext().getRealPath(fileName)); + } + else { + HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); + + // Get the file containing the servlet + file = new File(pageContext.getServletContext().getRealPath(request.getServletPath())); + } + + Date lastModified = new Date(file.lastModified()); + Converter conv = Converter.getInstance(); + + // Set the last modified value back + pageContext.setAttribute("lastModified", conv.toString(lastModified, format)); + + return Tag.EVAL_BODY_INCLUDE; + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java similarity index 96% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java index 8b376c00..ecdbdba2 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java @@ -1,10 +1,8 @@ - package com.twelvemonkeys.servlet.jsp.taglib; import java.io.IOException; import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; /** * This tag truncates all consecutive whitespace in sequence inside its body, @@ -26,7 +24,7 @@ public class TrimWhiteSpaceTag extends ExBodyTagSupport { */ public int doStartTag() throws JspException { - return BodyTag.EVAL_BODY_BUFFERED; + return EVAL_BODY_BUFFERED; } /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java similarity index 92% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java index 66c2d612..59b0c571 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java @@ -16,21 +16,17 @@ package com.twelvemonkeys.servlet.jsp.taglib; import java.io.*; import java.net.*; -import javax.servlet.*; import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; import javax.xml.transform.*; import javax.xml.transform.stream.*; -import com.twelvemonkeys.servlet.jsp.*; - /** * This tag performs XSL Transformations (XSLT) on a given XML document or its * body content. * * @author Harald Kuhr * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java#1 $ + * @version $Id: jsp/taglib/XMLTransformTag.java#1 $ */ public class XMLTransformTag extends ExBodyTagSupport { @@ -86,11 +82,11 @@ public class XMLTransformTag extends ExBodyTagSupport { throw new JspException(ioe.getMessage(), ioe); } - return Tag.SKIP_BODY; + return SKIP_BODY; } // ...else process the body - return BodyTag.EVAL_BODY_BUFFERED; + return EVAL_BODY_BUFFERED; } /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java similarity index 91% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java index 65e97915..4cb4136d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java @@ -23,8 +23,6 @@ package com.twelvemonkeys.servlet.jsp.taglib.logic; -import java.lang.*; - import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.TagSupport; @@ -38,8 +36,8 @@ import javax.servlet.jsp.tagext.TagSupport; public abstract class ConditionalTagBase extends TagSupport { // Members - protected String mObjectName; - protected String mObjectValue; + protected String objectName; + protected String objectValue; // Properties @@ -51,7 +49,7 @@ public abstract class ConditionalTagBase extends TagSupport { * */ public String getName() { - return mObjectName; + return objectName; } /** @@ -62,7 +60,7 @@ public abstract class ConditionalTagBase extends TagSupport { * */ public void setName(String pObjectName) { - this.mObjectName = pObjectName; + this.objectName = pObjectName; } /** @@ -73,7 +71,7 @@ public abstract class ConditionalTagBase extends TagSupport { * */ public String getValue() { - return mObjectValue; + return objectValue; } /** @@ -84,7 +82,7 @@ public abstract class ConditionalTagBase extends TagSupport { * */ public void setValue(String pObjectValue) { - this.mObjectValue = pObjectValue; + this.objectValue = pObjectValue; } /** @@ -120,8 +118,8 @@ public abstract class ConditionalTagBase extends TagSupport { public void release() { super.release(); - mObjectName = null; - mObjectValue = null; + objectName = null; + objectValue = null; } /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java similarity index 95% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java index 7e21111b..a3a856c5 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java @@ -8,13 +8,11 @@ package com.twelvemonkeys.servlet.jsp.taglib.logic; -import java.lang.*; +import com.twelvemonkeys.lang.StringUtil; import javax.servlet.http.Cookie; import javax.servlet.jsp.JspException; -import com.twelvemonkeys.lang.StringUtil; - /** *

    @@ -134,15 +132,15 @@ public class EqualTag extends ConditionalTagBase { */ protected boolean condition() throws JspException { - if (StringUtil.isEmpty(mObjectName)) { + if (StringUtil.isEmpty(objectName)) { return false; } - if (StringUtil.isEmpty(mObjectValue)) { + if (StringUtil.isEmpty(objectValue)) { return true; } - Object pageScopedAttribute = pageContext.getAttribute(mObjectName); + Object pageScopedAttribute = pageContext.getAttribute(objectName); if (pageScopedAttribute == null) { return false; } @@ -164,7 +162,7 @@ public class EqualTag extends ConditionalTagBase { return false; } - return (pageScopedStringAttribute.equals(mObjectValue)); + return (pageScopedStringAttribute.equals(objectValue)); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java similarity index 99% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java index 91ed1fb5..013c11b1 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java @@ -1,4 +1,3 @@ - package com.twelvemonkeys.servlet.jsp.taglib.logic; import javax.servlet.jsp.tagext.*; diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java similarity index 92% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java index 7c3e1da6..6409664d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java @@ -1,10 +1,9 @@ - package com.twelvemonkeys.servlet.jsp.taglib.logic; -import java.util.Iterator; - import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.*; +import javax.servlet.jsp.tagext.Tag; +import javax.servlet.jsp.tagext.TagSupport; +import java.util.Iterator; /** * Abstract base class for adding iterators to a page. @@ -24,7 +23,7 @@ public abstract class IteratorProviderTag extends TagSupport { public final static String ATTRIBUTE_TYPE = "type"; /** */ - private String mType = null; + private String type = null; /** * Gets the type. @@ -32,7 +31,7 @@ public abstract class IteratorProviderTag extends TagSupport { * @return the type (class name) */ public String getType() { - return mType; + return type; } /** @@ -42,7 +41,7 @@ public abstract class IteratorProviderTag extends TagSupport { */ public void setType(String pType) { - mType = pType; + type = pType; } /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java similarity index 95% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java index 05cf228f..3c489957 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java @@ -132,15 +132,15 @@ public class NotEqualTag extends ConditionalTagBase { */ protected boolean condition() throws JspException { - if (StringUtil.isEmpty(mObjectName)) { + if (StringUtil.isEmpty(objectName)) { return false; } - if (StringUtil.isEmpty(mObjectValue)) { + if (StringUtil.isEmpty(objectValue)) { return true; } - Object pageScopedAttribute = pageContext.getAttribute(mObjectName); + Object pageScopedAttribute = pageContext.getAttribute(objectName); if (pageScopedAttribute == null) { return false; } @@ -162,7 +162,7 @@ public class NotEqualTag extends ConditionalTagBase { return false; } - return (!(pageScopedStringAttribute.equals(mObjectValue))); + return (!(pageScopedStringAttribute.equals(objectValue))); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package_info.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package_info.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package-info.java diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java similarity index 75% rename from servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java rename to sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java index 49e7eb79..19194b5c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java +++ b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java @@ -2,28 +2,28 @@ package com.twelvemonkeys.servlet.log4j; import org.apache.log4j.Logger; -import java.util.Enumeration; -import java.util.Set; -import java.net.URL; -import java.net.MalformedURLException; -import java.io.InputStream; -import java.lang.reflect.Proxy; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; - -import javax.servlet.ServletContext; import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; +import javax.servlet.ServletContext; import javax.servlet.ServletException; +import java.io.InputStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Set; /** * Log4JContextWrapper *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java#1 $ + * @version $Id: log4j/Log4JContextWrapper.java#1 $ */ final class Log4JContextWrapper implements ServletContext { + // TODO: Move to sandbox // TODO: This solution sucks... // How about starting to create some kind of pluggable decorator system, @@ -32,7 +32,7 @@ final class Log4JContextWrapper implements ServletContext { // wrapped object based on configuration. // This way we could simply call ServletUtil.decorate(ServletContext):ServletContext // And the context would be decorated with all configured mixins at once, - // requiring less bolierplate delegation code, and less layers of wrapping + // requiring less boilerplate delegation code, and less layers of wrapping // (alternatively we could decorate the Servlet/FilterConfig objects). // See the ServletUtil.createWrapper methods for some hints.. @@ -57,15 +57,15 @@ final class Log4JContextWrapper implements ServletContext { }); } - private final ServletContext mContext; + private final ServletContext context; - private final Logger mLogger; + private final Logger logger; Log4JContextWrapper(ServletContext pContext) { - mContext = pContext; + context = pContext; // TODO: We want a logger per servlet, not per servlet context, right? - mLogger = Logger.getLogger(pContext.getServletContextName()); + logger = Logger.getLogger(pContext.getServletContextName()); // TODO: Automatic init/config of Log4J using context parameter for log4j.xml? // See Log4JInit.java @@ -85,99 +85,99 @@ final class Log4JContextWrapper implements ServletContext { // Should be possible using some stack peek hack, but that's slow... // Find a good way... // Maybe just pass it into the constuctor, and have one wrapper per servlet - mLogger.info(pMessage); + logger.info(pMessage); } public void log(String pMessage, Throwable pCause) { // TODO: Get logger for caller.. - mLogger.error(pMessage, pCause); + logger.error(pMessage, pCause); } public Object getAttribute(String pMessage) { - return mContext.getAttribute(pMessage); + return context.getAttribute(pMessage); } public Enumeration getAttributeNames() { - return mContext.getAttributeNames(); + return context.getAttributeNames(); } public ServletContext getContext(String pMessage) { - return mContext.getContext(pMessage); + return context.getContext(pMessage); } public String getInitParameter(String pMessage) { - return mContext.getInitParameter(pMessage); + return context.getInitParameter(pMessage); } public Enumeration getInitParameterNames() { - return mContext.getInitParameterNames(); + return context.getInitParameterNames(); } public int getMajorVersion() { - return mContext.getMajorVersion(); + return context.getMajorVersion(); } public String getMimeType(String pMessage) { - return mContext.getMimeType(pMessage); + return context.getMimeType(pMessage); } public int getMinorVersion() { - return mContext.getMinorVersion(); + return context.getMinorVersion(); } public RequestDispatcher getNamedDispatcher(String pMessage) { - return mContext.getNamedDispatcher(pMessage); + return context.getNamedDispatcher(pMessage); } public String getRealPath(String pMessage) { - return mContext.getRealPath(pMessage); + return context.getRealPath(pMessage); } public RequestDispatcher getRequestDispatcher(String pMessage) { - return mContext.getRequestDispatcher(pMessage); + return context.getRequestDispatcher(pMessage); } public URL getResource(String pMessage) throws MalformedURLException { - return mContext.getResource(pMessage); + return context.getResource(pMessage); } public InputStream getResourceAsStream(String pMessage) { - return mContext.getResourceAsStream(pMessage); + return context.getResourceAsStream(pMessage); } public Set getResourcePaths(String pMessage) { - return mContext.getResourcePaths(pMessage); + return context.getResourcePaths(pMessage); } public String getServerInfo() { - return mContext.getServerInfo(); + return context.getServerInfo(); } public Servlet getServlet(String pMessage) throws ServletException { //noinspection deprecation - return mContext.getServlet(pMessage); + return context.getServlet(pMessage); } public String getServletContextName() { - return mContext.getServletContextName(); + return context.getServletContextName(); } public Enumeration getServletNames() { //noinspection deprecation - return mContext.getServletNames(); + return context.getServletNames(); } public Enumeration getServlets() { //noinspection deprecation - return mContext.getServlets(); + return context.getServlets(); } public void removeAttribute(String pMessage) { - mContext.removeAttribute(pMessage); + context.removeAttribute(pMessage); } public void setAttribute(String pMessage, Object pExtension) { - mContext.setAttribute(pMessage, pExtension); + context.setAttribute(pMessage, pExtension); } } diff --git a/sandbox/sandbox-swing/pom.xml b/sandbox/sandbox-swing/pom.xml new file mode 100644 index 00000000..16936b35 --- /dev/null +++ b/sandbox/sandbox-swing/pom.xml @@ -0,0 +1,153 @@ + + + 4.0.0 + + com.twelvemonkeys.sandbox + sandbox + 3.0-SNAPSHOT + + sandbox-swing + jar + TwelveMonkeys :: Sandbox :: Swing + + The TwelveMonkeys Swing Sandbox. Experimental stuff. + + + + + /Library/JavaFX/Home + + + + + com.twelvemonkeys.common + common-lang + compile + + + com.twelvemonkeys.common + common-io + compile + + + com.twelvemonkeys.common + common-image + compile + + + com.twelvemonkeys.swing + swing-core + compile + + + com.twelvemonkeys.swing + swing-application + compile + + + com.twelvemonkeys.imageio + imageio-core + provided + + + + + + com.sun.javafx + javafx-common + 1.3 + system + ${javafx_home}/lib/desktop/javafx-common.jar + + + com.sun.javafx + javafx-io + 1.3 + system + ${javafx_home}/lib/desktop/javafx-io.jar + + + com.sun.javafx + javafx-geom + 1.3 + system + ${javafx_home}/lib/desktop/javafx-geom.jar + + + com.sun.javafx + javafx-ui-commmon + 1.3 + system + ${javafx_home}/lib/desktop/javafx-ui-common.jar + + + com.sun.javafx + javafx-ui-controls + 1.3 + system + ${javafx_home}/lib/desktop/javafx-ui-controls.jar + + + com.sun.javafx + javafx-ui-charts + 1.3 + system + ${javafx_home}/lib/desktop/javafx-ui-charts.jar + + + com.sun.javafx + javafx-ui-desktop + 1.3 + system + ${javafx_home}/lib/desktop/javafx-ui-desktop.jar + + + com.sun.javafx + javafx-ui-swing + 1.3 + system + ${javafx_home}/lib/desktop/javafx-ui-swing.jar + + + com.sun.javafx + javafx-ext-swing + 1.3 + system + ${javafx_home}/lib/desktop/javafx-ext-swing.jar + + + com.sun.javafx + javafx-xfdloader + 1.3 + system + ${javafx_home}/lib/desktop/fxdloader.jar + + + com.sun.javafx + javafx-websvc + 1.3 + system + ${javafx_home}/lib/desktop/websvc.jar + + + + + + com.sun.javafx.jmc + javafx-jmc + 1.3 + system + ${javafx_home}/lib/desktop/jmc.jar + + + com.sun.javafx.scriptapi + javafx-scriptapi + 1.3 + system + ${javafx_home}/lib/desktop/script-api.jar + + + + diff --git a/sandbox/sandbox-swing/src/main/java/com/twelvemonkeys/swing/filechooser/FileSystemViews.java b/sandbox/sandbox-swing/src/main/java/com/twelvemonkeys/swing/filechooser/FileSystemViews.java new file mode 100644 index 00000000..f08b366d --- /dev/null +++ b/sandbox/sandbox-swing/src/main/java/com/twelvemonkeys/swing/filechooser/FileSystemViews.java @@ -0,0 +1,207 @@ +package com.twelvemonkeys.swing.filechooser; + +import com.twelvemonkeys.lang.Platform; +import com.twelvemonkeys.lang.Validate; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import javax.swing.filechooser.FileView; +import javax.swing.plaf.FileChooserUI; +import java.io.File; +import java.io.IOException; + +/** + * FileSystemViews + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: FileSystemViews.java,v 1.0 Jan 14, 2010 3:19:51 PM haraldk Exp$ + */ +public final class FileSystemViews { + + public static FileSystemView getFileSystemView() { + if (Platform.os() == Platform.OperatingSystem.MacOS) { + return ProxyFileSystemView.instance; + } + + return FileSystemView.getFileSystemView(); + } + + private static class ProxyFileSystemView extends FileSystemView { + + private static final FileSystemView instance = createFSV(); + + private static FileSystemView createFSV() { + FileSystemView view = FileSystemView.getFileSystemView(); + + try { + FileChooserUI ui = null; +/* NOTE: The following is faster, but does not work reliably, as getSystemTypeDescription will return null... + + // The below is really a lot of hassle to avoid creating a JFileChooser. Maybe not a good idea? + String uiClassName = UIManager.getString("FileChooserUI"); + try { + @SuppressWarnings({"unchecked"}) + Class uiClass = (Class) Class.forName(uiClassName); + @SuppressWarnings({"unchecked"}) + Constructor[] constructors = uiClass.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + + Class[] parameterTypes = constructor.getParameterTypes(); + + // Test the two most likely constructors + if (parameterTypes.length == 0) { + ui = (FileChooserUI) constructor.newInstance(); + break; + } + else if (parameterTypes.length == 1 && parameterTypes[0] == JFileChooser.class) { + ui = (FileChooserUI) constructor.newInstance((JFileChooser) null); + break; + } + } + } + catch (Exception ignore) { + ignore.printStackTrace(); + } + + if (ui == null) { +*/ + // Somewhat slower, but should work even if constructors change + ui = new JFileChooser().getUI(); +// } + + return new ProxyFileSystemView(ui.getFileView(null), view); + } + catch (Throwable ignore) { + } + + // Fall back to default view + return view; + } + + private final FileView uiView; + private final FileSystemView defaultView; + + public ProxyFileSystemView(final FileView pUIView, final FileSystemView pDefaultView) { + Validate.notNull(pUIView, "uiView"); + Validate.notNull(pDefaultView, "defaultFileSystemView"); + + uiView = pUIView; + defaultView = pDefaultView; + } + + @Override + public Boolean isTraversable(File f) { + return uiView.isTraversable(f); + } + + @Override + public String getSystemDisplayName(File f) { + return uiView.getName(f); + } + + @Override + public String getSystemTypeDescription(File f) { + // TODO: Create something that gives a proper description here on the Mac... + return uiView.getTypeDescription(f); + } + + @Override + public Icon getSystemIcon(File f) { + return uiView.getIcon(f); + } + + @Override + public boolean isRoot(File f) { + return defaultView.isRoot(f); + } + + @Override + public boolean isParent(File folder, File file) { + return defaultView.isParent(folder, file); + } + + @Override + public File getChild(File parent, String fileName) { + return defaultView.getChild(parent, fileName); + } + + @Override + public boolean isFileSystem(File f) { + return defaultView.isFileSystem(f); + } + + @Override + public boolean isHiddenFile(File f) { + return defaultView.isHiddenFile(f); + } + + @Override + public boolean isFileSystemRoot(File dir) { + return defaultView.isFileSystemRoot(dir); + } + + @Override + public boolean isDrive(File dir) { + return defaultView.isDrive(dir); + } + + @Override + public boolean isFloppyDrive(File dir) { + return defaultView.isFloppyDrive(dir); + } + + @Override + public boolean isComputerNode(File dir) { + return defaultView.isComputerNode(dir); + } + + @Override + public File[] getRoots() { + return defaultView.getRoots(); + } + + @Override + public File getHomeDirectory() { + return defaultView.getHomeDirectory(); + } + + @Override + public File getDefaultDirectory() { + return defaultView.getDefaultDirectory(); + } + + @Override + public File createFileObject(File dir, String filename) { + return defaultView.createFileObject(dir, filename); + } + + @Override + public File createFileObject(String path) { + return defaultView.createFileObject(path); + } + + @Override + public File[] getFiles(File dir, boolean useFileHiding) { + return defaultView.getFiles(dir, useFileHiding); + } + + @Override + public File getParentDirectory(File dir) { + return defaultView.getParentDirectory(dir); + } + + @Override + public File createNewFolder(File containingDir) throws IOException { + return defaultView.createNewFolder(containingDir); + } + + @Override + public String toString() { + return super.toString() + "[" + uiView + ", " + defaultView + "]"; + } + } +} diff --git a/servlet/pom.xml b/servlet/pom.xml index dd981f87..28bb1982 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -11,7 +11,6 @@ 4.0.0 com.twelvemonkeys.servlet servlet - 3.0-SNAPSHOT TwelveMonkeys :: Servlet @@ -77,15 +76,14 @@ junit junit - 4.3.1 + 4.7 test - jmock - jmock-cglib - 1.0.1 - test + org.mockito + mockito-all + 1.8.5 diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java index 1dcf01f1..3d187231 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java @@ -9,41 +9,43 @@ import java.util.*; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java#1 $ + * @version $Id: AbstractServletMapAdapter.java#1 $ */ abstract class AbstractServletMapAdapter extends AbstractMap> { // TODO: This map is now a little too lazy.. Should cache entries too (instead?) ! private final static List NULL_LIST = new ArrayList(); - private transient Map> mCache = new HashMap>(); - private transient int mSize = -1; - private transient AbstractSet>> mEntries; + private transient Map> cache = new HashMap>(); + private transient int size = -1; + private transient AbstractSet>> entries; protected abstract Iterator keysImpl(); protected abstract Iterator valuesImpl(String pName); @Override - public List get(Object pKey) { + public List get(final Object pKey) { if (pKey instanceof String) { return getValues((String) pKey); } + return null; } - private List getValues(String pName) { - List values = mCache.get(pName); + private List getValues(final String pName) { + List values = cache.get(pName); if (values == null) { //noinspection unchecked Iterator headers = valuesImpl(pName); + if (headers == null) { - mCache.put(pName, NULL_LIST); + cache.put(pName, NULL_LIST); } else { values = toList(headers); - mCache.put(pName, values); + cache.put(pName, values); } } @@ -58,34 +60,35 @@ abstract class AbstractServletMapAdapter extends AbstractMap names = keysImpl(); - mSize = 0; - for (;names.hasNext(); names.next()) { - mSize++; + size = 0; + + for (Iterator names = keysImpl(); names.hasNext(); names.next()) { + size++; } } public Set>> entrySet() { - if (mEntries == null) { - mEntries = new AbstractSet>>() { + if (entries == null) { + entries = new AbstractSet>>() { public Iterator>> iterator() { return new Iterator>>() { - Iterator mHeaderNames = keysImpl(); + Iterator headerNames = keysImpl(); public boolean hasNext() { - return mHeaderNames.hasNext(); + return headerNames.hasNext(); } public Entry> next() { // TODO: Replace with cached lookup - return new HeaderEntry(mHeaderNames.next()); + return new HeaderEntry(headerNames.next()); } public void remove() { @@ -100,22 +103,22 @@ abstract class AbstractServletMapAdapter extends AbstractMap> { - String mHeaderName; + String headerName; public HeaderEntry(String pHeaderName) { - mHeaderName = pHeaderName; + headerName = pHeaderName; } public String getKey() { - return mHeaderName; + return headerName; } public List getValue() { - return get(mHeaderName); + return get(headerName); } public List setValue(List pValue) { @@ -125,7 +128,7 @@ abstract class AbstractServletMapAdapter extends AbstractMap value; - return (mHeaderName == null ? 0 : mHeaderName.hashCode()) ^ + return (headerName == null ? 0 : headerName.hashCode()) ^ ((value = getValue()) == null ? 0 : value.hashCode()); } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java index 939d8588..938dd16c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java @@ -30,16 +30,16 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.lang.StringUtil; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; import javax.servlet.FilterChain; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.IOException; -import java.util.Properties; -import java.util.List; import java.util.ArrayList; +import java.util.List; +import java.util.Properties; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -47,14 +47,15 @@ import java.util.regex.PatternSyntaxException; * BrowserHelperFilter * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java#1 $ + * @version $Id: BrowserHelperFilter.java#1 $ */ public class BrowserHelperFilter extends GenericFilter { private static final String HTTP_HEADER_ACCEPT = "Accept"; protected static final String HTTP_HEADER_USER_AGENT = "User-Agent"; - private Pattern[] mKnownAgentPatterns; - private String[] mKnownAgentAccpets; + // TODO: Consider using unmodifiable LinkedHashMap instead + private Pattern[] knownAgentPatterns; + private String[] knownAgentAccepts; /** * Sets the accept-mappings for this filter @@ -62,55 +63,63 @@ public class BrowserHelperFilter extends GenericFilter { * @throws ServletConfigException if the accept-mappings properties * file cannot be read. */ + @InitParam(name = "accept-mappings-file") public void setAcceptMappingsFile(String pPropertiesFile) throws ServletConfigException { // NOTE: Format is: // = // .accept= Properties mappings = new Properties(); + try { log("Reading Accept-mappings properties file: " + pPropertiesFile); mappings.load(getServletContext().getResourceAsStream(pPropertiesFile)); - List patterns = new ArrayList(); - List accepts = new ArrayList(); - //System.out.println("--> Loaded file: " + pPropertiesFile); - - for (Object key : mappings.keySet()) { - String agent = (String) key; - if (agent.endsWith(".accept")) { - continue; - } - - //System.out.println("--> Adding accept-mapping for User-Agent: " + agent); - - try { - String accept = (String) mappings.get(agent + ".accept"); - if (!StringUtil.isEmpty(accept)) { - patterns.add(Pattern.compile((String) mappings.get(agent))); - accepts.add(accept); - //System.out.println("--> " + agent + " accepts: " + accept); - } - else { - log("Missing Accept mapping for User-Agent: " + agent); - } - } - catch (PatternSyntaxException e) { - log("Could not parse User-Agent identification for " + agent, e); - } - - mKnownAgentPatterns = patterns.toArray(new Pattern[patterns.size()]); - mKnownAgentAccpets = accepts.toArray(new String[accepts.size()]); - } } catch (IOException e) { throw new ServletConfigException("Could not read Accept-mappings properties file: " + pPropertiesFile, e); } + + parseMappings(mappings); + } + + private void parseMappings(Properties mappings) { + List patterns = new ArrayList(); + List accepts = new ArrayList(); + + for (Object key : mappings.keySet()) { + String agent = (String) key; + + if (agent.endsWith(".accept")) { + continue; + } + + //System.out.println("--> Adding accept-mapping for User-Agent: " + agent); + + try { + String accept = (String) mappings.get(agent + ".accept"); + + if (!StringUtil.isEmpty(accept)) { + patterns.add(Pattern.compile((String) mappings.get(agent))); + accepts.add(accept); + //System.out.println("--> " + agent + " accepts: " + accept); + } + else { + log("Missing Accept mapping for User-Agent: " + agent); + } + } + catch (PatternSyntaxException e) { + log("Could not parse User-Agent identification for " + agent, e); + } + + knownAgentPatterns = patterns.toArray(new Pattern[patterns.size()]); + knownAgentAccepts = accepts.toArray(new String[accepts.size()]); + } } public void init() throws ServletException { - if (mKnownAgentAccpets == null || mKnownAgentAccpets.length == 0) { + if (knownAgentAccepts == null || knownAgentAccepts.length == 0) { throw new ServletConfigException("No User-Agent/Accept mappings for filter: " + getFilterName()); } } @@ -119,18 +128,19 @@ public class BrowserHelperFilter extends GenericFilter { if (pRequest instanceof HttpServletRequest) { //System.out.println("--> Trying to find User-Agent/Accept headers..."); HttpServletRequest request = (HttpServletRequest) pRequest; + // Check if User-Agent is in list of known agents - if (mKnownAgentPatterns != null && mKnownAgentPatterns.length > 0) { + if (knownAgentPatterns != null && knownAgentPatterns.length > 0) { String agent = request.getHeader(HTTP_HEADER_USER_AGENT); //System.out.println("--> User-Agent: " + agent); - for (int i = 0; i < mKnownAgentPatterns.length; i++) { - Pattern pattern = mKnownAgentPatterns[i]; + for (int i = 0; i < knownAgentPatterns.length; i++) { + Pattern pattern = knownAgentPatterns[i]; //System.out.println("--> Pattern: " + pattern); - if (pattern.matcher(agent).matches()) { - // TODO: Consider merge known with real accpet, in case plugins add extra capabilities? - final String fakeAccept = mKnownAgentAccpets[i]; + if (pattern.matcher(agent).matches()) { + // TODO: Consider merge known with real accept, in case plugins add extra capabilities? + final String fakeAccept = knownAgentAccepts[i]; //System.out.println("--> User-Agent: " + agent + " accepts: " + fakeAccept); pRequest = new HttpServletRequestWrapper(request) { @@ -138,14 +148,17 @@ public class BrowserHelperFilter extends GenericFilter { if (HTTP_HEADER_ACCEPT.equals(pName)) { return fakeAccept; } + return super.getHeader(pName); } }; + break; } } } } + pChain.doFilter(pRequest, pResponse); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java index 1796a5ef..004b1274 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java @@ -42,10 +42,10 @@ import java.util.Enumeration; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java#1 $ + * @version $Id: DebugServlet.java#1 $ */ public class DebugServlet extends GenericServlet { - private long mDateModified; + private long dateModified; public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException { service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse); @@ -53,13 +53,13 @@ public class DebugServlet extends GenericServlet { public void init() throws ServletException { super.init(); - mDateModified = System.currentTimeMillis(); + dateModified = System.currentTimeMillis(); } public void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException { pResponse.setContentType("text/plain"); // Include these to allow browser caching - pResponse.setDateHeader("Last-Modified", mDateModified); + pResponse.setDateHeader("Last-Modified", dateModified); pResponse.setHeader("ETag", getServletName()); ServletOutputStream out = pResponse.getOutputStream(); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java index 64b314c5..c7cc82d6 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java @@ -56,12 +56,12 @@ import java.util.Enumeration; * most basic types, if neccessary. *

    * To write a generic filter, you need only override the abstract - * {@link #doFilterImpl doFilterImpl} method. + * {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. * * @author Harald Kuhr * @author last modified by $Author: haku $ * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java#1 $ + * @version $Id: GenericFilter.java#1 $ * * @see Filter * @see FilterConfig @@ -71,14 +71,14 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl /** * The filter config. */ - private transient FilterConfig mFilterConfig = null; + private transient FilterConfig filterConfig = null; /** * Makes sure the filter runs once per request *

    * see #isRunOnce * - * @see #mOncePerRequest + * @see #oncePerRequest * see #ATTRIB_RUN_ONCE_VALUE */ private final static String ATTRIB_RUN_ONCE_EXT = ".REQUEST_HANDLED"; @@ -90,17 +90,17 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl *

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

    * see #isRunOnce * - * @see #mOncePerRequest + * @see #oncePerRequest * see #ATTRIB_RUN_ONCE_EXT */ private static final Object ATTRIB_RUN_ONCE_VALUE = new Object(); @@ -119,7 +119,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl *

    <dispatcher>REQUEST</dispatcher>
    * */ - protected boolean mOncePerRequest = false; + protected boolean oncePerRequest = false; /** * Does nothing. @@ -143,17 +143,17 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @param pConfig the filter config * @throws ServletException if an error occurs during init * - * @see Filter#init + * @see Filter#init(javax.servlet.FilterConfig) * @see #init() init * @see BeanUtil#configure(Object, java.util.Map, boolean) */ - public void init(FilterConfig pConfig) throws ServletException { + public void init(final FilterConfig pConfig) throws ServletException { if (pConfig == null) { - throw new ServletConfigException("filterconfig == null"); + throw new ServletConfigException("filter config == null"); } - // Store filterconfig - mFilterConfig = pConfig; + // Store filter config + filterConfig = pConfig; // Configure this try { @@ -164,8 +164,8 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl } // Create run-once attribute name - mAttribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT; - log("init (oncePerRequest=" + mOncePerRequest + ", attribRunOnce=" + mAttribRunOnce + ")"); + attribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT; + log("init (oncePerRequest=" + oncePerRequest + ", attribRunOnce=" + attribRunOnce + ")"); init(); } @@ -185,7 +185,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * client request for a resource at the end of the chain. *

    * Subclasses should not override this method, but rather the - * abstract {@link #doFilterImpl doFilterImpl} method. + * abstract {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. * * @param pRequest the servlet request * @param pResponse the servlet response @@ -194,12 +194,12 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @throws IOException * @throws ServletException * - * @see Filter#doFilter Filter.doFilter - * @see #doFilterImpl doFilterImpl + * @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter + * @see #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilterImpl */ - public final void doFilter(ServletRequest pRequest, ServletResponse pResponse, FilterChain pFilterChain) throws IOException, ServletException { - // If request filter and allready run, continue chain and return fast - if (mOncePerRequest && isRunOnce(pRequest)) { + public final void doFilter(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pFilterChain) throws IOException, ServletException { + // If request filter and already run, continue chain and return fast + if (oncePerRequest && isRunOnce(pRequest)) { pFilterChain.doFilter(pRequest, pResponse); return; } @@ -225,20 +225,20 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @return {@code true} if the request is allready filtered, otherwise * {@code false}. */ - private boolean isRunOnce(ServletRequest pRequest) { - // If request allready filtered, return true (skip) - if (pRequest.getAttribute(mAttribRunOnce) == ATTRIB_RUN_ONCE_VALUE) { + private boolean isRunOnce(final ServletRequest pRequest) { + // If request already filtered, return true (skip) + if (pRequest.getAttribute(attribRunOnce) == ATTRIB_RUN_ONCE_VALUE) { return true; } // Set attribute and return false (continue) - pRequest.setAttribute(mAttribRunOnce, ATTRIB_RUN_ONCE_VALUE); + pRequest.setAttribute(attribRunOnce, ATTRIB_RUN_ONCE_VALUE); return false; } /** * Invoked once, or each time a request/response pair is passed through the - * chain, depending on the {@link #mOncePerRequest} member variable. + * chain, depending on the {@link #oncePerRequest} member variable. * * @param pRequest the servlet request * @param pResponse the servlet response @@ -247,9 +247,9 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @throws IOException if an I/O error occurs * @throws ServletException if an exception occurs during the filter process * - * @see #mOncePerRequest - * @see #doFilter doFilter - * @see Filter#doFilter Filter.doFilter + * @see #oncePerRequest + * @see #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilter + * @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter */ protected abstract void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException; @@ -262,7 +262,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl */ public void destroy() { log("destroy"); - mFilterConfig = null; + filterConfig = null; } /** @@ -273,7 +273,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @see FilterConfig#getFilterName */ public String getFilterName() { - return mFilterConfig.getFilterName(); + return filterConfig.getFilterName(); } /** @@ -287,7 +287,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl */ public ServletContext getServletContext() { // TODO: Create a servlet context wrapper that lets you log to a log4j appender? - return mFilterConfig.getServletContext(); + return filterConfig.getServletContext(); } /** @@ -299,8 +299,8 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @return a {@code String} containing the value of the initialization * parameter */ - public String getInitParameter(String pKey) { - return mFilterConfig.getInitParameter(pKey); + public String getInitParameter(final String pKey) { + return filterConfig.getInitParameter(pKey); } /** @@ -312,7 +312,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * containing the mNames of the servlet's initialization parameters */ public Enumeration getInitParameterNames() { - return mFilterConfig.getInitParameterNames(); + return filterConfig.getInitParameterNames(); } /** @@ -322,7 +322,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @param pMessage the log message * @see ServletContext#log(String) */ - protected void log(String pMessage) { + protected void log(final String pMessage) { getServletContext().log(getFilterName() + ": " + pMessage); } @@ -335,7 +335,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @param pThrowable the exception * @see ServletContext#log(String,Throwable) */ - protected void log(String pMessage, Throwable pThrowable) { + protected void log(final String pMessage, final Throwable pThrowable) { getServletContext().log(getFilterName() + ": " + pMessage, pThrowable); } @@ -347,12 +347,12 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * * @deprecated For compatibility only, use {@link #init init} instead. */ - public void setFilterConfig(FilterConfig pFilterConfig) { + public void setFilterConfig(final FilterConfig pFilterConfig) { try { init(pFilterConfig); } catch (ServletException e) { - log("Error in init(), see stacktrace for details.", e); + log("Error in init(), see stack trace for details.", e); } } @@ -363,7 +363,7 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * @see FilterConfig */ public FilterConfig getFilterConfig() { - return mFilterConfig; + return filterConfig; } /** @@ -374,10 +374,10 @@ public abstract class GenericFilter implements Filter, FilterConfig, Serializabl * * @param pOncePerRequest {@code true} if the filter should run only * once per request - * @see #mOncePerRequest + * @see #oncePerRequest */ - @InitParam - public void setOncePerRequest(boolean pOncePerRequest) { - mOncePerRequest = pOncePerRequest; + @InitParam(name = "once-per-request") + public void setOncePerRequest(final boolean pOncePerRequest) { + oncePerRequest = pOncePerRequest; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java index 60603c2e..60535841 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java @@ -41,13 +41,13 @@ import java.lang.reflect.InvocationTargetException; * the method matching the signature {@code void setX(<Type>)}, * for every init-parameter {@code x}. Both camelCase and lisp-style paramter * naming is supported, lisp-style names will be converted to camelCase. - * Parameter values are automatically converted from string represenation to - * most basic types, if neccessary. + * Parameter values are automatically converted from string representation to + * most basic types, if necessary. * * @author Harald Kuhr * @author last modified by $Author: haku $ * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java#1 $ + * @version $Id: GenericServlet.java#1 $ */ public abstract class GenericServlet extends javax.servlet.GenericServlet { @@ -70,9 +70,9 @@ public abstract class GenericServlet extends javax.servlet.GenericServlet { * @see BeanUtil#configure(Object, java.util.Map, boolean) */ @Override - public void init(ServletConfig pConfig) throws ServletException { + public void init(final ServletConfig pConfig) throws ServletException { if (pConfig == null) { - throw new ServletConfigException("servletconfig == null"); + throw new ServletConfigException("servlet config == null"); } try { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java index f4307131..da91650c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java @@ -41,13 +41,13 @@ import java.lang.reflect.InvocationTargetException; * the method matching the signature {@code void setX(<Type>)}, * for every init-parameter {@code x}. Both camelCase and lisp-style paramter * naming is supported, lisp-style names will be converted to camelCase. - * Parameter values are automatically converted from string represenation to - * most basic types, if neccessary. + * Parameter values are automatically converted from string representation to + * most basic types, if necessary. * * @author Harald Kuhr * @author last modified by $Author: haku $ * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java#1 $ + * @version $Id: HttpServlet.java#1 $ */ public abstract class HttpServlet extends javax.servlet.http.HttpServlet { @@ -63,7 +63,7 @@ public abstract class HttpServlet extends javax.servlet.http.HttpServlet { * have a matching setter method annotated with {@link InitParam}. * * @param pConfig the servlet config - * @throws ServletException if an error ouccured during init + * @throws ServletException if an error occurred during init * * @see javax.servlet.GenericServlet#init * @see #init() init @@ -72,7 +72,7 @@ public abstract class HttpServlet extends javax.servlet.http.HttpServlet { @Override public void init(ServletConfig pConfig) throws ServletException { if (pConfig == null) { - throw new ServletConfigException("servletconfig == null"); + throw new ServletConfigException("servlet config == null"); } try { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java b/servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java index 6fb02155..51e160c7 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java @@ -31,20 +31,25 @@ package com.twelvemonkeys.servlet; import java.lang.annotation.*; /** - * Annotation to be used by serlvets/filters, to have their init-method + * Annotation to be used by servlets/filters, to have their {@code init}-method * automatically convert and set values from their respective configuration. * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java#1 $ + * @version $Id: InitParam.java#1 $ + * @see com.twelvemonkeys.servlet.ServletConfigurator * @see com.twelvemonkeys.servlet.GenericFilter#init(javax.servlet.FilterConfig) * @see com.twelvemonkeys.servlet.GenericServlet#init(javax.servlet.ServletConfig) * @see com.twelvemonkeys.servlet.HttpServlet#init(javax.servlet.ServletConfig) */ +// TODO: Actually implement for version 3.0! @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ElementType.METHOD/*, TODO: ElementType.FIELD*/}) public @interface InitParam { - String name() default ""; + static final String UNDEFINED = ""; + String name() default UNDEFINED; + String defaultValue() default UNDEFINED; // TODO: Consider separate annotation? + boolean required() default false; // TODO: Consider separate annotation? } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java index 5dc3e325..35c9ca48 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.servlet; +import com.twelvemonkeys.lang.Validate; + import javax.servlet.ServletOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -61,13 +63,13 @@ import java.io.OutputStream; * * @author Harald Kuhr * @author $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java#1 $ + * @version $Id: OutputStreamAdapter.java#1 $ * */ public class OutputStreamAdapter extends ServletOutputStream { /** The wrapped {@code OutputStream}. */ - protected final OutputStream mOut; + protected final OutputStream out; /** * Creates an {@code OutputStreamAdapter}. @@ -76,11 +78,9 @@ public class OutputStreamAdapter extends ServletOutputStream { * * @throws IllegalArgumentException if {@code pOut} is {@code null}. */ - public OutputStreamAdapter(OutputStream pOut) { - if (pOut == null) { - throw new IllegalArgumentException("out == null"); - } - mOut = pOut; + public OutputStreamAdapter(final OutputStream pOut) { + Validate.notNull(pOut, "out"); + out = pOut; } /** @@ -89,11 +89,12 @@ public class OutputStreamAdapter extends ServletOutputStream { * @return the wrapped {@code OutputStream}. */ public OutputStream getOutputStream() { - return mOut; + return out; } + @Override public String toString() { - return "ServletOutputStream adapted from " + mOut.toString(); + return "ServletOutputStream adapted from " + out.toString(); } /** @@ -103,20 +104,17 @@ public class OutputStreamAdapter extends ServletOutputStream { * * @throws IOException if an error occurs during writing */ - public void write(int pByte) - throws IOException { - mOut.write(pByte); + public void write(final int pByte) throws IOException { + out.write(pByte); } // Overide for efficiency - public void write(byte pBytes[]) - throws IOException { - mOut.write(pBytes); + public void write(final byte pBytes[]) throws IOException { + out.write(pBytes); } // Overide for efficiency - public void write(byte pBytes[], int pOff, int pLen) - throws IOException { - mOut.write(pBytes, pOff, pLen); + public void write(final byte pBytes[], final int pOff, final int pLen) throws IOException { + out.write(pBytes, pOff, pLen); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java index 6f47968e..f66df68d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java @@ -66,16 +66,16 @@ import java.util.Enumeration; * @author Harald Kuhr * @author last modified by $Author: haku $ * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java#1 $ + * @version $Id: ProxyServlet.java#1 $ */ public class ProxyServlet extends GenericServlet { /** Remote server host name or IP address */ - protected String mRemoteServer = null; + protected String remoteServer = null; /** Remote server port */ - protected int mRemotePort = 80; + protected int remotePort = 80; /** Remote server "mount" path */ - protected String mRemotePath = ""; + protected String remotePath = ""; private static final String HTTP_REQUEST_HEADER_HOST = "host"; private static final String HTTP_RESPONSE_HEADER_SERVER = "server"; @@ -88,7 +88,7 @@ public class ProxyServlet extends GenericServlet { * @param pRemoteServer */ public void setRemoteServer(String pRemoteServer) { - mRemoteServer = pRemoteServer; + remoteServer = pRemoteServer; } /** @@ -99,7 +99,7 @@ public class ProxyServlet extends GenericServlet { */ public void setRemotePort(String pRemotePort) { try { - mRemotePort = Integer.parseInt(pRemotePort); + remotePort = Integer.parseInt(pRemotePort); } catch (NumberFormatException e) { log("RemotePort must be a number!", e); @@ -121,7 +121,7 @@ public class ProxyServlet extends GenericServlet { pRemotePath = "/" + pRemotePath; } - mRemotePath = pRemotePath; + remotePath = pRemotePath; } /** @@ -153,7 +153,7 @@ public class ProxyServlet extends GenericServlet { */ protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException { // Sanity check configuration - if (mRemoteServer == null) { + if (remoteServer == null) { log(MESSAGE_REMOTE_SERVER_NOT_CONFIGURED); pResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MESSAGE_REMOTE_SERVER_NOT_CONFIGURED); @@ -164,7 +164,7 @@ public class ProxyServlet extends GenericServlet { try { // Recreate request URI for remote request String requestURI = createRemoteRequestURI(pRequest); - URL remoteURL = new URL(pRequest.getScheme(), mRemoteServer, mRemotePort, requestURI); + URL remoteURL = new URL(pRequest.getScheme(), remoteServer, remotePort, requestURI); // Get connection, with method from original request // NOTE: The actual connection is not done before we ask for streams... @@ -319,7 +319,7 @@ public class ProxyServlet extends GenericServlet { * @return a {@code String} representing the remote request URI */ private String createRemoteRequestURI(HttpServletRequest pRequest) { - StringBuilder requestURI = new StringBuilder(mRemotePath); + StringBuilder requestURI = new StringBuilder(remotePath); requestURI.append(pRequest.getPathInfo()); if (!StringUtil.isEmpty(pRequest.getQueryString())) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java deleted file mode 100755 index 0665e1fd..00000000 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.twelvemonkeys.servlet; - -import com.twelvemonkeys.util.CollectionUtil; - -import javax.servlet.http.HttpServletRequest; -import java.util.Iterator; -import java.util.Enumeration; - -/** - * HeaderMap - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java#1 $ - */ -class SerlvetParametersMapAdapter extends AbstractServletMapAdapter { - - protected final HttpServletRequest mRequest; - - public SerlvetParametersMapAdapter(HttpServletRequest pRequest) { - if (pRequest == null) { - throw new IllegalArgumentException("request == null"); - } - mRequest = pRequest; - } - - protected Iterator valuesImpl(String pName) { - String[] values = mRequest.getParameterValues(pName); - return values == null ? null : CollectionUtil.iterator(values); - } - - protected Iterator keysImpl() { - //noinspection unchecked - Enumeration names = mRequest.getParameterNames(); - return names == null ? null : CollectionUtil.iterator(names); - } - -} \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java index 29ea534b..9fbcbaf8 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java @@ -34,10 +34,12 @@ import javax.servlet.ServletException; * ServletConfigException. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java#2 $ + * @version $Id: ServletConfigException.java#2 $ */ public class ServletConfigException extends ServletException { + // TODO: Parameters for init-param at fault, and possibly servlet name? + /** * Creates a {@code ServletConfigException} with the given message. * @@ -53,11 +55,10 @@ public class ServletConfigException extends ServletException { * @param pMessage the exception message * @param pCause the exception cause */ - public ServletConfigException(String pMessage, Throwable pCause) { + public ServletConfigException(final String pMessage, final Throwable pCause) { super(pMessage, pCause); - if (getCause() == null) { - initCause(pCause); - } + + maybeInitCause(pCause); } /** @@ -65,28 +66,16 @@ public class ServletConfigException extends ServletException { * * @param pCause the exception cause */ - public ServletConfigException(Throwable pCause) { - super("Erorr in Servlet configuration: " + pCause.getMessage(), pCause); + public ServletConfigException(final Throwable pCause) { + super(String.format("Error in Servlet configuration: %s", pCause.getMessage()), pCause); + + maybeInitCause(pCause); + } + + private void maybeInitCause(Throwable pCause) { + // Workaround for ServletExceptions that does not do proper exception chaining if (getCause() == null) { initCause(pCause); } } - - /** - * Gets the cause of this {@code ServletConfigException}. - * - * @return the cause, or {@code null} if unknown. - * @see #getRootCause() - */ -// public final Throwable getCause() { -// Throwable cause = super.getCause(); -// return cause != null ? cause : super.getRootCause(); -// } - - /** - * @deprecated Use {@link #getCause()} instead. - */ -// public final Throwable getRootCause() { -// return getCause(); -// } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java index db93a47f..6806f5ea 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java @@ -29,6 +29,7 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; import javax.servlet.FilterConfig; import javax.servlet.ServletConfig; @@ -44,7 +45,7 @@ import java.util.*; *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java#2 $ + * @version $Id: ServletConfigMapAdapter.java#2 $ */ class ServletConfigMapAdapter extends AbstractMap implements Map, Serializable, Cloneable { @@ -52,51 +53,48 @@ class ServletConfigMapAdapter extends AbstractMap implements Map ServletConfig, FilterConfig, ServletContext } -// private final boolean mIsServlet; - private final ConfigType mType; + private final ConfigType type; - private final ServletConfig mServletConfig; - private final FilterConfig mFilterConfig; - private final ServletContext mServletContext; + private final ServletConfig servletConfig; + private final FilterConfig filterConfig; + private final ServletContext servletContext; // Cache the entry set - private transient Set> mEntrySet; + private transient Set> entrySet; - public ServletConfigMapAdapter(ServletConfig pConfig) { + public ServletConfigMapAdapter(final ServletConfig pConfig) { this(pConfig, ConfigType.ServletConfig); } - public ServletConfigMapAdapter(FilterConfig pConfig) { + public ServletConfigMapAdapter(final FilterConfig pConfig) { this(pConfig, ConfigType.FilterConfig); } - public ServletConfigMapAdapter(ServletContext pContext) { + public ServletConfigMapAdapter(final ServletContext pContext) { this(pContext, ConfigType.ServletContext); } - private ServletConfigMapAdapter(Object pConfig, ConfigType pType) { - if (pConfig == null) { - // Could happen of client code invokes with null reference - throw new IllegalArgumentException("Config == null"); - } + private ServletConfigMapAdapter(final Object pConfig, final ConfigType pType) { + // Could happen if client code invokes with null reference + Validate.notNull(pConfig, "config"); - mType = pType; + type = pType; - switch (mType) { + switch (type) { case ServletConfig: - mServletConfig = (ServletConfig) pConfig; - mFilterConfig = null; - mServletContext = null; + servletConfig = (ServletConfig) pConfig; + filterConfig = null; + servletContext = null; break; case FilterConfig: - mServletConfig = null; - mFilterConfig = (FilterConfig) pConfig; - mServletContext = null; + servletConfig = null; + filterConfig = (FilterConfig) pConfig; + servletContext = null; break; case ServletContext: - mServletConfig = null; - mFilterConfig = null; - mServletContext = (ServletContext) pConfig; + servletConfig = null; + filterConfig = null; + servletContext = (ServletContext) pConfig; break; default: throw new IllegalArgumentException("Wrong type: " + pType); @@ -109,13 +107,13 @@ class ServletConfigMapAdapter extends AbstractMap implements Map * @return the servlet or filter name */ public final String getName() { - switch (mType) { + switch (type) { case ServletConfig: - return mServletConfig.getServletName(); + return servletConfig.getServletName(); case FilterConfig: - return mFilterConfig.getFilterName(); + return filterConfig.getFilterName(); case ServletContext: - return mServletContext.getServletContextName(); + return servletContext.getServletContextName(); default: throw new IllegalStateException(); } @@ -127,67 +125,67 @@ class ServletConfigMapAdapter extends AbstractMap implements Map * @return the servlet context */ public final ServletContext getServletContext() { - switch (mType) { + switch (type) { case ServletConfig: - return mServletConfig.getServletContext(); + return servletConfig.getServletContext(); case FilterConfig: - return mFilterConfig.getServletContext(); + return filterConfig.getServletContext(); case ServletContext: - return mServletContext; + return servletContext; default: throw new IllegalStateException(); } } public final Enumeration getInitParameterNames() { - switch (mType) { + switch (type) { case ServletConfig: - return mServletConfig.getInitParameterNames(); + return servletConfig.getInitParameterNames(); case FilterConfig: - return mFilterConfig.getInitParameterNames(); + return filterConfig.getInitParameterNames(); case ServletContext: - return mServletContext.getInitParameterNames(); + return servletContext.getInitParameterNames(); default: throw new IllegalStateException(); } } public final String getInitParameter(final String pName) { - switch (mType) { + switch (type) { case ServletConfig: - return mServletConfig.getInitParameter(pName); + return servletConfig.getInitParameter(pName); case FilterConfig: - return mFilterConfig.getInitParameter(pName); + return filterConfig.getInitParameter(pName); case ServletContext: - return mServletContext.getInitParameter(pName); + return servletContext.getInitParameter(pName); default: throw new IllegalStateException(); } } public Set> entrySet() { - if (mEntrySet == null) { - mEntrySet = createEntrySet(); + if (entrySet == null) { + entrySet = createEntrySet(); } - return mEntrySet; + return entrySet; } private Set> createEntrySet() { return new AbstractSet>() { // Cache size, if requested, -1 means not calculated - private int mSize = -1; + private int size = -1; public Iterator> iterator() { return new Iterator>() { // Iterator is backed by initParameterNames enumeration - final Enumeration mNames = getInitParameterNames(); + final Enumeration names = getInitParameterNames(); public boolean hasNext() { - return mNames.hasMoreElements(); + return names.hasMoreElements(); } public Entry next() { - final String key = (String) mNames.nextElement(); + final String key = (String) names.nextElement(); return new Entry() { public String getKey() { return key; @@ -236,11 +234,11 @@ class ServletConfigMapAdapter extends AbstractMap implements Map } public int size() { - if (mSize < 0) { - mSize = calculateSize(); + if (size < 0) { + size = calculateSize(); } - return mSize; + return size; } private int calculateSize() { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigurator.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigurator.java new file mode 100644 index 00000000..d04bfeee --- /dev/null +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigurator.java @@ -0,0 +1,241 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.FilterIterator; +import com.twelvemonkeys.util.convert.ConversionException; +import com.twelvemonkeys.util.convert.Converter; + +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * ServletConfigurator + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ServletConfigurator.java,v 1.0 Apr 30, 2010 2:51:38 PM haraldk Exp$ + * @see com.twelvemonkeys.servlet.InitParam + */ +final class ServletConfigurator { + // TODO: Rethink @InitParam? Allow annotation of method parameters instead? Allows setLocation(@InitParam int x, @InitParam int y) + // TODO: At least allow field injection + // TODO: defaultValue, required + + private ServletConfigurator() { + } + + public static void configure(final Servlet pServlet, final ServletConfig pConfig) throws ServletConfigException { + new Configurator(pServlet, pConfig.getServletName()).configure(ServletUtil.asMap(pConfig)); + } + + public static void configure(final Filter pFilter, final FilterConfig pConfig) throws ServletConfigException { + new Configurator(pFilter, pConfig.getFilterName()).configure(ServletUtil.asMap(pConfig)); + } + + private static class Configurator { + private final Object servletOrFilter; + private final String name; + + private Configurator(final Object servletOrFilter, final String name) { + this.servletOrFilter = servletOrFilter; + this.name = name; + } + + private void configure(final Map pMapping) throws ServletConfigException { + // Loop over methods with InitParam annotations + for (Method method : annotatedMethods(servletOrFilter.getClass(), InitParam.class)) { + assertAcceptableMethod(method); + + // Get value or default, throw exception if missing required value + Object value = getConfiguredValue(method, pMapping); + + if (value != null) { + // Inject value to this method + try { + method.invoke(servletOrFilter, value); + } + catch (IllegalAccessException e) { + // We know the method is accessible, so this should never happen + throw new Error(e); + } + catch (InvocationTargetException e) { + throw new ServletConfigException(String.format("Could not configure %s: %s", name, e.getCause().getMessage()), e.getCause()); + } + } + } + + // TODO: Loop over fields with InitParam annotations + + // TODO: Log warning for mappings not present among InitParam annotated methods? + } + + private Object getConfiguredValue(final Method method, final Map mapping) throws ServletConfigException { + InitParam initParam = method.getAnnotation(InitParam.class); + String paramName = getParameterName(method, initParam); + + // Get parameter value + String stringValue = mapping.get(paramName); + + if (stringValue == null && initParam.name().equals(InitParam.UNDEFINED)) { + stringValue = mapping.get(StringUtil.camelToLisp(paramName)); + } + + if (stringValue == null) { + // InitParam support required = true and throw exception if not present in map + if (initParam.required()) { + throw new ServletConfigException( + String.format( + "Could not configure %s: Required init-parameter \"%s\" of type %s is missing", + name, paramName, method.getParameterTypes()[0] + ) + ); + } + else if (!initParam.defaultValue().equals(InitParam.UNDEFINED)) { + // Support default values + stringValue = initParam.defaultValue(); + } + } + + // Convert value based on method arguments... + return stringValue == null ? null : convertValue(method, stringValue); + } + + private Object convertValue(final Method method, final String stringValue) throws ServletConfigException { + // We know it's a single parameter method + Class type = method.getParameterTypes()[0]; + + try { + return String.class.equals(type) ? stringValue : Converter.getInstance().toObject(stringValue, type); + } + catch (ConversionException e) { + throw new ServletConfigException(e); + } + } + + private String getParameterName(final Method method, final InitParam initParam) throws ServletConfigException { + String paramName = initParam.name(); + + if (paramName.equals(InitParam.UNDEFINED)) { + String methodName = method.getName(); + if (methodName.startsWith("set") && methodName.length() > 3) { + paramName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); + } + else { + throw new ServletConfigException( + String.format( + "Could not configure %s: InitParam annotated method must either specify name or follow Bean standard for properties (ie. setFoo => 'foo'): %s", + name, method + ) + ); + } + } + + return paramName; + } + + private void assertAcceptableMethod(final Method method) throws ServletConfigException { + // Try to use setAccessible, if not public + boolean isAccessible = Modifier.isPublic(method.getModifiers()); + + if (!isAccessible) { + try { + method.setAccessible(true); + isAccessible = true; + } + catch (SecurityException ignore) { + // Won't be accessible, we'll fail below + } + } + + if (!isAccessible || method.getReturnType() != Void.TYPE || method.getParameterTypes().length != 1) { + throw new ServletConfigException( + String.format( + "Could not configure %s: InitParam annotated method must be public void and have a single parameter argument list: %s", + name, method + ) + ); + } + } + + + /** + * Gets all methods annotated with the given annotations. + * + * @param pClass the class to get annotated methods from + * @param pAnnotations the annotations to test for + * @return an iterable that allows iterating over all methods with the given annotations. + */ + private Iterable annotatedMethods(final Class pClass, final Class... pAnnotations) { + return new Iterable() { + public Iterator iterator() { + Set methods = new LinkedHashSet(); + + Class cl = pClass; + while (cl.getSuperclass() != null) { // There's no annotations of interest on java.lang.Object + methods.addAll(Arrays.asList(cl.getDeclaredMethods())); + + // TODO: What about interface methods? Do we really want them? + Class[] interfaces = cl.getInterfaces(); + for (Class i : interfaces) { + methods.addAll(Arrays.asList(i.getDeclaredMethods())); + } + + cl = cl.getSuperclass(); + } + + return new FilterIterator(methods.iterator(), new FilterIterator.Filter() { + public boolean accept(final Method pMethod) { + for (Class annotation : pAnnotations) { + if (!pMethod.isAnnotationPresent(annotation) || isOverriddenWithAnnotation(pMethod, annotation)) { + return false; + } + } + + return true; + } + + /** + * @param pMethod the method to test for override + * @param pAnnotation the annotation that must be present + * @return {@code true} iff the method is overridden in a subclass, and has annotation + * @see The Java Language Specification: Classes: Inheritance, Overriding, and Hiding + */ + private boolean isOverriddenWithAnnotation(final Method pMethod, final Class pAnnotation) { + if (Modifier.isPrivate(pMethod.getModifiers())) { + return false; + } + + Class cl = pClass; + + // Loop down up from subclass to superclass declaring the method + while (cl != null && !pMethod.getDeclaringClass().equals(cl)) { + try { + Method override = cl.getDeclaredMethod(pMethod.getName(), pMethod.getParameterTypes()); + + // Overridden, test if it has the annotation present + if (override.isAnnotationPresent(pAnnotation)) { + return true; + } + + } + catch (NoSuchMethodException ignore) { + } + + cl = cl.getSuperclass(); + } + + return false; + } + }); + } + }; + } + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java similarity index 51% rename from servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java rename to servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java index 3db799fe..76054e11 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapter.java @@ -1,5 +1,6 @@ package com.twelvemonkeys.servlet; +import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.util.CollectionUtil; import javax.servlet.http.HttpServletRequest; @@ -7,33 +8,29 @@ import java.util.Enumeration; import java.util.Iterator; /** - * HeaderMap + * ServletHeadersMapAdapter * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java#1 $ + * @version $Id: ServletHeadersMapAdapter.java#1 $ */ -class SerlvetHeadersMapAdapter extends AbstractServletMapAdapter { +class ServletHeadersMapAdapter extends AbstractServletMapAdapter { - protected final HttpServletRequest mRequest; + protected final HttpServletRequest request; - public SerlvetHeadersMapAdapter(HttpServletRequest pRequest) { - if (pRequest == null) { - throw new IllegalArgumentException("request == null"); - } - mRequest = pRequest; + public ServletHeadersMapAdapter(HttpServletRequest pRequest) { + request = Validate.notNull(pRequest, "request"); } - protected Iterator valuesImpl(String pName) { //noinspection unchecked - Enumeration headers = mRequest.getHeaders(pName); + Enumeration headers = request.getHeaders(pName); return headers == null ? null : CollectionUtil.iterator(headers); } protected Iterator keysImpl() { //noinspection unchecked - Enumeration headerNames = mRequest.getHeaderNames(); + Enumeration headerNames = request.getHeaderNames(); return headerNames == null ? null : CollectionUtil.iterator(headerNames); } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java new file mode 100755 index 00000000..01dca170 --- /dev/null +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletParametersMapAdapter.java @@ -0,0 +1,36 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.util.CollectionUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * ServletParametersMapAdapter + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: ServletParametersMapAdapter.java#1 $ + */ +class ServletParametersMapAdapter extends AbstractServletMapAdapter { + + protected final HttpServletRequest request; + + public ServletParametersMapAdapter(HttpServletRequest pRequest) { + request = Validate.notNull(pRequest, "request"); + } + + protected Iterator valuesImpl(String pName) { + String[] values = request.getParameterValues(pName); + return values == null ? null : CollectionUtil.iterator(values); + } + + protected Iterator keysImpl() { + //noinspection unchecked + Enumeration names = request.getParameterNames(); + return names == null ? null : CollectionUtil.iterator(names); + } + +} \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java index f9de3105..853c5a6c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.servlet; +import com.twelvemonkeys.lang.Validate; + import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; import java.io.IOException; @@ -40,47 +42,44 @@ import java.io.OutputStream; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java#2 $ + * @version $Id: ServletResponseStreamDelegate.java#2 $ */ public class ServletResponseStreamDelegate { - private Object mOut = null; - protected final ServletResponse mResponse; + private Object out = null; + protected final ServletResponse response; public ServletResponseStreamDelegate(ServletResponse pResponse) { - if (pResponse == null) { - throw new IllegalArgumentException("response == null"); - } - mResponse = pResponse; + response = Validate.notNull(pResponse, "response"); } // NOTE: Intentionally NOT threadsafe, as one request/response should be // handled by one thread ONLY. public final ServletOutputStream getOutputStream() throws IOException { - if (mOut == null) { + if (out == null) { OutputStream out = createOutputStream(); - mOut = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out); + this.out = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out); } - else if (mOut instanceof PrintWriter) { + else if (out instanceof PrintWriter) { throw new IllegalStateException("getWriter() allready called."); } - return (ServletOutputStream) mOut; + return (ServletOutputStream) out; } // NOTE: Intentionally NOT threadsafe, as one request/response should be // handled by one thread ONLY. public final PrintWriter getWriter() throws IOException { - if (mOut == null) { + if (out == null) { // NOTE: getCharacterEncoding may should not return null OutputStream out = createOutputStream(); - String charEncoding = mResponse.getCharacterEncoding(); - mOut = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out)); + String charEncoding = response.getCharacterEncoding(); + this.out = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out)); } - else if (mOut instanceof ServletOutputStream) { + else if (out instanceof ServletOutputStream) { throw new IllegalStateException("getOutputStream() allready called."); } - return (PrintWriter) mOut; + return (PrintWriter) out; } /** @@ -95,20 +94,20 @@ public class ServletResponseStreamDelegate { * @throws IOException if an I/O exception occurs */ protected OutputStream createOutputStream() throws IOException { - return mResponse.getOutputStream(); + return response.getOutputStream(); } public void flushBuffer() throws IOException { - if (mOut instanceof ServletOutputStream) { - ((ServletOutputStream) mOut).flush(); + if (out instanceof ServletOutputStream) { + ((ServletOutputStream) out).flush(); } - else if (mOut != null) { - ((PrintWriter) mOut).flush(); + else if (out != null) { + ((PrintWriter) out).flush(); } } public void resetBuffer() { // TODO: Is this okay? Probably not... :-) - mOut = null; + out = null; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java index c93af83b..0892075e 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java @@ -37,15 +37,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.File; -import java.io.PrintStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URL; -import java.util.Date; -import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -56,57 +53,57 @@ import java.util.Map; * @author Harald Kuhr * @author Eirik Torske * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java#3 $ + * @version $Id: ServletUtil.java#3 $ */ public final class ServletUtil { /** - * "javax.servlet.include.request_uri" + * {@code "javax.servlet.include.request_uri"} */ private final static String ATTRIB_INC_REQUEST_URI = "javax.servlet.include.request_uri"; /** - * "javax.servlet.include.context_path" + * {@code "javax.servlet.include.context_path"} */ private final static String ATTRIB_INC_CONTEXT_PATH = "javax.servlet.include.context_path"; /** - * "javax.servlet.include.servlet_path" + * {@code "javax.servlet.include.servlet_path"} */ private final static String ATTRIB_INC_SERVLET_PATH = "javax.servlet.include.servlet_path"; /** - * "javax.servlet.include.path_info" + * {@code "javax.servlet.include.path_info"} */ private final static String ATTRIB_INC_PATH_INFO = "javax.servlet.include.path_info"; /** - * "javax.servlet.include.query_string" + * {@code "javax.servlet.include.query_string"} */ private final static String ATTRIB_INC_QUERY_STRING = "javax.servlet.include.query_string"; /** - * "javax.servlet.forward.request_uri" + * {@code "javax.servlet.forward.request_uri"} */ private final static String ATTRIB_FWD_REQUEST_URI = "javax.servlet.forward.request_uri"; /** - * "javax.servlet.forward.context_path" + * {@code "javax.servlet.forward.context_path"} */ private final static String ATTRIB_FWD_CONTEXT_PATH = "javax.servlet.forward.context_path"; /** - * "javax.servlet.forward.servlet_path" + * {@code "javax.servlet.forward.servlet_path"} */ private final static String ATTRIB_FWD_SERVLET_PATH = "javax.servlet.forward.servlet_path"; /** - * "javax.servlet.forward.path_info" + * {@code "javax.servlet.forward.path_info"} */ private final static String ATTRIB_FWD_PATH_INFO = "javax.servlet.forward.path_info"; /** - * "javax.servlet.forward.query_string" + * {@code "javax.servlet.forward.query_string"} */ private final static String ATTRIB_FWD_QUERY_STRING = "javax.servlet.forward.query_string"; @@ -126,10 +123,10 @@ public final class ServletUtil { * @return the value of the parameter, or the default value, if the * parameter is not set. */ - public static String getParameter(ServletRequest pReq, String pName, String pDefault) { + public static String getParameter(final ServletRequest pReq, final String pName, final String pDefault) { String str = pReq.getParameter(pName); - return ((str != null) ? str : pDefault); + return str != null ? str : pDefault; } /** @@ -148,13 +145,10 @@ public final class ServletUtil { * non-{@code null} and not an instance of {@code pType} * @throws NullPointerException if {@code pReq}, {@code pName} or * {@code pType} is {@code null}. - * @todo Well, it's done. Need some thinking... + * @todo Well, it's done. Need some thinking... We probably don't want default if conversion fails... * @see Converter#toObject */ - - // public static T getParameter(ServletRequest pReq, String pName, - // String pFormat, T pDefault) { - static T getParameter(ServletRequest pReq, String pName, Class pType, String pFormat, T pDefault) { + static T getParameter(final ServletRequest pReq, final String pName, final Class pType, final String pFormat, final T pDefault) { // Test if pDefault is either null or instance of pType if (pDefault != null && !pType.isInstance(pDefault)) { throw new IllegalArgumentException("default value not instance of " + pType + ": " + pDefault.getClass()); @@ -165,6 +159,7 @@ public final class ServletUtil { if (str == null) { return pDefault; } + try { return pType.cast(Converter.getInstance().toObject(str, pType, pFormat)); } @@ -175,20 +170,20 @@ public final class ServletUtil { /** * Gets the value of the given parameter from the request converted to - * a boolean. If the parameter is not set or not parseable, the default + * a {@code boolean}. If the parameter is not set or not parseable, the default * value is returned. * * @param pReq the servlet request * @param pName the parameter name * @param pDefault the default value - * @return the value of the parameter converted to a boolean, or the + * @return the value of the parameter converted to a {@code boolean}, or the * default value, if the parameter is not set. */ - public static boolean getBooleanParameter(ServletRequest pReq, String pName, boolean pDefault) { + public static boolean getBooleanParameter(final ServletRequest pReq, final String pName, final boolean pDefault) { String str = pReq.getParameter(pName); try { - return ((str != null) ? Boolean.valueOf(str) : pDefault); + return str != null ? Boolean.valueOf(str) : pDefault; } catch (NumberFormatException nfe) { return pDefault; @@ -197,20 +192,20 @@ public final class ServletUtil { /** * Gets the value of the given parameter from the request converted to - * an int. If the parameter is not set or not parseable, the default + * an {@code int}. If the parameter is not set or not parseable, the default * value is returned. * * @param pReq the servlet request * @param pName the parameter name * @param pDefault the default value - * @return the value of the parameter converted to an int, or the default + * @return the value of the parameter converted to an {@code int}, or the default * value, if the parameter is not set. */ - public static int getIntParameter(ServletRequest pReq, String pName, int pDefault) { + public static int getIntParameter(final ServletRequest pReq, final String pName, final int pDefault) { String str = pReq.getParameter(pName); try { - return ((str != null) ? Integer.parseInt(str) : pDefault); + return str != null ? Integer.parseInt(str) : pDefault; } catch (NumberFormatException nfe) { return pDefault; @@ -219,20 +214,20 @@ public final class ServletUtil { /** * Gets the value of the given parameter from the request converted to - * an long. If the parameter is not set or not parseable, the default + * an {@code long}. If the parameter is not set or not parseable, the default * value is returned. * * @param pReq the servlet request * @param pName the parameter name * @param pDefault the default value - * @return the value of the parameter converted to an long, or the default + * @return the value of the parameter converted to an {@code long}, or the default * value, if the parameter is not set. */ - public static long getLongParameter(ServletRequest pReq, String pName, long pDefault) { + public static long getLongParameter(final ServletRequest pReq, final String pName, final long pDefault) { String str = pReq.getParameter(pName); try { - return ((str != null) ? Long.parseLong(str) : pDefault); + return str != null ? Long.parseLong(str) : pDefault; } catch (NumberFormatException nfe) { return pDefault; @@ -241,20 +236,20 @@ public final class ServletUtil { /** * Gets the value of the given parameter from the request converted to - * a float. If the parameter is not set or not parseable, the default + * a {@code float}. If the parameter is not set or not parseable, the default * value is returned. * * @param pReq the servlet request * @param pName the parameter name * @param pDefault the default value - * @return the value of the parameter converted to a float, or the default + * @return the value of the parameter converted to a {@code float}, or the default * value, if the parameter is not set. */ - public static float getFloatParameter(ServletRequest pReq, String pName, float pDefault) { + public static float getFloatParameter(final ServletRequest pReq, final String pName, final float pDefault) { String str = pReq.getParameter(pName); try { - return ((str != null) ? Float.parseFloat(str) : pDefault); + return str != null ? Float.parseFloat(str) : pDefault; } catch (NumberFormatException nfe) { return pDefault; @@ -263,20 +258,20 @@ public final class ServletUtil { /** * Gets the value of the given parameter from the request converted to - * a double. If the parameter is not set or not parseable, the default + * a {@code double}. If the parameter is not set or not parseable, the default * value is returned. * * @param pReq the servlet request * @param pName the parameter name * @param pDefault the default value - * @return the value of the parameter converted to n double, or the default + * @return the value of the parameter converted to n {@code double}, or the default * value, if the parameter is not set. */ - public static double getDoubleParameter(ServletRequest pReq, String pName, double pDefault) { + public static double getDoubleParameter(final ServletRequest pReq, final String pName, final double pDefault) { String str = pReq.getParameter(pName); try { - return ((str != null) ? Double.parseDouble(str) : pDefault); + return str != null ? Double.parseDouble(str) : pDefault; } catch (NumberFormatException nfe) { return pDefault; @@ -285,20 +280,20 @@ public final class ServletUtil { /** * Gets the value of the given parameter from the request converted to - * a Date. If the parameter is not set or not parseable, the + * a {@code Date}. If the parameter is not set or not parseable, the * default value is returned. * * @param pReq the servlet request * @param pName the parameter name * @param pDefault the default value - * @return the value of the parameter converted to a Date, or the + * @return the value of the parameter converted to a {@code Date}, or the * default value, if the parameter is not set. * @see com.twelvemonkeys.lang.StringUtil#toDate(String) */ - public static long getDateParameter(ServletRequest pReq, String pName, long pDefault) { + public static long getDateParameter(final ServletRequest pReq, final String pName, final long pDefault) { String str = pReq.getParameter(pName); try { - return ((str != null) ? StringUtil.toDate(str).getTime() : pDefault); + return str != null ? StringUtil.toDate(str).getTime() : pDefault; } catch (IllegalArgumentException iae) { return pDefault; @@ -341,7 +336,7 @@ public final class ServletUtil { * @deprecated Use {@link javax.servlet.http.HttpServletRequest#getRequestURL()} * instead. */ - static StringBuffer buildHTTPURL(HttpServletRequest pRequest) { + static StringBuffer buildHTTPURL(final HttpServletRequest pRequest) { StringBuffer resultURL = new StringBuffer(); // Scheme, as in http, https, ftp etc @@ -381,7 +376,7 @@ public final class ServletUtil { * @see HttpServletRequest#getRequestURI * @since Servlet 2.2 */ - public static String getIncludeRequestURI(ServletRequest pRequest) { + public static String getIncludeRequestURI(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_INC_REQUEST_URI); } @@ -395,7 +390,7 @@ public final class ServletUtil { * @see HttpServletRequest#getContextPath * @since Servlet 2.2 */ - public static String getIncludeContextPath(ServletRequest pRequest) { + public static String getIncludeContextPath(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_INC_CONTEXT_PATH); } @@ -409,7 +404,7 @@ public final class ServletUtil { * @see HttpServletRequest#getServletPath * @since Servlet 2.2 */ - public static String getIncludeServletPath(ServletRequest pRequest) { + public static String getIncludeServletPath(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_INC_SERVLET_PATH); } @@ -423,7 +418,7 @@ public final class ServletUtil { * @see HttpServletRequest#getPathInfo * @since Servlet 2.2 */ - public static String getIncludePathInfo(ServletRequest pRequest) { + public static String getIncludePathInfo(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_INC_PATH_INFO); } @@ -437,7 +432,7 @@ public final class ServletUtil { * @see HttpServletRequest#getQueryString * @since Servlet 2.2 */ - public static String getIncludeQueryString(ServletRequest pRequest) { + public static String getIncludeQueryString(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_INC_QUERY_STRING); } @@ -451,7 +446,7 @@ public final class ServletUtil { * @see HttpServletRequest#getRequestURI * @since Servlet 2.4 */ - public static String getForwardRequestURI(ServletRequest pRequest) { + public static String getForwardRequestURI(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_FWD_REQUEST_URI); } @@ -465,7 +460,7 @@ public final class ServletUtil { * @see HttpServletRequest#getContextPath * @since Servlet 2.4 */ - public static String getForwardContextPath(ServletRequest pRequest) { + public static String getForwardContextPath(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_FWD_CONTEXT_PATH); } @@ -479,7 +474,7 @@ public final class ServletUtil { * @see HttpServletRequest#getServletPath * @since Servlet 2.4 */ - public static String getForwardServletPath(ServletRequest pRequest) { + public static String getForwardServletPath(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_FWD_SERVLET_PATH); } @@ -493,7 +488,7 @@ public final class ServletUtil { * @see HttpServletRequest#getPathInfo * @since Servlet 2.4 */ - public static String getForwardPathInfo(ServletRequest pRequest) { + public static String getForwardPathInfo(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_FWD_PATH_INFO); } @@ -507,7 +502,7 @@ public final class ServletUtil { * @see HttpServletRequest#getQueryString * @since Servlet 2.4 */ - public static String getForwardQueryString(ServletRequest pRequest) { + public static String getForwardQueryString(final ServletRequest pRequest) { return (String) pRequest.getAttribute(ATTRIB_FWD_QUERY_STRING); } @@ -519,7 +514,7 @@ public final class ServletUtil { * @todo Read the spec, seems to be a mismatch with the Servlet API... * @see javax.servlet.http.HttpServletRequest#getServletPath() */ - static String getScriptName(HttpServletRequest pRequest) { + static String getScriptName(final HttpServletRequest pRequest) { String requestURI = pRequest.getRequestURI(); return StringUtil.getLastElement(requestURI, "/"); } @@ -536,11 +531,13 @@ public final class ServletUtil { * @param pRequest the current HTTP request * @return the request URI relative to the current context path. */ - public static String getContextRelativeURI(HttpServletRequest pRequest) { + public static String getContextRelativeURI(final HttpServletRequest pRequest) { String context = pRequest.getContextPath(); + if (!StringUtil.isEmpty(context)) { // "" for root context return pRequest.getRequestURI().substring(context.length()); } + return pRequest.getRequestURI(); } @@ -557,12 +554,14 @@ public final class ServletUtil { * @see ServletContext#getRealPath(java.lang.String) * @see ServletContext#getResource(java.lang.String) */ - public static URL getRealURL(ServletContext pContext, String pPath) throws MalformedURLException { + public static URL getRealURL(final ServletContext pContext, final String pPath) throws MalformedURLException { String realPath = pContext.getRealPath(pPath); + if (realPath != null) { // NOTE: First convert to URI, as of Java 6 File.toURL is deprecated return new File(realPath).toURI().toURL(); } + return null; } @@ -572,20 +571,19 @@ public final class ServletUtil { * @param pContext the servlet context * @return the temp directory */ - public static File getTempDir(ServletContext pContext) { + public static File getTempDir(final ServletContext pContext) { return (File) pContext.getAttribute("javax.servlet.context.tempdir"); } /** - * Gets the identificator string containing the unique identifier assigned - * to this session. + * Gets the unique identifier assigned to this session. * The identifier is assigned by the servlet container and is implementation * dependent. * * @param pRequest The HTTP servlet request object. * @return the session Id */ - public static String getSessionId(HttpServletRequest pRequest) { + public static String getSessionId(final HttpServletRequest pRequest) { HttpSession session = pRequest.getSession(); return (session != null) ? session.getId() : null; @@ -598,11 +596,11 @@ public final class ServletUtil { * operations and iterating over it's {@code keySet}. * For other operations it may not perform well. * - * @param pConfig the serlvet configuration + * @param pConfig the servlet configuration * @return a {@code Map} view of the config * @throws IllegalArgumentException if {@code pConfig} is {@code null} */ - public static Map asMap(ServletConfig pConfig) { + public static Map asMap(final ServletConfig pConfig) { return new ServletConfigMapAdapter(pConfig); } @@ -617,7 +615,7 @@ public final class ServletUtil { * @return a {@code Map} view of the config * @throws IllegalArgumentException if {@code pConfig} is {@code null} */ - public static Map asMap(FilterConfig pConfig) { + public static Map asMap(final FilterConfig pConfig) { return new ServletConfigMapAdapter(pConfig); } @@ -636,6 +634,13 @@ public final class ServletUtil { return new ServletConfigMapAdapter(pContext); } + // TODO? +// public static Map attributesAsMap(final ServletContext pContext) { +// } +// +// public static Map attributesAsMap(final ServletRequest pRequest) { +// } +// /** * Creates an unmodifiable {@code Map} view of the given * {@code HttpServletRequest}s request parameters. @@ -645,7 +650,7 @@ public final class ServletUtil { * @throws IllegalArgumentException if {@code pRequest} is {@code null} */ public static Map> parametersAsMap(final HttpServletRequest pRequest) { - return new SerlvetParametersMapAdapter(pRequest); + return new ServletParametersMapAdapter(pRequest); } /** @@ -657,7 +662,7 @@ public final class ServletUtil { * @throws IllegalArgumentException if {@code pRequest} is {@code null} */ public static Map> headersAsMap(final HttpServletRequest pRequest) { - return new SerlvetHeadersMapAdapter(pRequest); + return new ServletHeadersMapAdapter(pRequest); } /** @@ -700,329 +705,22 @@ public final class ServletUtil { return pImplementation; } - - /** - * Prints the init parameters in a {@code javax.servlet.ServletConfig} - * object to a {@code java.io.PrintStream}. - *

    - * - * @param pServletConfig The Servlet Config object. - * @param pPrintStream The {@code java.io.PrintStream} for flushing - * the results. - */ - public static void printDebug(final ServletConfig pServletConfig, final PrintStream pPrintStream) { - Enumeration parameterNames = pServletConfig.getInitParameterNames(); - - while (parameterNames.hasMoreElements()) { - String initParameterName = (String) parameterNames.nextElement(); - - pPrintStream.println(initParameterName + ": " + pServletConfig.getInitParameter(initParameterName)); - } - } - - /** - * Prints the init parameters in a {@code javax.servlet.ServletConfig} - * object to {@code System.out}. - * - * @param pServletConfig the Servlet Config object. - */ - public static void printDebug(final ServletConfig pServletConfig) { - printDebug(pServletConfig, System.out); - } - - /** - * Prints the init parameters in a {@code javax.servlet.ServletContext} - * object to a {@code java.io.PrintStream}. - * - * @param pServletContext the Servlet Context object. - * @param pPrintStream the {@code java.io.PrintStream} for flushing the - * results. - */ - public static void printDebug(final ServletContext pServletContext, final PrintStream pPrintStream) { - Enumeration parameterNames = pServletContext.getInitParameterNames(); - - while (parameterNames.hasMoreElements()) { - String initParameterName = (String) parameterNames.nextElement(); - - pPrintStream.println(initParameterName + ": " + pServletContext.getInitParameter(initParameterName)); - } - } - - /** - * Prints the init parameters in a {@code javax.servlet.ServletContext} - * object to {@code System.out}. - * - * @param pServletContext The Servlet Context object. - */ - public static void printDebug(final ServletContext pServletContext) { - printDebug(pServletContext, System.out); - } - - /** - * Prints an excerpt of the residing information in a - * {@code javax.servlet.http.HttpServletRequest} object to a - * {@code java.io.PrintStream}. - * - * @param pRequest The HTTP servlet request object. - * @param pPrintStream The {@code java.io.PrintStream} for flushing - * the results. - */ - public static void printDebug(final HttpServletRequest pRequest, final PrintStream pPrintStream) { - String indentation = " "; - StringBuilder buffer = new StringBuilder(); - - // Returns the name of the authentication scheme used to protect the - // servlet, for example, "BASIC" or "SSL," or null if the servlet was - // not protected. - buffer.append(indentation); - buffer.append("Authentication scheme: "); - buffer.append(pRequest.getAuthType()); - buffer.append("\n"); - - // Returns the portion of the request URI that indicates the context - // of the request. - buffer.append(indentation); - buffer.append("Context path: "); - buffer.append(pRequest.getContextPath()); - buffer.append("\n"); - - // Returns an enumeration of all the header mNames this request contains. - buffer.append(indentation); - buffer.append("Header:"); - buffer.append("\n"); - Enumeration headerNames = pRequest.getHeaderNames(); - - while (headerNames.hasMoreElements()) { - String headerElement = (String) headerNames.nextElement(); - - buffer.append(indentation); - buffer.append(indentation); - buffer.append(headerElement); - buffer.append(": "); - buffer.append(pRequest.getHeader(headerElement)); - buffer.append("\n"); - } - - // Returns the name of the HTTP method with which this request was made, - // for example, GET, POST, or PUT. - buffer.append(indentation); - buffer.append("HTTP method: "); - buffer.append(pRequest.getMethod()); - buffer.append("\n"); - - // Returns any extra path information associated with the URL the client - // sent when it made this request. - buffer.append(indentation); - buffer.append("Extra path information from client: "); - buffer.append(pRequest.getPathInfo()); - buffer.append("\n"); - - // Returns any extra path information after the servlet name but before - // the query string, and translates it to a real path. - buffer.append(indentation); - buffer.append("Extra translated path information from client: "); - buffer.append(pRequest.getPathTranslated()); - buffer.append("\n"); - - // Returns the login of the user making this request, if the user has - // been authenticated, or null if the user has not been authenticated. - buffer.append(indentation); - String userInfo = pRequest.getRemoteUser(); - - if (StringUtil.isEmpty(userInfo)) { - buffer.append("User is not authenticated"); - } - else { - buffer.append("User logint: "); - buffer.append(userInfo); - } - buffer.append("\n"); - - // Returns the session ID specified by the client. - buffer.append(indentation); - buffer.append("Session ID from client: "); - buffer.append(pRequest.getRequestedSessionId()); - buffer.append("\n"); - - // Returns the server name. - buffer.append(indentation); - buffer.append("Server name: "); - buffer.append(pRequest.getServerName()); - buffer.append("\n"); - - // Returns the part of this request's URL from the protocol name up - // to the query string in the first line of the HTTP request. - buffer.append(indentation); - buffer.append("Request URI: ").append(pRequest.getRequestURI()); - buffer.append("\n"); - - // Returns the path info. - buffer.append(indentation); - buffer.append("Path information: ").append(pRequest.getPathInfo()); - buffer.append("\n"); - - // Returns the part of this request's URL that calls the servlet. - buffer.append(indentation); - buffer.append("Servlet path: ").append(pRequest.getServletPath()); - buffer.append("\n"); - - // Returns the query string that is contained in the request URL after - // the path. - buffer.append(indentation); - buffer.append("Query string: ").append(pRequest.getQueryString()); - buffer.append("\n"); - - // Returns an enumeration of all the parameters bound to this request. - buffer.append(indentation); - buffer.append("Parameters:"); - buffer.append("\n"); - Enumeration parameterNames = pRequest.getParameterNames(); - while (parameterNames.hasMoreElements()) { - String parameterName = (String) parameterNames.nextElement(); - - buffer.append(indentation); - buffer.append(indentation); - buffer.append(parameterName); - buffer.append(": "); - buffer.append(pRequest.getParameter(parameterName)); - buffer.append("\n"); - } - - // Returns an enumeration of all the attribute objects bound to this - // request. - buffer.append(indentation); - buffer.append("Attributes:"); - buffer.append("\n"); - Enumeration attributeNames = pRequest.getAttributeNames(); - while (attributeNames.hasMoreElements()) { - String attributeName = (String) attributeNames.nextElement(); - - buffer.append(indentation); - buffer.append(indentation); - buffer.append(attributeName); - buffer.append(": "); - buffer.append(pRequest.getAttribute(attributeName).toString()); - buffer.append("\n"); - } - pPrintStream.println(buffer.toString()); - } - - /** - * Prints an excerpt of the residing information in a - * {@code javax.servlet.http.HttpServletRequest} object to - * {@code System.out}. - * - * @param pRequest The HTTP servlet request object. - */ - public static void printDebug(final HttpServletRequest pRequest) { - printDebug(pRequest, System.out); - } - - /** - * Prints an excerpt of a {@code javax.servlet.http.HttpSession} object - * to a {@code java.io.PrintStream}. - * - * @param pHttpSession The HTTP Session object. - * @param pPrintStream The {@code java.io.PrintStream} for flushing - * the results. - */ - public static void printDebug(final HttpSession pHttpSession, final PrintStream pPrintStream) { - String indentation = " "; - StringBuilder buffer = new StringBuilder(); - - if (pHttpSession == null) { - buffer.append(indentation); - buffer.append("No session object available"); - buffer.append("\n"); - } - else { - - // Returns a string containing the unique identifier assigned to - //this session - buffer.append(indentation); - buffer.append("Session ID: ").append(pHttpSession.getId()); - buffer.append("\n"); - - // Returns the last time the client sent a request associated with - // this session, as the number of milliseconds since midnight - // January 1, 1970 GMT, and marked by the time the container - // recieved the request - buffer.append(indentation); - buffer.append("Last accessed time: "); - buffer.append(new Date(pHttpSession.getLastAccessedTime())); - buffer.append("\n"); - - // Returns the time when this session was created, measured in - // milliseconds since midnight January 1, 1970 GMT - buffer.append(indentation); - buffer.append("Creation time: "); - buffer.append(new Date(pHttpSession.getCreationTime())); - buffer.append("\n"); - - // Returns true if the client does not yet know about the session - // or if the client chooses not to join the session - buffer.append(indentation); - buffer.append("New session?: "); - buffer.append(pHttpSession.isNew()); - buffer.append("\n"); - - // Returns the maximum time interval, in seconds, that the servlet - // container will keep this session open between client accesses - buffer.append(indentation); - buffer.append("Max inactive interval: "); - buffer.append(pHttpSession.getMaxInactiveInterval()); - buffer.append("\n"); - - // Returns an enumeration of all the attribute objects bound to - // this session - buffer.append(indentation); - buffer.append("Attributes:"); - buffer.append("\n"); - Enumeration attributeNames = pHttpSession.getAttributeNames(); - - while (attributeNames.hasMoreElements()) { - String attributeName = (String) attributeNames.nextElement(); - - buffer.append(indentation); - buffer.append(indentation); - buffer.append(attributeName); - buffer.append(": "); - buffer.append(pHttpSession.getAttribute(attributeName).toString()); - buffer.append("\n"); - } - } - pPrintStream.println(buffer.toString()); - } - - /** - * Prints an excerpt of a {@code javax.servlet.http.HttpSession} - * object to {@code System.out}. - *

    - * - * @param pHttpSession The HTTP Session object. - */ - public static void printDebug(final HttpSession pHttpSession) { - printDebug(pHttpSession, System.out); - } - private static class HttpServletResponseHandler implements InvocationHandler { - private ServletResponse mResponse; - private HttpServletResponse mHttpResponse; + private final ServletResponseWrapper response; - HttpServletResponseHandler(ServletResponseWrapper pResponse) { - mResponse = pResponse; - mHttpResponse = (HttpServletResponse) pResponse.getResponse(); + HttpServletResponseHandler(final ServletResponseWrapper pResponse) { + response = pResponse; } - public Object invoke(Object pProxy, Method pMethod, Object[] pArgs) throws Throwable { + public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArgs) throws Throwable { try { - if (pMethod.getDeclaringClass().isInstance(mResponse)) { - //System.out.println("Invoking " + pMethod + " on wrapper"); - return pMethod.invoke(mResponse, pArgs); + // TODO: Allow partial implementing? + if (pMethod.getDeclaringClass().isInstance(response)) { + return pMethod.invoke(response, pArgs); } + // Method is not implemented in wrapper - //System.out.println("Invoking " + pMethod + " on wrapped object"); - return pMethod.invoke(mHttpResponse, pArgs); + return pMethod.invoke(response.getResponse(), pArgs); } catch (InvocationTargetException e) { // Unwrap, to avoid UndeclaredThrowableException... @@ -1032,23 +730,21 @@ public final class ServletUtil { } private static class HttpServletRequestHandler implements InvocationHandler { - private ServletRequest mRequest; - private HttpServletRequest mHttpRequest; + private final ServletRequestWrapper request; - HttpServletRequestHandler(ServletRequestWrapper pRequest) { - mRequest = pRequest; - mHttpRequest = (HttpServletRequest) pRequest.getRequest(); + HttpServletRequestHandler(final ServletRequestWrapper pRequest) { + request = pRequest; } - public Object invoke(Object pProxy, Method pMethod, Object[] pArgs) throws Throwable { + public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArgs) throws Throwable { try { - if (pMethod.getDeclaringClass().isInstance(mRequest)) { - //System.out.println("Invoking " + pMethod + " on wrapper"); - return pMethod.invoke(mRequest, pArgs); + // TODO: Allow partial implementing? + if (pMethod.getDeclaringClass().isInstance(request)) { + return pMethod.invoke(request, pArgs); } + // Method is not implemented in wrapper - //System.out.println("Invoking " + pMethod + " on wrapped object"); - return pMethod.invoke(mHttpRequest, pArgs); + return pMethod.invoke(request.getRequest(), pArgs); } catch (InvocationTargetException e) { // Unwrap, to avoid UndeclaredThrowableException... diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/StaticContentServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/StaticContentServlet.java new file mode 100644 index 00000000..63fcabb6 --- /dev/null +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/StaticContentServlet.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.io.FileUtil; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A minimal servlet that can serve static files. Also from outside the web application. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: StaticContentServlet.java,v 1.0 12.12.11 15:09 haraldk Exp$ + * + * @see #setRoot(java.io.File) + */ +public final class StaticContentServlet extends HttpServlet { + + private File root; + + /** + * Configures the file system {@code root} for this servlet. + * If {@code root} is a directory, files will be served relative to the directory. + * If {@code root} is a file, only this file may be served + * + * @param root the file system root. + */ + @InitParam(name = "root") + public void setRoot(final File root) { + this.root = root; + } + + @Override + public void init() throws ServletException { + if (root == null) { + throw new ServletConfigException("File system root not configured, check 'root' init-param"); + } + else if (!root.exists()) { + throw new ServletConfigException( + String.format("File system root '%s' does not exist, check 'root' init-param", root.getAbsolutePath()) + ); + } + + log(String.format("Serving %s '%s'", root.isDirectory() ? "files from directory" : "single file", root.getAbsolutePath())); + } + + @Override + protected long getLastModified(final HttpServletRequest request) { + File file = findFileForRequest(request); + + return file.exists() ? file.lastModified() : -1L; + } + + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException { + File file = findFileForRequest(request); + + if (file.isFile() && file.canRead()) { + // Normal file, all ok + response.setStatus(HttpServletResponse.SC_OK); + String contentType = getServletContext().getMimeType(file.getName()); + response.setContentType(contentType != null ? contentType : "application/octet-stream"); + if (file.length() <= Integer.MAX_VALUE) { + response.setContentLength((int) file.length()); + } + else { + response.setHeader("Content-Length", String.valueOf(file.length())); + } + response.setDateHeader("Last-Modified", file.lastModified()); + + InputStream in = new FileInputStream(file); + + try { + FileUtil.copy(in, response.getOutputStream()); + } + finally { + in.close(); + } + } + else { + if (file.exists()) { + // Attempted directory listing or non-readable file + response.sendError(HttpServletResponse.SC_FORBIDDEN, request.getRequestURI()); + } + else { + // No such file + response.sendError(HttpServletResponse.SC_NOT_FOUND, request.getRequestURI()); + } + } + } + + private File findFileForRequest(final HttpServletRequest request) { + String relativePath = request.getPathInfo(); + + return relativePath != null ? new File(root, relativePath) : root; + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java index 52f2bb88..71778898 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java @@ -46,19 +46,16 @@ import java.util.Map; /** * ThrottleFilter, a filter for easing server during heavy load. - * - * Intercepts requests, and returns HTTP response code 503 - * (Service Unavailable), if there are more than a given number of concurrent + *

    + * Intercepts requests, and returns HTTP response code {@code 503 (Service Unavailable)}, + * if there are more than a given number of concurrent * requests, to avoid large backlogs. The number of concurrent requests and the * response messages sent to the user agent, is configurable from the web * descriptor. * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java#1 $ + * @version $Id: ThrottleFilter.java#1 $ * @see #setMaxConcurrentThreadCount * @see #setResponseMessages */ @@ -67,13 +64,13 @@ public class ThrottleFilter extends GenericFilter { /** * Minimum free thread count, defaults to {@code 10} */ - protected int mMaxConcurrentThreadCount = 10; + protected int maxConcurrentThreadCount = 10; /** * The number of running request threads */ - private int mRunningThreads = 0; - private final Object mRunningThreadsLock = new Object(); + private int runningThreads = 0; + private final Object runningThreadsLock = new Object(); /** * Default response message sent to user agents, if the request is rejected @@ -89,17 +86,17 @@ public class ThrottleFilter extends GenericFilter { /** * The reposne message sent to user agenta, if the request is rejected */ - private Map mResponseMessageNames = new HashMap(10); + private Map responseMessageNames = new HashMap(10); /** * The reposne message sent to user agents, if the request is rejected */ - private String[] mResponseMessageTypes = null; + private String[] responseMessageTypes = null; /** * Cache for response messages */ - private Map mResponseCache = new HashMap(10); + private Map responseCache = new HashMap(10); /** @@ -110,7 +107,7 @@ public class ThrottleFilter extends GenericFilter { public void setMaxConcurrentThreadCount(String pMaxConcurrentThreadCount) { if (!StringUtil.isEmpty(pMaxConcurrentThreadCount)) { try { - mMaxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount); + maxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount); } catch (NumberFormatException nfe) { // Use default @@ -133,23 +130,24 @@ public class ThrottleFilter extends GenericFilter { public void setResponseMessages(String pResponseMessages) { // Split string in type=filename pairs String[] mappings = StringUtil.toStringArray(pResponseMessages, ", \r\n\t"); - List types = new ArrayList(); + List types = new ArrayList(); - for (int i = 0; i < mappings.length; i++) { + for (String pair : mappings) { // Split pairs on '=' - String[] mapping = StringUtil.toStringArray(mappings[i], "= "); + String[] mapping = StringUtil.toStringArray(pair, "= "); // Test for wrong mapping if ((mapping == null) || (mapping.length < 2)) { log("Error in init param \"responseMessages\": " + pResponseMessages); continue; } + types.add(mapping[0]); - mResponseMessageNames.put(mapping[0], mapping[1]); + responseMessageNames.put(mapping[0], mapping[1]); } // Create arrays - mResponseMessageTypes = (String[]) types.toArray(new String[types.size()]); + responseMessageTypes = types.toArray(new String[types.size()]); } /** @@ -159,8 +157,7 @@ public class ThrottleFilter extends GenericFilter { * @throws IOException * @throws ServletException */ - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) - throws IOException, ServletException { + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { try { if (beginRequest()) { // Continue request @@ -195,21 +192,22 @@ public class ThrottleFilter extends GenericFilter { /** * Marks the beginning of a request * - * @return true if the request should be handled. + * @return {@code true} if the request should be handled. */ private boolean beginRequest() { - synchronized (mRunningThreadsLock) { - mRunningThreads++; + synchronized (runningThreadsLock) { + runningThreads++; } - return (mRunningThreads <= mMaxConcurrentThreadCount); + + return (runningThreads <= maxConcurrentThreadCount); } /** * Marks the end of the request */ private void doneRequest() { - synchronized (mRunningThreadsLock) { - mRunningThreads--; + synchronized (runningThreadsLock) { + runningThreads--; } } @@ -220,12 +218,10 @@ public class ThrottleFilter extends GenericFilter { * @return the content type */ private String getContentType(HttpServletRequest pRequest) { - if (mResponseMessageTypes != null) { + if (responseMessageTypes != null) { String accept = pRequest.getHeader("Accept"); - for (int i = 0; i < mResponseMessageTypes.length; i++) { - String type = mResponseMessageTypes[i]; - + for (String type : responseMessageTypes) { // Note: This is not 100% correct way of doing content negotiation // But we just want a compatible result, quick, so this is okay if (StringUtil.contains(accept, type)) { @@ -245,17 +241,16 @@ public class ThrottleFilter extends GenericFilter { * @return the message */ private String getMessage(String pContentType) { - - String fileName = (String) mResponseMessageNames.get(pContentType); + String fileName = responseMessageNames.get(pContentType); // Get cached value - CacheEntry entry = (CacheEntry) mResponseCache.get(fileName); + CacheEntry entry = responseCache.get(fileName); if ((entry == null) || entry.isExpired()) { // Create and add or replace cached value entry = new CacheEntry(readMessage(fileName)); - mResponseCache.put(fileName, entry); + responseCache.put(fileName, entry); } // Return value @@ -292,20 +287,20 @@ public class ThrottleFilter extends GenericFilter { * Keeps track of Cached objects */ private static class CacheEntry { - private Object mValue; - private long mTimestamp = -1; + private Object value; + private long timestamp = -1; CacheEntry(Object pValue) { - mValue = pValue; - mTimestamp = System.currentTimeMillis(); + value = pValue; + timestamp = System.currentTimeMillis(); } Object getValue() { - return mValue; + return value; } boolean isExpired() { - return (System.currentTimeMillis() - mTimestamp) > 60000; // Cache 1 minute + return (System.currentTimeMillis() - timestamp) > 60000; // Cache 1 minute } } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java index 586b1f82..38664018 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java @@ -40,11 +40,11 @@ import java.io.IOException; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java#1 $ + * @version $Id: TimingFilter.java#1 $ */ public class TimingFilter extends GenericFilter { - private String mAttribUsage = null; + private String attribUsage = null; /** * Method init @@ -52,7 +52,7 @@ public class TimingFilter extends GenericFilter { * @throws ServletException */ public void init() throws ServletException { - mAttribUsage = getFilterName() + ".timerDelta"; + attribUsage = getFilterName() + ".timerDelta"; } /** @@ -66,13 +66,13 @@ public class TimingFilter extends GenericFilter { protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { // Get total usage of earlier filters on same level - Object usageAttrib = pRequest.getAttribute(mAttribUsage); + Object usageAttrib = pRequest.getAttribute(attribUsage); long total = 0; if (usageAttrib instanceof Long) { // If set, get value, and remove attribute for nested resources - total = ((Long) usageAttrib).longValue(); - pRequest.removeAttribute(mAttribUsage); + total = (Long) usageAttrib; + pRequest.removeAttribute(attribUsage); } // Start timing @@ -87,10 +87,10 @@ public class TimingFilter extends GenericFilter { long end = System.currentTimeMillis(); // Get time usage of included resources, add to total usage - usageAttrib = pRequest.getAttribute(mAttribUsage); + usageAttrib = pRequest.getAttribute(attribUsage); long usage = 0; if (usageAttrib instanceof Long) { - usage = ((Long) usageAttrib).longValue(); + usage = (Long) usageAttrib; } // Get the name of the included resource @@ -102,12 +102,11 @@ public class TimingFilter extends GenericFilter { } long delta = end - start; - log("Request processing time for resource \"" + resourceURI + "\": " + - (delta - usage) + " ms (accumulated: " + delta + " ms)."); + log(String.format("Request processing time for resource \"%s\": %d ms (accumulated: %d ms).", resourceURI, (delta - usage), delta)); // Store total usage total += delta; - pRequest.setAttribute(mAttribUsage, new Long(total)); + pRequest.setAttribute(attribUsage, total); } } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java index 935cd31b..5dbb30fb 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java @@ -109,32 +109,32 @@ import java.io.FilterOutputStream; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java#2 $ + * @version $Id: TrimWhiteSpaceFilter.java#2 $ */ public class TrimWhiteSpaceFilter extends GenericFilter { - private boolean mAutoFlush = true; + private boolean autoFlush = true; @InitParam public void setAutoFlush(final boolean pAutoFlush) { - mAutoFlush = pAutoFlush; + autoFlush = pAutoFlush; } public void init() throws ServletException { super.init(); - log("Automatic flushing is " + (mAutoFlush ? "enabled" : "disabled")); + log("Automatic flushing is " + (autoFlush ? "enabled" : "disabled")); } protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { ServletResponseWrapper wrapped = new TrimWSServletResponseWrapper(pResponse); pChain.doFilter(pRequest, ServletUtil.createWrapper(wrapped)); - if (mAutoFlush) { + if (autoFlush) { wrapped.flushBuffer(); } } static final class TrimWSFilterOutputStream extends FilterOutputStream { - boolean mLastWasWS = true; // Avoids leading WS by init to true + boolean lastWasWS = true; // Avoids leading WS by init to true public TrimWSFilterOutputStream(OutputStream pOut) { super(pOut); @@ -175,12 +175,12 @@ public class TrimWhiteSpaceFilter extends GenericFilter { if (!Character.isWhitespace((char) pByte)) { // If char is not WS, just store super.write(pByte); - mLastWasWS = false; + lastWasWS = false; } else { // TODO: Consider writing only 0x0a (LF) and 0x20 (space) // Else, if char is WS, store first, skip the rest - if (!mLastWasWS) { + if (!lastWasWS) { if (pByte == 0x0d) { // Convert all CR/LF's to 0x0a super.write(0x0a); } @@ -188,7 +188,7 @@ public class TrimWhiteSpaceFilter extends GenericFilter { super.write(pByte); } } - mLastWasWS = true; + lastWasWS = true; } } } @@ -199,23 +199,23 @@ public class TrimWhiteSpaceFilter extends GenericFilter { } protected OutputStream createOutputStream() throws IOException { - return new TrimWSFilterOutputStream(mResponse.getOutputStream()); + return new TrimWSFilterOutputStream(response.getOutputStream()); } } static class TrimWSServletResponseWrapper extends ServletResponseWrapper { - private final ServletResponseStreamDelegate mStreamDelegate = new TrimWSStreamDelegate(getResponse()); + private final ServletResponseStreamDelegate streamDelegate = new TrimWSStreamDelegate(getResponse()); public TrimWSServletResponseWrapper(ServletResponse pResponse) { super(pResponse); } public ServletOutputStream getOutputStream() throws IOException { - return mStreamDelegate.getOutputStream(); + return streamDelegate.getOutputStream(); } public PrintWriter getWriter() throws IOException { - return mStreamDelegate.getWriter(); + return streamDelegate.getWriter(); } public void setContentLength(int pLength) { @@ -224,12 +224,12 @@ public class TrimWhiteSpaceFilter extends GenericFilter { @Override public void flushBuffer() throws IOException { - mStreamDelegate.flushBuffer(); + streamDelegate.flushBuffer(); } @Override public void resetBuffer() { - mStreamDelegate.resetBuffer(); + streamDelegate.resetBuffer(); } // TODO: Consider picking up content-type/encoding, as we can only diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java index a4ad2d15..523c2f2d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java @@ -1,6 +1,7 @@ package com.twelvemonkeys.servlet.cache; -import java.io.File; +import com.twelvemonkeys.lang.Validate; + import java.net.URI; /** @@ -8,30 +9,23 @@ import java.net.URI; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java#1 $ + * @version $Id: AbstractCacheRequest.java#1 $ */ public abstract class AbstractCacheRequest implements CacheRequest { - private final URI mRequestURI; - private final String mMethod; + private final URI requestURI; + private final String method; protected AbstractCacheRequest(final URI pRequestURI, final String pMethod) { - if (pRequestURI == null) { - throw new IllegalArgumentException("request URI == null"); - } - if (pMethod == null) { - throw new IllegalArgumentException("method == null"); - } - - mRequestURI = pRequestURI; - mMethod = pMethod; + requestURI = Validate.notNull(pRequestURI, "requestURI"); + method = Validate.notNull(pMethod, "method"); } public URI getRequestURI() { - return mRequestURI; + return requestURI; } public String getMethod() { - return mMethod; + return method; } // TODO: Consider overriding equals/hashcode @@ -39,7 +33,7 @@ public abstract class AbstractCacheRequest implements CacheRequest { @Override public String toString() { return new StringBuilder(getClass().getSimpleName()) - .append("[URI=").append(mRequestURI) + .append("[URI=").append(requestURI) .append(", parameters=").append(getParameters()) .append(", headers=").append(getHeaders()) .append("]").toString(); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java index 3379b526..98d10356 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java @@ -7,19 +7,19 @@ import java.util.*; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java#1 $ + * @version $Id: AbstractCacheResponse.java#1 $ */ public abstract class AbstractCacheResponse implements CacheResponse { - private int mStatus; - private final Map> mHeaders = new LinkedHashMap>(); // Insertion order - private final Map> mReadableHeaders = Collections.unmodifiableMap(mHeaders); + private int status; + private final Map> headers = new LinkedHashMap>(); // Insertion order + private final Map> readableHeaders = Collections.unmodifiableMap(headers); public int getStatus() { - return mStatus; + return status; } public void setStatus(int pStatusCode) { - mStatus = pStatusCode; + status = pStatusCode; } public void addHeader(String pHeaderName, String pHeaderValue) { @@ -31,15 +31,17 @@ public abstract class AbstractCacheResponse implements CacheResponse { } private void setHeader(String pHeaderName, String pHeaderValue, boolean pAdd) { - List values = pAdd ? mHeaders.get(pHeaderName) : null; + List values = pAdd ? headers.get(pHeaderName) : null; + if (values == null) { values = new ArrayList(); - mHeaders.put(pHeaderName, values); + headers.put(pHeaderName, values); } + values.add(pHeaderValue); } public Map> getHeaders() { - return mReadableHeaders; + return readableHeaders; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java index fb9be851..bc7ca17c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java @@ -5,7 +5,7 @@ package com.twelvemonkeys.servlet.cache; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java#1 $ + * @version $Id: CacheException.java#1 $ */ public class CacheException extends Exception { public CacheException(Throwable pCause) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java index 812bfd8a..ec917a37 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java @@ -52,12 +52,12 @@ import java.util.logging.Logger; * @author Jayson Falkner * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java#4 $ + * @version $Id: CacheFilter.java#4 $ * */ public class CacheFilter extends GenericFilter { - HTTPCache mCache; + HTTPCache cache; /** * Initializes the filter @@ -67,7 +67,7 @@ public class CacheFilter extends GenericFilter { public void init() throws ServletException { FilterConfig config = getFilterConfig(); - // Default don't delete cache files on exit (peristent cache) + // Default don't delete cache files on exit (persistent cache) boolean deleteCacheOnExit = "TRUE".equalsIgnoreCase(config.getInitParameter("deleteCacheOnExit")); // Default expiry time 10 minutes @@ -76,6 +76,7 @@ public class CacheFilter extends GenericFilter { String expiryTimeStr = config.getInitParameter("expiryTime"); if (!StringUtil.isEmpty(expiryTimeStr)) { try { + // TODO: This is insane.. :-P Let the expiry time be in minutes or seconds.. expiryTime = Integer.parseInt(expiryTimeStr); } catch (NumberFormatException e) { @@ -99,7 +100,7 @@ public class CacheFilter extends GenericFilter { int maxCachedEntites = 10000; try { - mCache = new HTTPCache( + cache = new HTTPCache( getTempFolder(), expiryTime, memCacheSize * 1024 * 1024, @@ -120,7 +121,7 @@ public class CacheFilter extends GenericFilter { return null; } }; - log("Created cache: " + mCache); + log("Created cache: " + cache); } catch (IllegalArgumentException e) { throw new ServletConfigException("Could not create cache: " + e.toString(), e); @@ -136,8 +137,8 @@ public class CacheFilter extends GenericFilter { } public void destroy() { - log("Destroying cache: " + mCache); - mCache = null; + log("Destroying cache: " + cache); + cache = null; super.destroy(); } @@ -155,7 +156,7 @@ public class CacheFilter extends GenericFilter { // Render fast try { - mCache.doCached(cacheRequest, cacheResponse, resolver); + cache.doCached(cacheRequest, cacheResponse, resolver); } catch (CacheException e) { if (e.getCause() instanceof ServletException) { @@ -179,21 +180,21 @@ public class CacheFilter extends GenericFilter { // TODO: Extract, complete and document this class, might be useful in other cases // Maybe add it to the ServletUtil class static class ServletContextLoggerAdapter extends Logger { - private final ServletContext mContext; + private final ServletContext context; public ServletContextLoggerAdapter(String pName, ServletContext pContext) { super(pName, null); - mContext = pContext; + context = pContext; } @Override public void log(Level pLevel, String pMessage) { - mContext.log(pMessage); + context.log(pMessage); } @Override public void log(Level pLevel, String pMessage, Throwable pThrowable) { - mContext.log(pMessage, pThrowable); + context.log(pMessage, pThrowable); } } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java index 93ddde33..486dca11 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java @@ -9,7 +9,7 @@ import java.util.Map; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java#1 $ + * @version $Id: CacheRequest.java#1 $ */ public interface CacheRequest { URI getRequestURI(); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java index 94328669..2e94b4c8 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java @@ -10,7 +10,7 @@ import java.util.Map; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java#1 $ + * @version $Id: CacheResponse.java#1 $ */ public interface CacheResponse { OutputStream getOutputStream() throws IOException; diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java index 04eb74df..1db91f9f 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java @@ -49,45 +49,45 @@ import java.io.PrintWriter; * @author Jayson Falkner * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java#3 $ + * @version $Id: CacheResponseWrapper.java#3 $ */ class CacheResponseWrapper extends HttpServletResponseWrapper { - private ServletResponseStreamDelegate mStreamDelegate; + private ServletResponseStreamDelegate streamDelegate; - private CacheResponse mResponse; - private CachedEntity mCached; - private WritableCachedResponse mCachedResponse; + private CacheResponse response; + private CachedEntity cached; + private WritableCachedResponse cachedResponse; - private Boolean mCachable; - private int mStatus; + private Boolean cacheable; + private int status; public CacheResponseWrapper(final ServletCacheResponse pResponse, final CachedEntity pCached) { super(pResponse.getResponse()); - mResponse = pResponse; - mCached = pCached; + response = pResponse; + cached = pCached; init(); } /* - NOTE: This class defers determining if a response is cachable until the + NOTE: This class defers determining if a response is cacheable until the output stream is needed. This it the reason for the somewhat complicated logic in the add/setHeader methods below. */ private void init() { - mCachable = null; - mStatus = SC_OK; - mCachedResponse = mCached.createCachedResponse(); - mStreamDelegate = new ServletResponseStreamDelegate(this) { + cacheable = null; + status = SC_OK; + cachedResponse = cached.createCachedResponse(); + streamDelegate = new ServletResponseStreamDelegate(this) { protected OutputStream createOutputStream() throws IOException { - // Test if this request is really cachable, otherwise, + // Test if this request is really cacheable, otherwise, // just write through to underlying response, and don't cache - if (isCachable()) { - return mCachedResponse.getOutputStream(); + if (isCacheable()) { + return cachedResponse.getOutputStream(); } else { - mCachedResponse.setStatus(mStatus); - mCachedResponse.writeHeadersTo(CacheResponseWrapper.this.mResponse); + cachedResponse.setStatus(status); + cachedResponse.writeHeadersTo(CacheResponseWrapper.this.response); return super.getOutputStream(); } } @@ -95,25 +95,25 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { } CachedResponse getCachedResponse() { - return mCachedResponse.getCachedResponse(); + return cachedResponse.getCachedResponse(); } - public boolean isCachable() { + public boolean isCacheable() { // NOTE: Intentionally not synchronized - if (mCachable == null) { - mCachable = isCachableImpl(); + if (cacheable == null) { + cacheable = isCacheableImpl(); } - return mCachable; + return cacheable; } - private boolean isCachableImpl() { - if (mStatus != SC_OK) { + private boolean isCacheableImpl() { + if (status != SC_OK) { return false; } // Vary: * - String[] values = mCachedResponse.getHeaderValues(HTTPCache.HEADER_VARY); + String[] values = cachedResponse.getHeaderValues(HTTPCache.HEADER_VARY); if (values != null) { for (String value : values) { if ("*".equals(value)) { @@ -123,7 +123,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { } // Cache-Control: no-cache, no-store, must-revalidate - values = mCachedResponse.getHeaderValues(HTTPCache.HEADER_CACHE_CONTROL); + values = cachedResponse.getHeaderValues(HTTPCache.HEADER_CACHE_CONTROL); if (values != null) { for (String value : values) { if (StringUtil.contains(value, "no-cache") @@ -135,7 +135,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { } // Pragma: no-cache - values = mCachedResponse.getHeaderValues(HTTPCache.HEADER_PRAGMA); + values = cachedResponse.getHeaderValues(HTTPCache.HEADER_PRAGMA); if (values != null) { for (String value : values) { if (StringUtil.contains(value, "no-cache")) { @@ -148,43 +148,43 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { } public void flushBuffer() throws IOException { - mStreamDelegate.flushBuffer(); + streamDelegate.flushBuffer(); } public void resetBuffer() { // Servlet 2.3 - mStreamDelegate.resetBuffer(); + streamDelegate.resetBuffer(); } public void reset() { - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.reset(); } - // No else, might be cachable after all.. + // No else, might be cacheable after all.. init(); } public ServletOutputStream getOutputStream() throws IOException { - return mStreamDelegate.getOutputStream(); + return streamDelegate.getOutputStream(); } public PrintWriter getWriter() throws IOException { - return mStreamDelegate.getWriter(); + return streamDelegate.getWriter(); } public boolean containsHeader(String name) { - return mCachedResponse.getHeaderValues(name) != null; + return cachedResponse.getHeaderValues(name) != null; } public void sendError(int pStatusCode, String msg) throws IOException { - // NOT cachable - mStatus = pStatusCode; + // NOT cacheable + status = pStatusCode; super.sendError(pStatusCode, msg); } public void sendError(int pStatusCode) throws IOException { - // NOT cachable - mStatus = pStatusCode; + // NOT cacheable + status = pStatusCode; super.sendError(pStatusCode); } @@ -194,65 +194,65 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { } public void setStatus(int pStatusCode) { - // NOT cachable unless pStatusCode == 200 (or a FEW others?) + // NOT cacheable unless pStatusCode == 200 (or a FEW others?) if (pStatusCode != SC_OK) { - mStatus = pStatusCode; + status = pStatusCode; super.setStatus(pStatusCode); } } public void sendRedirect(String pLocation) throws IOException { - // NOT cachable - mStatus = SC_MOVED_TEMPORARILY; + // NOT cacheable + status = SC_MOVED_TEMPORARILY; super.sendRedirect(pLocation); } public void setDateHeader(String pName, long pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - mCachedResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - mCachedResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.setHeader(pName, pValue); } - mCachedResponse.setHeader(pName, pValue); + cachedResponse.setHeader(pName, pValue); } public void addHeader(String pName, String pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.addHeader(pName, pValue); } - mCachedResponse.addHeader(pName, pValue); + cachedResponse.addHeader(pName, pValue); } public void setIntHeader(String pName, int pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.setIntHeader(pName, pValue); } - mCachedResponse.setHeader(pName, String.valueOf(pValue)); + cachedResponse.setHeader(pName, String.valueOf(pValue)); } public void addIntHeader(String pName, int pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.addIntHeader(pName, pValue); } - mCachedResponse.addHeader(pName, String.valueOf(pValue)); + cachedResponse.addHeader(pName, String.valueOf(pValue)); } public final void setContentType(String type) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java index f39c4e14..d68708a5 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java @@ -34,7 +34,7 @@ import java.io.IOException; * CachedEntity * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java#3 $ + * @version $Id: CachedEntity.java#3 $ */ interface CachedEntity { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java index d7719814..1a502456 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.servlet.cache; +import com.twelvemonkeys.lang.Validate; + import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @@ -36,24 +38,20 @@ import java.util.List; * CachedEntity * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java#3 $ + * @version $Id: CachedEntityImpl.java#3 $ */ class CachedEntityImpl implements CachedEntity { - private String mCacheURI; - private HTTPCache mCache; + private String cacheURI; + private HTTPCache cache; CachedEntityImpl(String pCacheURI, HTTPCache pCache) { - if (pCacheURI == null) { - throw new IllegalArgumentException("cacheURI == null"); - } - - mCacheURI = pCacheURI; - mCache = pCache; + cacheURI = Validate.notNull(pCacheURI, "cacheURI"); + cache = pCache; } public void render(CacheRequest pRequest, CacheResponse pResponse) throws IOException { // Get cached content - CachedResponse cached = mCache.getContent(mCacheURI, pRequest); + CachedResponse cached = cache.getContent(cacheURI, pRequest); // Sanity check if (cached == null) { @@ -93,7 +91,7 @@ class CachedEntityImpl implements CachedEntity { } catch (IllegalArgumentException e) { // Seems to be a bug in FireFox 1.0.2..?! - mCache.log("Error in date header from user-agent. User-Agent: " + pRequest.getHeaders().get("User-Agent"), e); + cache.log("Error in date header from user-agent. User-Agent: " + pRequest.getHeaders().get("User-Agent"), e); } if (lastModified == -1L || (ifModifiedSince < (lastModified / 1000L) * 1000L)) { @@ -133,9 +131,9 @@ class CachedEntityImpl implements CachedEntity { // // CacheResponseWrapper response = (CacheResponseWrapper) pResponse; -// if (response.isCachable()) { - mCache.registerContent( - mCacheURI, +// if (response.isCacheable()) { + cache.registerContent( + cacheURI, pRequest, pResponse instanceof WritableCachedResponse ? ((WritableCachedResponse) pResponse).getCachedResponse() : pResponse ); @@ -149,7 +147,7 @@ class CachedEntityImpl implements CachedEntity { } public boolean isStale(CacheRequest pRequest) { - return mCache.isContentStale(mCacheURI, pRequest); + return cache.isContentStale(cacheURI, pRequest); } public WritableCachedResponse createCachedResponse() { @@ -157,16 +155,16 @@ class CachedEntityImpl implements CachedEntity { } public int hashCode() { - return (mCacheURI != null ? mCacheURI.hashCode() : 0) + 1397; + return (cacheURI != null ? cacheURI.hashCode() : 0) + 1397; } public boolean equals(Object pOther) { return pOther instanceof CachedEntityImpl && - ((mCacheURI == null && ((CachedEntityImpl) pOther).mCacheURI == null) || - mCacheURI != null && mCacheURI.equals(((CachedEntityImpl) pOther).mCacheURI)); + ((cacheURI == null && ((CachedEntityImpl) pOther).cacheURI == null) || + cacheURI != null && cacheURI.equals(((CachedEntityImpl) pOther).cacheURI)); } public String toString() { - return "CachedEntity[URI=" + mCacheURI + "]"; + return "CachedEntity[URI=" + cacheURI + "]"; } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java index 933314f5..bc475324 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java @@ -35,7 +35,7 @@ import java.io.OutputStream; * CachedResponse * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java#3 $ + * @version $Id: CachedResponse.java#3 $ */ interface CachedResponse { /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java index f3489173..8ee3c48c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java @@ -29,45 +29,39 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.io.FastByteArrayOutputStream; -import com.twelvemonkeys.util.LinkedMap; +import com.twelvemonkeys.lang.Validate; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * CachedResponseImpl * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java#4 $ + * @version $Id: CachedResponseImpl.java#4 $ */ class CachedResponseImpl implements CachedResponse { - final protected Map> mHeaders; - protected int mHeadersSize; - protected ByteArrayOutputStream mContent = null; - int mStatus; + final protected Map> headers; + protected int headersSize; + protected ByteArrayOutputStream content = null; + int status; protected CachedResponseImpl() { - mHeaders = new LinkedMap>(); // Keep headers in insertion order + headers = new LinkedHashMap>(); // Keep headers in insertion order } // For use by HTTPCache, when recreating CachedResponses from disk cache - CachedResponseImpl(final int pStatus, final LinkedMap> pHeaders, final int pHeaderSize, final byte[] pContent) { - if (pHeaders == null) { - throw new IllegalArgumentException("headers == null"); - } - mStatus = pStatus; - mHeaders = pHeaders; - mHeadersSize = pHeaderSize; - mContent = new FastByteArrayOutputStream(pContent); + CachedResponseImpl(final int pStatus, final LinkedHashMap> pHeaders, final int pHeaderSize, final byte[] pContent) { + status = pStatus; + headers = Validate.notNull(pHeaders, "headers"); + headersSize = pHeaderSize; + content = new FastByteArrayOutputStream(pContent); } public int getStatus() { - return mStatus; + return status; } /** @@ -84,12 +78,13 @@ class CachedResponseImpl implements CachedResponse { continue; } - // TODO: Replace Last-Modified with X-Cached-At? See CachedEntityImpl, line 50 + // TODO: Replace Last-Modified with X-Cached-At? See CachedEntityImpl String[] headerValues = getHeaderValues(header); for (int i = 0; i < headerValues.length; i++) { String headerValue = headerValues[i]; + if (i == 0) { pResponse.setHeader(header, headerValue); } @@ -101,17 +96,17 @@ class CachedResponseImpl implements CachedResponse { } /** - * Writes the cahced content to the response + * Writes the cached content to the response * * @param pStream the response stream * @throws java.io.IOException */ public void writeContentsTo(final OutputStream pStream) throws IOException { - if (mContent == null) { + if (content == null) { throw new IOException("Cache is null, no content to write."); } - mContent.writeTo(pStream); + content.writeTo(pStream); } /** @@ -120,7 +115,8 @@ class CachedResponseImpl implements CachedResponse { * @return an array of {@code String}s */ public String[] getHeaderNames() { - Set headers = mHeaders.keySet(); + Set headers = this.headers.keySet(); + return headers.toArray(new String[headers.size()]); } @@ -133,13 +129,9 @@ class CachedResponseImpl implements CachedResponse { * such header in this response. */ public String[] getHeaderValues(final String pHeaderName) { - List values = mHeaders.get(pHeaderName); - if (values == null) { - return null; - } - else { - return values.toArray(new String[values.size()]); - } + List values = headers.get(pHeaderName); + + return values == null ? null : values.toArray(new String[values.size()]); } /** @@ -153,13 +145,14 @@ class CachedResponseImpl implements CachedResponse { * such header in this response. */ public String getHeaderValue(final String pHeaderName) { - List values = mHeaders.get(pHeaderName); + List values = headers.get(pHeaderName); + return (values != null && values.size() > 0) ? values.get(0) : null; } public int size() { - // mContent.size() is exact size in bytes, mHeadersSize is an estimate - return (mContent != null ? mContent.size() : 0) + mHeadersSize; + // content.size() is exact size in bytes, headersSize is an estimate + return (content != null ? content.size() : 0) + headersSize; } public boolean equals(final Object pOther) { @@ -180,9 +173,9 @@ class CachedResponseImpl implements CachedResponse { } private boolean equalsImpl(final CachedResponseImpl pOther) { - return mHeadersSize == pOther.mHeadersSize && - (mContent == null ? pOther.mContent == null : mContent.equals(pOther.mContent)) && - mHeaders.equals(pOther.mHeaders); + return headersSize == pOther.headersSize && + (content == null ? pOther.content == null : content.equals(pOther.content)) && + headers.equals(pOther.headers); } private boolean equalsGeneric(final CachedResponse pOther) { @@ -212,9 +205,9 @@ class CachedResponseImpl implements CachedResponse { public int hashCode() { int result; - result = mHeaders.hashCode(); - result = 29 * result + mHeadersSize; - result = 37 * result + (mContent != null ? mContent.hashCode() : 0); + result = headers.hashCode(); + result = 29 * result + headersSize; + result = 37 * result + (content != null ? content.hashCode() : 0); return result; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java index 2c1287c0..80b02f00 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java @@ -10,16 +10,16 @@ import java.util.Map; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java#1 $ + * @version $Id: ClientCacheRequest.java#1 $ */ public final class ClientCacheRequest extends AbstractCacheRequest { - private Map> mParameters; - private Map> mHeaders; + private Map> parameters; + private Map> headers; public ClientCacheRequest(final URI pRequestURI,final Map> pParameters, final Map> pHeaders) { super(pRequestURI, "GET"); // TODO: Consider supporting more than get? At least HEAD and OPTIONS... - mParameters = normalizeMap(pParameters); - mHeaders = normalizeMap(pHeaders); + parameters = normalizeMap(pParameters); + headers = normalizeMap(pHeaders); } private Map normalizeMap(Map pMap) { @@ -27,11 +27,11 @@ public final class ClientCacheRequest extends AbstractCacheRequest { } public Map> getParameters() { - return mParameters; + return parameters; } public Map> getHeaders() { - return mHeaders; + return headers; } public String getServerName() { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java index 6b1ae816..795e3180 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java @@ -9,10 +9,10 @@ import java.io.OutputStream; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java#2 $ + * @version $Id: ClientCacheResponse.java#2 $ */ public final class ClientCacheResponse extends AbstractCacheResponse { - // It's quite useless to cahce the data either on disk or in memory, as it already is cached in the client's cache... + // It's quite useless to cache the data either on disk or in memory, as it already is cached in the client's cache... // It would be nice if we could bypass that... public OutputStream getOutputStream() throws IOException { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java index 999537dd..0591821b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -30,13 +30,14 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.net.MIMEUtil; import com.twelvemonkeys.net.NetUtil; import com.twelvemonkeys.util.LRUHashMap; -import com.twelvemonkeys.util.LinkedMap; import com.twelvemonkeys.util.NullMap; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; import java.io.*; import java.util.*; import java.util.logging.Level; @@ -47,7 +48,7 @@ import java.util.logging.Logger; *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java#4 $ + * @version $Id: HTTPCache.java#4 $ * @todo OMPTIMIZE: Cache parsed vary-info objects, not the properties-files * @todo BUG: Better filename handling, as some filenames become too long.. * - Use a mix of parameters and hashcode + lenght with fixed (max) lenght? @@ -148,35 +149,33 @@ public class HTTPCache { */ protected static final String FILE_EXT_VARY = ".vary"; - protected static final int STATUS_OK = 200; - /** * The directory used for the disk-based cache */ - private File mTempDir; + private File tempDir; /** * Indicates wether the disk-based cache should be deleted when the * container shuts down/VM exits */ - private boolean mDeleteCacheOnExit; + private boolean deleteCacheOnExit; /** * In-memory content cache */ - private final Map mContentCache; + private final Map contentCache; /** * In-memory enity cache */ - private final Map mEntityCache; + private final Map entityCache; /** * In-memory varyiation-info cache */ - private final Map mVaryCache; + private final Map varyCache; - private long mDefaultExpiryTime = -1; + private long defaultExpiryTime = -1; - private final Logger mLogger; + private final Logger logger; // Internal constructor for sublcasses only protected HTTPCache( @@ -187,44 +186,33 @@ public class HTTPCache { final boolean pDeleteCacheOnExit, final Logger pLogger ) { - if (pTempFolder == null) { - throw new IllegalArgumentException("temp folder == null"); - } - if (!pTempFolder.exists() && !pTempFolder.mkdirs()) { - throw new IllegalArgumentException("Could not create required temp directory: " + mTempDir.getAbsolutePath()); - } - if (!(pTempFolder.canRead() && pTempFolder.canWrite())) { - throw new IllegalArgumentException("Must have read/write access to temp folder: " + mTempDir.getAbsolutePath()); - } - if (pDefaultCacheExpiryTime < 0) { - throw new IllegalArgumentException("Negative expiry time"); - } - if (pMaxMemCacheSize < 0) { - throw new IllegalArgumentException("Negative maximum memory cache size"); - } - if (pMaxCachedEntites < 0) { - throw new IllegalArgumentException("Negative maximum number of cached entries"); - } + Validate.notNull(pTempFolder, "temp folder"); + Validate.isTrue(pTempFolder.exists() || pTempFolder.mkdirs(), pTempFolder.getAbsolutePath(), "Could not create required temp directory: %s"); + Validate.isTrue(pTempFolder.canRead() && pTempFolder.canWrite(), pTempFolder.getAbsolutePath(), "Must have read/write access to temp folder: %s"); - mDefaultExpiryTime = pDefaultCacheExpiryTime; + Validate.isTrue(pDefaultCacheExpiryTime >= 0, pDefaultCacheExpiryTime, "Negative expiry time: %d"); + Validate.isTrue(pMaxMemCacheSize >= 0, pDefaultCacheExpiryTime, "Negative maximum memory cache size: %d"); + Validate.isTrue(pMaxCachedEntites >= 0, pDefaultCacheExpiryTime, "Negative maximum number of cached entries: %d"); + + defaultExpiryTime = pDefaultCacheExpiryTime; if (pMaxMemCacheSize > 0) { // Map backing = new SizedLRUMap(pMaxMemCacheSize); // size in bytes -// mContentCache = new TimeoutMap(backing, null, pDefaultCacheExpiryTime); - mContentCache = new SizedLRUMap(pMaxMemCacheSize); // size in bytes +// contentCache = new TimeoutMap(backing, null, pDefaultCacheExpiryTime); + contentCache = new SizedLRUMap(pMaxMemCacheSize); // size in bytes } else { - mContentCache = new NullMap(); + contentCache = new NullMap(); } - mEntityCache = new LRUHashMap(pMaxCachedEntites); - mVaryCache = new LRUHashMap(pMaxCachedEntites); + entityCache = new LRUHashMap(pMaxCachedEntites); + varyCache = new LRUHashMap(pMaxCachedEntites); - mDeleteCacheOnExit = pDeleteCacheOnExit; + deleteCacheOnExit = pDeleteCacheOnExit; - mTempDir = pTempFolder; + tempDir = pTempFolder; - mLogger = pLogger != null ? pLogger : Logger.getLogger(getClass().getName()); + logger = pLogger != null ? pLogger : Logger.getLogger(getClass().getName()); } /** @@ -289,19 +277,15 @@ public class HTTPCache { } private static File getTempFolder(String pName, ServletContext pContext) { - if (pName == null) { - throw new IllegalArgumentException("name == null"); - } - if (pName.trim().length() == 0) { - throw new IllegalArgumentException("Empty name"); - } - if (pContext == null) { - throw new IllegalArgumentException("servlet context == null"); - } + Validate.notNull(pName, "name"); + Validate.isTrue(!StringUtil.isEmpty(pName), pName, "empty name: '%s'"); + Validate.notNull(pContext, "context"); + File tempRoot = (File) pContext.getAttribute("javax.servlet.context.tempdir"); if (tempRoot == null) { throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\""); } + return new File(tempRoot, pName); } @@ -309,36 +293,36 @@ public class HTTPCache { StringBuilder buf = new StringBuilder(getClass().getSimpleName()); buf.append("["); buf.append("Temp dir: "); - buf.append(mTempDir.getAbsolutePath()); - if (mDeleteCacheOnExit) { + buf.append(tempDir.getAbsolutePath()); + if (deleteCacheOnExit) { buf.append(" (non-persistent)"); } else { buf.append(" (persistent)"); } buf.append(", EntityCache: {"); - buf.append(mEntityCache.size()); + buf.append(entityCache.size()); buf.append(" entries in a "); - buf.append(mEntityCache.getClass().getName()); + buf.append(entityCache.getClass().getName()); buf.append("}, VaryCache: {"); - buf.append(mVaryCache.size()); + buf.append(varyCache.size()); buf.append(" entries in a "); - buf.append(mVaryCache.getClass().getName()); + buf.append(varyCache.getClass().getName()); buf.append("}, ContentCache: {"); - buf.append(mContentCache.size()); + buf.append(contentCache.size()); buf.append(" entries in a "); - buf.append(mContentCache.getClass().getName()); + buf.append(contentCache.getClass().getName()); buf.append("}]"); return buf.toString(); } void log(final String pMessage) { - mLogger.log(Level.INFO, pMessage); + logger.log(Level.INFO, pMessage); } void log(final String pMessage, Throwable pException) { - mLogger.log(Level.WARNING, pMessage, pException); + logger.log(Level.WARNING, pMessage, pException); } /** @@ -363,16 +347,15 @@ public class HTTPCache { // Get/create cached entity CachedEntity cached; - synchronized (mEntityCache) { - cached = mEntityCache.get(cacheURI); + synchronized (entityCache) { + cached = entityCache.get(cacheURI); if (cached == null) { cached = new CachedEntityImpl(cacheURI, this); - mEntityCache.put(cacheURI, cached); + entityCache.put(cacheURI, cached); } } - - // else if (not cached || stale), resolve through wrapped (caching) response + // else if (not cached || stale), resolve through wrapped (caching) response // else render to response // TODO: This is a bottleneck for uncachable resources. Should not @@ -412,11 +395,11 @@ public class HTTPCache { // Get/create cached entity CachedEntity cached; - synchronized (mEntityCache) { - cached = mEntityCache.get(cacheURI); + synchronized (entityCache) { + cached = entityCache.get(cacheURI); if (cached != null) { // TODO; Remove all variants - mEntityCache.remove(cacheURI); + entityCache.remove(cacheURI); } } @@ -461,7 +444,7 @@ public class HTTPCache { } private boolean isCachable(final CacheResponse pResponse) { - if (pResponse.getStatus() != STATUS_OK) { + if (pResponse.getStatus() != HttpServletResponse.SC_OK) { return false; } @@ -531,7 +514,7 @@ public class HTTPCache { File file = null; // Get base dir - File base = new File(mTempDir, "./" + pCacheURI); + File base = new File(tempDir, "./" + pCacheURI); final String basePath = base.getAbsolutePath(); File directory = base.getParentFile(); @@ -603,7 +586,7 @@ public class HTTPCache { synchronized (pVariations) { try { File file = getVaryPropertiesFile(pCacheURI); - if (!file.exists() && mDeleteCacheOnExit) { + if (!file.exists() && deleteCacheOnExit) { file.deleteOnExit(); } @@ -624,11 +607,11 @@ public class HTTPCache { private Properties getVaryProperties(final String pCacheURI) { Properties variations; - synchronized (mVaryCache) { - variations = mVaryCache.get(pCacheURI); + synchronized (varyCache) { + variations = varyCache.get(pCacheURI); if (variations == null) { variations = loadVaryProperties(pCacheURI); - mVaryCache.put(pCacheURI, variations); + varyCache.put(pCacheURI, variations); } } @@ -657,7 +640,7 @@ public class HTTPCache { } private File getVaryPropertiesFile(final String pCacheURI) { - return new File(mTempDir, "./" + pCacheURI + FILE_EXT_VARY); + return new File(tempDir, "./" + pCacheURI + FILE_EXT_VARY); } private static String generateCacheURI(final CacheRequest pRequest) { @@ -769,18 +752,18 @@ public class HTTPCache { extension = "[NULL]"; } - synchronized (mContentCache) { - mContentCache.put(pCacheURI + '.' + extension, pCachedResponse); + synchronized (contentCache) { + contentCache.put(pCacheURI + '.' + extension, pCachedResponse); // This will be the default version - if (!mContentCache.containsKey(pCacheURI)) { - mContentCache.put(pCacheURI, pCachedResponse); + if (!contentCache.containsKey(pCacheURI)) { + contentCache.put(pCacheURI, pCachedResponse); } } // Write the cached content to disk - File content = new File(mTempDir, "./" + pCacheURI + '.' + extension); - if (mDeleteCacheOnExit && !content.exists()) { + File content = new File(tempDir, "./" + pCacheURI + '.' + extension); + if (deleteCacheOnExit && !content.exists()) { content.deleteOnExit(); } @@ -809,7 +792,7 @@ public class HTTPCache { // Write the cached headers to disk (in pseudo-properties-format) File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); - if (mDeleteCacheOnExit && !headers.exists()) { + if (deleteCacheOnExit && !headers.exists()) { headers.deleteOnExit(); } @@ -872,13 +855,13 @@ public class HTTPCache { String extension = getVaryExtension(pCacheURI, pRequest); CachedResponse response; - synchronized (mContentCache) { -// System.out.println(" ## HTTPCache ## Looking up content with ext: \"" + extension + "\" from memory cache (" + mContentCache /*.size()*/ + " entries)..."); + synchronized (contentCache) { +// System.out.println(" ## HTTPCache ## Looking up content with ext: \"" + extension + "\" from memory cache (" + contentCache /*.size()*/ + " entries)..."); if ("ANY".equals(extension)) { - response = mContentCache.get(pCacheURI); + response = contentCache.get(pCacheURI); } else { - response = mContentCache.get(pCacheURI + '.' + extension); + response = contentCache.get(pCacheURI + '.' + extension); } if (response == null) { @@ -913,7 +896,7 @@ public class HTTPCache { int headerSize = (int) headers.length(); BufferedReader reader = new BufferedReader(new FileReader(headers)); - LinkedMap> headerMap = new LinkedMap>(); + LinkedHashMap> headerMap = new LinkedHashMap>(); String line; while ((line = reader.readLine()) != null) { int colIdx = line.indexOf(':'); @@ -931,8 +914,8 @@ public class HTTPCache { headerMap.put(name, Arrays.asList(StringUtil.toStringArray(value, "\\"))); } - response = new CachedResponseImpl(STATUS_OK, headerMap, headerSize, contents); - mContentCache.put(pCacheURI + '.' + FileUtil.getExtension(content), response); + response = new CachedResponseImpl(HttpServletResponse.SC_OK, headerMap, headerSize, contents); + contentCache.put(pCacheURI + '.' + FileUtil.getExtension(content), response); } } catch (IOException e) { @@ -997,7 +980,7 @@ public class HTTPCache { // If Cache-Control: max-age is present, use it, otherwise default int maxAge = getIntHeader(response, HEADER_CACHE_CONTROL, "max-age"); if (maxAge == -1) { - expires = lastModified + mDefaultExpiryTime; + expires = lastModified + defaultExpiryTime; //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); } else { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java index a92a6801..6e513928 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java @@ -7,7 +7,7 @@ import java.io.IOException; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java#2 $ + * @version $Id: ResponseResolver.java#2 $ */ public interface ResponseResolver { void resolve(CacheRequest pRequest, CacheResponse pResponse) throws IOException, CacheException; diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java index 44ec91f5..0b616ab4 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java @@ -52,42 +52,42 @@ import java.util.Map; * @author Jayson Falkner * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java#2 $ + * @version $Id: SerlvetCacheResponseWrapper.java#2 $ */ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { - private ServletResponseStreamDelegate mStreamDelegate; + private ServletResponseStreamDelegate streamDelegate; - private CacheResponse mCacheResponse; + private CacheResponse cacheResponse; - private Boolean mCachable; - private int mStatus; + private Boolean cacheable; + private int status; public SerlvetCacheResponseWrapper(final HttpServletResponse pServletResponse, final CacheResponse pResponse) { super(pServletResponse); - mCacheResponse = pResponse; + cacheResponse = pResponse; init(); } /* - NOTE: This class defers determining if a response is cachable until the + NOTE: This class defers determining if a response is cacheable until the output stream is needed. This it the reason for the somewhat complicated logic in the add/setHeader methods below. */ private void init() { - mCachable = null; - mStatus = SC_OK; - mStreamDelegate = new ServletResponseStreamDelegate(this) { + cacheable = null; + status = SC_OK; + streamDelegate = new ServletResponseStreamDelegate(this) { protected OutputStream createOutputStream() throws IOException { - // Test if this request is really cachable, otherwise, + // Test if this request is really cacheable, otherwise, // just write through to underlying response, and don't cache - if (isCachable()) { - return mCacheResponse.getOutputStream(); + if (isCacheable()) { + return cacheResponse.getOutputStream(); } else { // TODO: We need to tell the cache about this, somehow... - writeHeaders(mCacheResponse, (HttpServletResponse) getResponse()); + writeHeaders(cacheResponse, (HttpServletResponse) getResponse()); return super.getOutputStream(); } } @@ -109,23 +109,23 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { } } - public boolean isCachable() { + public boolean isCacheable() { // NOTE: Intentionally not synchronized - if (mCachable == null) { - mCachable = isCachableImpl(); + if (cacheable == null) { + cacheable = isCacheableImpl(); } - return mCachable; + return cacheable; } - private boolean isCachableImpl() { + private boolean isCacheableImpl() { // TODO: This code is duped in the cache... - if (mStatus != SC_OK) { + if (status != SC_OK) { return false; } // Vary: * - List values = mCacheResponse.getHeaders().get(HTTPCache.HEADER_VARY); + List values = cacheResponse.getHeaders().get(HTTPCache.HEADER_VARY); if (values != null) { for (String value : values) { if ("*".equals(value)) { @@ -135,7 +135,7 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { } // Cache-Control: no-cache, no-store, must-revalidate - values = mCacheResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL); + values = cacheResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL); if (values != null) { for (String value : values) { if (StringUtil.contains(value, "no-cache") @@ -147,7 +147,7 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { } // Pragma: no-cache - values = mCacheResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); + values = cacheResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); if (values != null) { for (String value : values) { if (StringUtil.contains(value, "no-cache")) { @@ -160,43 +160,43 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { } public void flushBuffer() throws IOException { - mStreamDelegate.flushBuffer(); + streamDelegate.flushBuffer(); } public void resetBuffer() { // Servlet 2.3 - mStreamDelegate.resetBuffer(); + streamDelegate.resetBuffer(); } public void reset() { - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.reset(); } - // No else, might be cachable after all.. + // No else, might be cacheable after all.. init(); } public ServletOutputStream getOutputStream() throws IOException { - return mStreamDelegate.getOutputStream(); + return streamDelegate.getOutputStream(); } public PrintWriter getWriter() throws IOException { - return mStreamDelegate.getWriter(); + return streamDelegate.getWriter(); } public boolean containsHeader(String name) { - return mCacheResponse.getHeaders().get(name) != null; + return cacheResponse.getHeaders().get(name) != null; } public void sendError(int pStatusCode, String msg) throws IOException { - // NOT cachable - mStatus = pStatusCode; + // NOT cacheable + status = pStatusCode; super.sendError(pStatusCode, msg); } public void sendError(int pStatusCode) throws IOException { - // NOT cachable - mStatus = pStatusCode; + // NOT cacheable + status = pStatusCode; super.sendError(pStatusCode); } @@ -206,65 +206,65 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { } public void setStatus(int pStatusCode) { - // NOT cachable unless pStatusCode == 200 (or a FEW others?) + // NOT cacheable unless pStatusCode == 200 (or a FEW others?) if (pStatusCode != SC_OK) { - mStatus = pStatusCode; + status = pStatusCode; super.setStatus(pStatusCode); } } public void sendRedirect(String pLocation) throws IOException { - // NOT cachable - mStatus = SC_MOVED_TEMPORARILY; + // NOT cacheable + status = SC_MOVED_TEMPORARILY; super.sendRedirect(pLocation); } public void setDateHeader(String pName, long pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - mCacheResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cacheResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - mCacheResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cacheResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.setHeader(pName, pValue); } - mCacheResponse.setHeader(pName, pValue); + cacheResponse.setHeader(pName, pValue); } public void addHeader(String pName, String pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.addHeader(pName, pValue); } - mCacheResponse.addHeader(pName, pValue); + cacheResponse.addHeader(pName, pValue); } public void setIntHeader(String pName, int pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.setIntHeader(pName, pValue); } - mCacheResponse.setHeader(pName, String.valueOf(pValue)); + cacheResponse.setHeader(pName, String.valueOf(pValue)); } public void addIntHeader(String pName, int pValue) { // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(mCachable)) { + if (Boolean.FALSE.equals(cacheable)) { super.addIntHeader(pName, pValue); } - mCacheResponse.addHeader(pName, String.valueOf(pValue)); + cacheResponse.addHeader(pName, String.valueOf(pValue)); } public final void setContentType(String type) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java index 40ef8d0d..1f88a02f 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java @@ -12,45 +12,45 @@ import java.util.Map; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java#1 $ + * @version $Id: ServletCacheRequest.java#1 $ */ public final class ServletCacheRequest extends AbstractCacheRequest { - private final HttpServletRequest mRequest; + private final HttpServletRequest request; - private Map> mHeaders; - private Map> mParameters; + private Map> headers; + private Map> parameters; protected ServletCacheRequest(final HttpServletRequest pRequest) { super(URI.create(pRequest.getRequestURI()), pRequest.getMethod()); - mRequest = pRequest; + request = pRequest; } public Map> getHeaders() { - if (mHeaders == null) { - mHeaders = ServletUtil.headersAsMap(mRequest); + if (headers == null) { + headers = ServletUtil.headersAsMap(request); } - return mHeaders; + return headers; } public Map> getParameters() { - if (mParameters == null) { - mParameters = ServletUtil.parametersAsMap(mRequest); + if (parameters == null) { + parameters = ServletUtil.parametersAsMap(request); } - return mParameters; + return parameters; } public String getServerName() { - return mRequest.getServerName(); + return request.getServerName(); } public int getServerPort() { - return mRequest.getServerPort(); + return request.getServerPort(); } HttpServletRequest getRequest() { - return mRequest; + return request; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java index 7ffebc95..855f6655 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java @@ -9,38 +9,38 @@ import java.io.OutputStream; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java#2 $ + * @version $Id: ServletCacheResponse.java#2 $ */ public final class ServletCacheResponse extends AbstractCacheResponse { - private HttpServletResponse mResponse; + private HttpServletResponse response; public ServletCacheResponse(HttpServletResponse pResponse) { - mResponse = pResponse; + response = pResponse; } public OutputStream getOutputStream() throws IOException { - return mResponse.getOutputStream(); + return response.getOutputStream(); } @Override public void setStatus(int pStatusCode) { - mResponse.setStatus(pStatusCode); + response.setStatus(pStatusCode); super.setStatus(pStatusCode); } @Override public void addHeader(String pHeaderName, String pHeaderValue) { - mResponse.addHeader(pHeaderName, pHeaderValue); + response.addHeader(pHeaderName, pHeaderValue); super.addHeader(pHeaderName, pHeaderValue); } @Override public void setHeader(String pHeaderName, String pHeaderValue) { - mResponse.setHeader(pHeaderName, pHeaderValue); + response.setHeader(pHeaderName, pHeaderValue); super.setHeader(pHeaderName, pHeaderValue); } HttpServletResponse getResponse() { - return mResponse; + return response; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java index 5f7decab..314dd69a 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java @@ -10,25 +10,25 @@ import java.io.IOException; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java#2 $ + * @version $Id: ServletResponseResolver.java#2 $ */ final class ServletResponseResolver implements ResponseResolver { - final private ServletCacheRequest mRequest; - final private ServletCacheResponse mResponse; - final private FilterChain mChain; + final private ServletCacheRequest request; + final private ServletCacheResponse response; + final private FilterChain chain; ServletResponseResolver(final ServletCacheRequest pRequest, final ServletCacheResponse pResponse, final FilterChain pChain) { - mRequest = pRequest; - mResponse = pResponse; - mChain = pChain; + request = pRequest; + response = pResponse; + chain = pChain; } public void resolve(final CacheRequest pRequest, final CacheResponse pResponse) throws IOException, CacheException { - // Need only wrap if pResponse is not mResponse... - HttpServletResponse response = pResponse == mResponse ? mResponse.getResponse() : new SerlvetCacheResponseWrapper(mResponse.getResponse(), pResponse); + // Need only wrap if pResponse is not response... + HttpServletResponse response = pResponse == this.response ? this.response.getResponse() : new SerlvetCacheResponseWrapper(this.response.getResponse(), pResponse); try { - mChain.doFilter(mRequest.getRequest(), response); + chain.doFilter(request.getRequest(), response); } catch (ServletException e) { throw new CacheException(e); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java index eecee196..e9b124de 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java @@ -34,7 +34,7 @@ import java.io.OutputStream; * WritableCachedResponse * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java#2 $ + * @version $Id: WritableCachedResponse.java#2 $ */ public interface WritableCachedResponse extends CachedResponse, CacheResponse { /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java index e350568e..4565e593 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java @@ -42,22 +42,22 @@ import java.util.Map; * WritableCachedResponseImpl * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java#3 $ + * @version $Id: WritableCachedResponseImpl.java#3 $ */ class WritableCachedResponseImpl implements WritableCachedResponse { - private final CachedResponseImpl mCachedResponse; + private final CachedResponseImpl cachedResponse; /** * Creates a {@code WritableCachedResponseImpl}. */ protected WritableCachedResponseImpl() { - mCachedResponse = new CachedResponseImpl(); + cachedResponse = new CachedResponseImpl(); // Hmmm.. setHeader(HTTPCache.HEADER_CACHED_TIME, NetUtil.formatHTTPDate(System.currentTimeMillis())); } public CachedResponse getCachedResponse() { - return mCachedResponse; + return cachedResponse; } public void setHeader(String pName, String pValue) { @@ -69,7 +69,7 @@ class WritableCachedResponseImpl implements WritableCachedResponse { } public Map> getHeaders() { - return mCachedResponse.mHeaders; + return cachedResponse.headers; } /** @@ -81,28 +81,26 @@ class WritableCachedResponseImpl implements WritableCachedResponse { private void setHeader(String pName, String pValue, boolean pAdd) { // System.out.println(" ++ CachedResponse ++ " + (pAdd ? "addHeader(" : "setHeader(") + pName + ", " + pValue + ")"); // If adding, get list and append, otherwise replace list - List values = null; - if (pAdd) { - values = mCachedResponse.mHeaders.get(pName); - } + List values = pAdd ? cachedResponse.headers.get(pName) : null; if (values == null) { values = new ArrayList(); if (pAdd) { // Add length of pName - mCachedResponse.mHeadersSize += (pName != null ? pName.length() : 0); + cachedResponse.headersSize += (pName != null ? pName.length() : 0); } else { // Remove length of potential replaced old values + pName String[] oldValues = getHeaderValues(pName); + if (oldValues != null) { for (String oldValue : oldValues) { - mCachedResponse.mHeadersSize -= oldValue.length(); + cachedResponse.headersSize -= oldValue.length(); } } else { - mCachedResponse.mHeadersSize += (pName != null ? pName.length() : 0); + cachedResponse.headersSize += (pName != null ? pName.length() : 0); } } } @@ -112,31 +110,31 @@ class WritableCachedResponseImpl implements WritableCachedResponse { values.add(pValue); // Add length of pValue - mCachedResponse.mHeadersSize += pValue.length(); + cachedResponse.headersSize += pValue.length(); } // Always add to headers - mCachedResponse.mHeaders.put(pName, values); + cachedResponse.headers.put(pName, values); } public OutputStream getOutputStream() { // TODO: Hmm.. Smells like DCL..? - if (mCachedResponse.mContent == null) { + if (cachedResponse.content == null) { createOutputStream(); } - return mCachedResponse.mContent; + return cachedResponse.content; } public void setStatus(int pStatusCode) { - mCachedResponse.mStatus = pStatusCode; + cachedResponse.status = pStatusCode; } public int getStatus() { - return mCachedResponse.getStatus(); + return cachedResponse.getStatus(); } private synchronized void createOutputStream() { - ByteArrayOutputStream cache = mCachedResponse.mContent; + ByteArrayOutputStream cache = cachedResponse.content; if (cache == null) { String contentLengthStr = getHeaderValue("Content-Length"); if (contentLengthStr != null) { @@ -146,43 +144,43 @@ class WritableCachedResponseImpl implements WritableCachedResponse { else { cache = new FastByteArrayOutputStream(1024); } - mCachedResponse.mContent = cache; + cachedResponse.content = cache; } } public void writeHeadersTo(CacheResponse pResponse) { - mCachedResponse.writeHeadersTo(pResponse); + cachedResponse.writeHeadersTo(pResponse); } public void writeContentsTo(OutputStream pStream) throws IOException { - mCachedResponse.writeContentsTo(pStream); + cachedResponse.writeContentsTo(pStream); } public String[] getHeaderNames() { - return mCachedResponse.getHeaderNames(); + return cachedResponse.getHeaderNames(); } public String[] getHeaderValues(String pHeaderName) { - return mCachedResponse.getHeaderValues(pHeaderName); + return cachedResponse.getHeaderValues(pHeaderName); } public String getHeaderValue(String pHeaderName) { - return mCachedResponse.getHeaderValue(pHeaderName); + return cachedResponse.getHeaderValue(pHeaderName); } public int size() { - return mCachedResponse.size(); + return cachedResponse.size(); } public boolean equals(Object pOther) { if (pOther instanceof WritableCachedResponse) { // Take advantage of faster implementation - return mCachedResponse.equals(((WritableCachedResponse) pOther).getCachedResponse()); + return cachedResponse.equals(((WritableCachedResponse) pOther).getCachedResponse()); } - return mCachedResponse.equals(pOther); + return cachedResponse.equals(pOther); } public int hashCode() { - return mCachedResponse.hashCode(); + return cachedResponse.hashCode(); } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java index 09a9d35f..4195692d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java @@ -33,7 +33,7 @@ package com.twelvemonkeys.servlet.fileupload; *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java#1 $ + * @version $Id: FileSizeExceededException.java#1 $ */ public class FileSizeExceededException extends FileUploadException { public FileSizeExceededException(Throwable pCause) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java index a02bd7b0..9048b854 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java @@ -35,7 +35,7 @@ import javax.servlet.ServletException; *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java#1 $ + * @version $Id: FileUploadException.java#1 $ */ public class FileUploadException extends ServletException { public FileUploadException(String pMessage) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java index 05118a8d..96489fa1 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java @@ -51,11 +51,11 @@ import java.net.MalformedURLException; * @see HttpFileUploadRequest * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java#1 $ + * @version $Id: FileUploadFilter.java#1 $ */ public class FileUploadFilter extends GenericFilter { - private File mUploadDir; - private long mMaxFileSize = 1024 * 1024; // 1 MByte + private File uploadDir; + private long maxFileSize = 1024 * 1024; // 1 MByte /** * This method is called by the server before the filter goes into service, @@ -66,17 +66,19 @@ public class FileUploadFilter extends GenericFilter { public void init() throws ServletException { // Get the name of the upload directory. String uploadDirParam = getInitParameter("uploadDir"); + if (!StringUtil.isEmpty(uploadDirParam)) { try { URL uploadDirURL = getServletContext().getResource(uploadDirParam); - mUploadDir = FileUtil.toFile(uploadDirURL); + uploadDir = FileUtil.toFile(uploadDirURL); } catch (MalformedURLException e) { throw new ServletException(e.getMessage(), e); } } - if (mUploadDir == null) { - mUploadDir = ServletUtil.getTempDir(getServletContext()); + + if (uploadDir == null) { + uploadDir = ServletUtil.getTempDir(getServletContext()); } } @@ -86,23 +88,9 @@ public class FileUploadFilter extends GenericFilter { * * @param pMaxSize */ -// public void setMaxFileSize(String pMaxSize) { -// try { -// setMaxFileSize(Long.parseLong(pMaxSize)); -// } -// catch (NumberFormatException e) { -// log("Error setting maxFileSize, using default: " + mMaxFileSize, e); -// } -// } - - /** - * Sets max filesize allowed for upload. - * - * @param pMaxSize - */ public void setMaxFileSize(long pMaxSize) { log("maxFileSize=" + pMaxSize); - mMaxFileSize = pMaxSize; + maxFileSize = pMaxSize; } /** @@ -125,7 +113,7 @@ public class FileUploadFilter extends GenericFilter { // If the content type is multipart, wrap if (isMultipartFileUpload(contentType)) { - pRequest = new HttpFileUploadRequestWrapper(request, mUploadDir, mMaxFileSize); + pRequest = new HttpFileUploadRequestWrapper(request, uploadDir, maxFileSize); } pChain.doFilter(pRequest, pResponse); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java index 6d40a323..64c27ef7 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java @@ -35,7 +35,7 @@ import javax.servlet.http.HttpServletRequest; * Form-based File Upload in HTML (RFC1867). * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java#1 $ + * @version $Id: HttpFileUploadRequest.java#1 $ */ public interface HttpFileUploadRequest extends HttpServletRequest { /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java index 8f4b3447..d3b5adb8 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java @@ -43,12 +43,12 @@ import java.util.*; * Jakarta Commons FileUpload. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java#1 $ + * @version $Id: HttpFileUploadRequestWrapper.java#1 $ */ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements HttpFileUploadRequest { - private final Map mParameters = new HashMap(); - private final Map mFiles = new HashMap(); + private final Map parameters = new HashMap(); + private final Map files = new HashMap(); public HttpFileUploadRequestWrapper(HttpServletRequest pRequest, File pUploadDir, long pMaxSize) throws ServletException { super(pRequest); @@ -86,7 +86,7 @@ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements String name = pItem.getFieldName(); UploadedFile[] values; - UploadedFile[] oldValues = mFiles.get(name); + UploadedFile[] oldValues = files.get(name); if (oldValues != null) { values = new UploadedFile[oldValues.length + 1]; @@ -97,7 +97,7 @@ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements values = new UploadedFile[] {value}; } - mFiles.put(name, values); + files.put(name, values); // Also add to normal fields processFormField(name, value.getName()); @@ -108,7 +108,7 @@ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements // probably faster to just use arrays... // TODO: Research and document... String[] values; - String[] oldValues = mParameters.get(pName); + String[] oldValues = parameters.get(pName); if (oldValues != null) { values = new String[oldValues.length + 1]; @@ -119,17 +119,17 @@ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements values = new String[] {pValue}; } - mParameters.put(pName, values); + parameters.put(pName, values); } public Map getParameterMap() { // TODO: The spec dicates immutable map, but what about the value arrays?! // Probably just leave as-is, for performance - return Collections.unmodifiableMap(mParameters); + return Collections.unmodifiableMap(parameters); } public Enumeration getParameterNames() { - return Collections.enumeration(mParameters.keySet()); + return Collections.enumeration(parameters.keySet()); } public String getParameter(String pString) { @@ -139,7 +139,7 @@ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements public String[] getParameterValues(String pString) { // TODO: Optimize? - return mParameters.get(pString).clone(); + return parameters.get(pString).clone(); } public UploadedFile getUploadedFile(String pName) { @@ -149,6 +149,6 @@ class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements public UploadedFile[] getUploadedFiles(String pName) { // TODO: Optimize? - return mFiles.get(pName).clone(); + return files.get(pName).clone(); } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java index 4a11f5c3..6eed9409 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java @@ -36,7 +36,7 @@ import java.io.IOException; * This class represents an uploaded file. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java#1 $ + * @version $Id: UploadedFile.java#1 $ */ public interface UploadedFile { /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java index bcb2eda5..a6f8343b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java @@ -40,38 +40,38 @@ import java.io.File; * Jakarta Commons FileUpload. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java#1 $ + * @version $Id: UploadedFileImpl.java#1 $ */ class UploadedFileImpl implements UploadedFile { - private final FileItem mItem; + private final FileItem item; public UploadedFileImpl(FileItem pItem) { if (pItem == null) { throw new IllegalArgumentException("fileitem == null"); } - mItem = pItem; + item = pItem; } public String getContentType() { - return mItem.getContentType(); + return item.getContentType(); } public InputStream getInputStream() throws IOException { - return mItem.getInputStream(); + return item.getInputStream(); } public String getName() { - return mItem.getName(); + return item.getName(); } public long length() { - return mItem.getSize(); + return item.getSize(); } public void writeTo(File pFile) throws IOException { try { - mItem.write(pFile); + item.write(pFile); } catch(RuntimeException e) { throw e; diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java index 949e8657..96d8cf2d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java @@ -104,12 +104,12 @@ import java.io.IOException; * @author Jayson Falkner * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java#1 $ + * @version $Id: GZIPFilter.java#1 $ */ public class GZIPFilter extends GenericFilter { { - mOncePerRequest = true; + oncePerRequest = true; } protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { @@ -120,22 +120,22 @@ public class GZIPFilter extends GenericFilter { // If GZIP is supported, use compression String accept = request.getHeader("Accept-Encoding"); - if (accept != null && accept.indexOf("gzip") != -1) { + if (accept != null && accept.contains("gzip")) { //System.out.println("GZIP supported, compressing."); - // TODO: Set Vary: Accept-Encoding ?! - GZIPResponseWrapper wrapped = new GZIPResponseWrapper(response); + try { pChain.doFilter(pRequest, wrapped); } finally { wrapped.flushResponse(); } + return; } } - // Else, contiue chain + // Else, continue chain pChain.doFilter(pRequest, pResponse); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java index 4555d6bd..4ee781da 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java @@ -48,99 +48,100 @@ import java.util.zip.GZIPOutputStream; * @author Jayson Falkner * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java#1 $ + * @version $Id: GZIPResponseWrapper.java#1 $ */ public class GZIPResponseWrapper extends HttpServletResponseWrapper { - protected ServletOutputStream mOut = null; - protected PrintWriter mWriter = null; - protected GZIPOutputStream mGZIPOut = null; - protected int mContentLength = -1; + // TODO: Remove/update ETags if needed? Read the spec (RFC 2616) on Vary/ETag for caching - public GZIPResponseWrapper(HttpServletResponse response) { + protected ServletOutputStream out; + protected PrintWriter writer; + protected GZIPOutputStream gzipOut; + protected int contentLength = -1; + + public GZIPResponseWrapper(final HttpServletResponse response) { super(response); + response.addHeader("Content-Encoding", "gzip"); + response.addHeader("Vary", "Accept"); } public ServletOutputStream createOutputStream() throws IOException { // FIX: Write directly to servlet output stream, for faster responses. // Relies on chunked streams, or buffering in the servlet engine. - if (mContentLength >= 0) { - mGZIPOut = new GZIPOutputStream(getResponse().getOutputStream(), mContentLength); + if (contentLength >= 0) { + gzipOut = new GZIPOutputStream(getResponse().getOutputStream(), contentLength); } else { - mGZIPOut = new GZIPOutputStream(getResponse().getOutputStream()); + gzipOut = new GZIPOutputStream(getResponse().getOutputStream()); } // Wrap in ServletOutputStream and return - return new OutputStreamAdapter(mGZIPOut); + return new OutputStreamAdapter(gzipOut); } // TODO: Move this to flushbuffer or something? Hmmm.. - public void flushResponse() { + public void flushResponse() throws IOException { try { - try { - // Finish GZIP encodig - if (mGZIPOut != null) { - mGZIPOut.finish(); - } + // Finish GZIP encodig + if (gzipOut != null) { + gzipOut.finish(); + } - flushBuffer(); - } - finally { - // Close stream - if (mWriter != null) { - mWriter.close(); - } - else { - if (mOut != null) { - mOut.close(); - } - } - } + flushBuffer(); } - catch (IOException e) { - // TODO: Fix this one... - e.printStackTrace(); + finally { + // Close stream + if (writer != null) { + writer.close(); + } + else { + if (out != null) { + out.close(); + } + } } } public void flushBuffer() throws IOException { - if (mWriter != null) { - mWriter.flush(); + if (writer != null) { + writer.flush(); } - else if (mOut != null) { - mOut.flush(); + else if (out != null) { + out.flush(); } } public ServletOutputStream getOutputStream() throws IOException { - if (mWriter != null) { + if (writer != null) { throw new IllegalStateException("getWriter() has already been called!"); } - if (mOut == null) { - mOut = createOutputStream(); + if (out == null) { + out = createOutputStream(); } - return (mOut); + + return out; } public PrintWriter getWriter() throws IOException { - if (mWriter != null) { - return (mWriter); + if (writer != null) { + return (writer); } - if (mOut != null) { + if (out != null) { throw new IllegalStateException("getOutputStream() has already been called!"); } - mOut = createOutputStream(); - // TODO: This is wrong. Should use getCharacterEncoding() or "ISO-8859-1" if gCE returns null. - mWriter = new PrintWriter(new OutputStreamWriter(mOut, "UTF-8")); - return (mWriter); + out = createOutputStream(); + + // TODO: This is wrong. Should use getCharacterEncoding() or "ISO-8859-1" if getCE returns null. + writer = new PrintWriter(new OutputStreamWriter(out, "UTF-8")); + + return writer; } public void setContentLength(int pLength) { // NOTE: Do not call super, as we will shrink the size. - mContentLength = pLength; + contentLength = pLength; } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java index 045fedcf..7bee9158 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java @@ -38,18 +38,18 @@ import java.awt.image.RenderedImage; /** * AWTImageFilterAdapter * - * @author $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java#1 $ + * @author Harald Kuhr + * @version $Id: AWTImageFilterAdapter.java#1 $ * */ public class AWTImageFilterAdapter extends ImageFilter { - private java.awt.image.ImageFilter mFilter = null; + private java.awt.image.ImageFilter imageFilter = null; public void setImageFilter(String pFilterClass) { try { Class filterClass = Class.forName(pFilterClass); - mFilter = (java.awt.image.ImageFilter) filterClass.newInstance(); + imageFilter = (java.awt.image.ImageFilter) filterClass.newInstance(); } catch (ClassNotFoundException e) { log("Could not load filter class.", e); @@ -64,9 +64,9 @@ public class AWTImageFilterAdapter extends ImageFilter { protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { // Filter - Image img = ImageUtil.filter(pImage, mFilter); + Image img = ImageUtil.filter(pImage, imageFilter); // Create BufferedImage & return - return ImageUtil.toBuffered(img, BufferedImage.TYPE_INT_RGB); // TODO: This is for JPEG only... + return ImageUtil.toBuffered(img, BufferedImage.TYPE_INT_RGB); // TODO: This is ok for JPEG only... } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java index 999f17f5..d64540e6 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java @@ -36,18 +36,18 @@ import java.awt.image.RenderedImage; /** * BufferedImageOpAdapter * - * @author $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java#1 $ + * @author Harald Kuhr + * @version $Id: BufferedImageOpAdapter.java#1 $ * */ public class BufferedImageOpAdapter extends ImageFilter { - private BufferedImageOp mFilter = null; + private BufferedImageOp filter = null; public void setImageFilter(String pFilterClass) { try { Class filterClass = Class.forName(pFilterClass); - mFilter = (BufferedImageOp) filterClass.newInstance(); + filter = (BufferedImageOp) filterClass.newInstance(); } catch (ClassNotFoundException e) { log("Could not instantiate filter class.", e); @@ -62,6 +62,6 @@ public class BufferedImageOpAdapter extends ImageFilter { protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { // Filter & return - return mFilter.filter(pImage, null); + return filter.filter(pImage, null); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java index 539aca8d..22b40225 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java @@ -47,7 +47,7 @@ import java.util.zip.CRC32; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java#2 $ + * @version $Id: ColorServlet.java#2 $ */ public class ColorServlet extends GenericServlet { private final static String RGB_PARAME = "color"; @@ -87,7 +87,7 @@ public class ColorServlet extends GenericServlet { private final static int GREEN_IDX = RED_IDX + 1; private final static int BLUE_IDX = GREEN_IDX + 1; - private final CRC32 mCRC = new CRC32(); + private final CRC32 crc = new CRC32(); /** * Creates a ColorDroplet. @@ -108,7 +108,6 @@ public class ColorServlet extends GenericServlet { * @throws ServletException */ public void service(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException { - int red = 0; int green = 0; int blue = 0; @@ -172,10 +171,10 @@ public class ColorServlet extends GenericServlet { private void updateCRC(byte[] pBytes, int pOff, int pLen) { int value; - synchronized (mCRC) { - mCRC.reset(); - mCRC.update(pBytes, pOff, pLen); - value = (int) mCRC.getValue(); + synchronized (crc) { + crc.reset(); + crc.update(pBytes, pOff, pLen); + value = (int) crc.getValue(); } pBytes[pOff + pLen ] = (byte) ((value >> 24) & 0xff); diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java index fc9a67e8..6dec5d60 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java @@ -38,7 +38,7 @@ import java.io.IOException; *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java#1 $ + * @version $Id: ComposeFilter.java#1 $ */ public class ComposeFilter extends ImageFilter { protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java index 37fea59f..90920683 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java @@ -54,6 +54,9 @@ import java.util.*; * unneccessary conversion (as IE supports PNG, the latests FireFox supports * JPEG and GIF, etc. even though they both don't explicitly list these formats * in their Accept headers). + * + * @author Harald Kuhr + * @version $Id: ContentNegotiationFilter.java#1 $ */ public class ContentNegotiationFilter extends ImageFilter { @@ -72,12 +75,12 @@ public class ContentNegotiationFilter extends ImageFilter { private final static String[] sKnownFormats = new String[] { FORMAT_JPEG, FORMAT_PNG, FORMAT_GIF, FORMAT_WBMP }; - private float[] mKnownFormatQuality = new float[] { + private float[] knownFormatQuality = new float[] { 1f, 1f, 0.99f, 0.5f }; - private HashMap mFormatQuality; // HashMap, as I need to clone this for each request - private final Object mLock = new Object(); + private HashMap formatQuality; // HashMap, as I need to clone this for each request + private final Object lock = new Object(); /* private Pattern[] mKnownAgentPatterns; @@ -86,7 +89,7 @@ public class ContentNegotiationFilter extends ImageFilter { { // Hack: Make sure the filter don't trigger all the time // See: super.trigger(ServletRequest) - mTriggerParams = new String[] {}; + triggerParams = new String[] {}; } /* @@ -242,9 +245,9 @@ public class ContentNegotiationFilter extends ImageFilter { } private Map getFormatQualityMapping() { - synchronized(mLock) { - if (mFormatQuality == null) { - mFormatQuality = new HashMap(); + synchronized(lock) { + if (formatQuality == null) { + formatQuality = new HashMap(); // Use ImageIO to find formats we can actually write String[] formats = ImageIO.getWriterMIMETypes(); @@ -252,12 +255,12 @@ public class ContentNegotiationFilter extends ImageFilter { // All known formats qs are initially 1.0 // Others should be 0.1 or something like that... for (String format : formats) { - mFormatQuality.put(format, getKnownFormatQuality(format)); + formatQuality.put(format, getKnownFormatQuality(format)); } } } //noinspection unchecked - return (Map) mFormatQuality.clone(); + return (Map) formatQuality.clone(); } /** @@ -428,7 +431,7 @@ public class ContentNegotiationFilter extends ImageFilter { private float getKnownFormatQuality(String pFormat) { for (int i = 0; i < sKnownFormats.length; i++) { if (pFormat.equals(sKnownFormats[i])) { - return mKnownFormatQuality[i]; + return knownFormatQuality[i]; } } return 0.1f; diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java index 13edab1b..a1040a42 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java @@ -91,7 +91,7 @@ import java.awt.image.RenderedImage; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java#1 $ + * @version $Id: CropFilter.java#1 $ */ public class CropFilter extends ScaleFilter { /** {@code cropX}*/ diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java new file mode 100644 index 00000000..8a30e8ba --- /dev/null +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.servlet.image; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ServiceRegistry; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.util.Iterator; + +/** + * Takes care of registering and de-registering local ImageIO plugins (service providers) for the servlet context. + *

    + * Registers all available plugins on {@code contextInitialized} event, using {@code ImageIO.scanForPlugins()}, to make + * sure they are available to the current servlet context. + * De-registers all plugins which have the {@link Thread#getContextClassLoader() current thread's context class loader} + * as its class loader on {@code contextDestroyed} event, to avoid class/resource leak. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IIOProviderContextListener.java,v 1.0 14.02.12 21:53 haraldk Exp$ + * @see javax.imageio.ImageIO#scanForPlugins() + */ +public final class IIOProviderContextListener implements ServletContextListener { + + public void contextInitialized(final ServletContextEvent event) { + // Registers all locally available IIO plugins. + ImageIO.scanForPlugins(); + } + + public void contextDestroyed(final ServletContextEvent event) { + // De-register any locally registered IIO plugins. Relies on each web app having its own context class loader. + final IIORegistry registry = IIORegistry.getDefaultInstance(); + final LocalFilter localFilter = new LocalFilter(Thread.currentThread().getContextClassLoader()); // scanForPlugins uses context class loader + + Iterator> categories = registry.getCategories(); + + while (categories.hasNext()) { + Class category = categories.next(); + Iterator providers = registry.getServiceProviders(category, localFilter, false); + + while (providers.hasNext()) { + Object provider = providers.next(); + registry.deregisterServiceProvider(provider); + event.getServletContext().log(String.format("Unregistered locally installed provider class: %s", provider.getClass())); + } + } + } + + static class LocalFilter implements ServiceRegistry.Filter { + private final ClassLoader loader; + + public LocalFilter(ClassLoader loader) { + this.loader = loader; + } + + public boolean filter(Object provider) { + return provider.getClass().getClassLoader() == loader; + } + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java index 7b716c9e..b0a9e6c0 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java @@ -33,6 +33,7 @@ import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.servlet.GenericFilter; import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.IOException; @@ -44,21 +45,25 @@ import java.io.IOException; * @see #doFilter(java.awt.image.BufferedImage,javax.servlet.ServletRequest,ImageServletResponse) * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java#2 $ + * @version $Id: ImageFilter.java#2 $ * */ public abstract class ImageFilter extends GenericFilter { + // TODO: Take the design back to the drawing board (see ImageServletResponseImpl) + // - Allow multiple filters to set size attribute + // - Allow a later filter to reset, to get pass-through given certain criteria... + // - Or better yet, allow a filter to decide if it wants to decode, based on image metadata on the original image (ie: width/height) - protected String[] mTriggerParams = null; + protected String[] triggerParams = null; /** * The {@code doFilterImpl} method is called once, or each time a * request/response pair is passed through the chain, depending on the - * {@link #mOncePerRequest} member variable. + * {@link #oncePerRequest} member variable. * - * @see #mOncePerRequest - * @see com.twelvemonkeys.servlet.GenericFilter#doFilterImpl doFilter - * @see Filter#doFilter Filter.doFilter + * @see #oncePerRequest + * @see com.twelvemonkeys.servlet.GenericFilter#doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilter + * @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter * * @param pRequest the servlet request * @param pResponse the servlet response @@ -67,7 +72,7 @@ public abstract class ImageFilter extends GenericFilter { * @throws IOException * @throws ServletException */ - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) + protected void doFilterImpl(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pChain) throws IOException, ServletException { //System.out.println("Starting filtering..."); @@ -78,19 +83,12 @@ public abstract class ImageFilter extends GenericFilter { pChain.doFilter(pRequest, pResponse); } else { + // If already wrapped, the image will be encoded later in the chain + // Or, if this is first filter in chain, we must encode when done + boolean encode = !(pResponse instanceof ImageServletResponse); + // For images, we do post filtering only and need to wrap the response - ImageServletResponse imageResponse; - boolean encode; - if (pResponse instanceof ImageServletResponse) { - //System.out.println("Allready ImageServletResponse"); - imageResponse = (ImageServletResponse) pResponse; - encode = false; // Allready wrapped, will be encoded later in the chain - } - else { - //System.out.println("Wrapping in ImageServletResponse"); - imageResponse = new ImageServletResponseImpl(pRequest, pResponse, getServletContext()); - encode = true; // This is first filter in chain, must encode when done - } + ImageServletResponse imageResponse = createImageServletResponse(pRequest, pResponse); //System.out.println("Passing request on to next in chain..."); // Pass the request on @@ -113,36 +111,55 @@ public abstract class ImageFilter extends GenericFilter { //System.out.println("Done filtering."); //System.out.println("Making image available..."); - // Make image available to other filters (avoid unnecessary - // serializing/deserializing) + // Make image available to other filters (avoid unnecessary serializing/deserializing) imageResponse.setImage(image); //System.out.println("Done."); - - if (encode) { - //System.out.println("Encoding image..."); - // Encode image to original repsonse - if (image != null) { - // TODO: Be smarter than this... - // TODO: Make sure ETag is same, if image content is the same... - // Use ETag of original response (or derived from) - // Use last modified of original response? Or keep original resource's, don't set at all? - // TODO: Why weak ETag? - String etag = "W/\"" + Integer.toHexString(hashCode()) + "-" + Integer.toHexString(image.hashCode()) + "\""; - ((ImageServletResponseImpl) imageResponse).setHeader("ETag", etag); - ((ImageServletResponseImpl) imageResponse).setDateHeader("Last-Modified", (System.currentTimeMillis() / 1000) * 1000); - imageResponse.flush(); - } - //System.out.println("Done encoding."); + } + if (encode) { + //System.out.println("Encoding image..."); + // Encode image to original response + if (image != null) { + // TODO: Be smarter than this... + // TODO: Make sure ETag is same, if image content is the same... + // Use ETag of original response (or derived from) + // Use last modified of original response? Or keep original resource's, don't set at all? + // TODO: Why weak ETag? + String etag = "W/\"" + Integer.toHexString(hashCode()) + "-" + Integer.toHexString(image.hashCode()) + "\""; + // TODO: This breaks for wrapped instances, need to either unwrap or test for HttpSR... + ((HttpServletResponse) pResponse).setHeader("ETag", etag); + ((HttpServletResponse) pResponse).setDateHeader("Last-Modified", (System.currentTimeMillis() / 1000) * 1000); } + + imageResponse.flush(); + //System.out.println("Done encoding."); } } //System.out.println("Filtering done."); } + /** + * Creates the image servlet response for this response. + * + * @param pResponse the original response + * @param pRequest the original request + * @return the new response, or {@code pResponse} if the response is already wrapped + * + * @see com.twelvemonkeys.servlet.image.ImageServletResponseWrapper + */ + private ImageServletResponse createImageServletResponse(final ServletRequest pRequest, final ServletResponse pResponse) { + if (pResponse instanceof ImageServletResponseImpl) { + ImageServletResponseImpl response = (ImageServletResponseImpl) pResponse; +// response.setRequest(pRequest); + return response; + } + + return new ImageServletResponseImpl(pRequest, pResponse, getServletContext()); + } + /** * Tests if the filter should do image filtering/processing. *

    - * This default implementation uses {@link #mTriggerParams} to test if: + * This default implementation uses {@link #triggerParams} to test if: *

    *
    {@code mTriggerParams == null}
    *
    {@code return true}
    @@ -157,14 +174,14 @@ public abstract class ImageFilter extends GenericFilter { * @param pRequest the servlet request * @return {@code true} if the filter should do image filtering */ - protected boolean trigger(ServletRequest pRequest) { + protected boolean trigger(final ServletRequest pRequest) { // If triggerParams not set, assume always trigger - if (mTriggerParams == null) { + if (triggerParams == null) { return true; } // Trigger only for certain request parameters - for (String triggerParam : mTriggerParams) { + for (String triggerParam : triggerParams) { if (pRequest.getParameter(triggerParam) != null) { return true; } @@ -181,8 +198,9 @@ public abstract class ImageFilter extends GenericFilter { * * @param pTriggerParams a comma-separated string of parameter names. */ - public void setTriggerParams(String pTriggerParams) { - mTriggerParams = StringUtil.toStringArray(pTriggerParams); + // TODO: Make it an @InitParam, and make sure we may set String[]/Collection as parameter? + public void setTriggerParams(final String pTriggerParams) { + triggerParams = StringUtil.toStringArray(pTriggerParams); } /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java index 6587c8cb..bb4c1022 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java @@ -31,13 +31,13 @@ package com.twelvemonkeys.servlet.image; import javax.servlet.*; /** - * This excpetion is a subclass of ServletException, and acts just as a marker - * for excpetions thrown by the ImageServlet API. + * This exception is a subclass of ServletException, and acts just as a marker + * for exceptions thrown by the ImageServlet API. * * @author Harald Kuhr * @author last modified by $Author: haku $ * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java#2 $ + * @version $Id: ImageServletException.java#2 $ */ public class ImageServletException extends ServletException { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java index 557c07cd..fe8ffd84 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java @@ -41,7 +41,7 @@ import java.awt.image.BufferedImage; * {@link #getImage()} to have any effect. * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java#4 $ + * @version $Id: ImageServletResponse.java#4 $ */ public interface ImageServletResponse extends ServletResponse { /** 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 bc3b3f04..c4c013b5 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java @@ -60,29 +60,31 @@ import java.util.Iterator; * This {@link ImageServletResponse} implementation can be used with image * requests, to have the image immediately decoded to a {@code BufferedImage}. * The image may be optionally subsampled, scaled and/or cropped. - * The response also automtically handles writing the image back to the underlying response stream + * 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: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java#10 $ + * @version $Id: ImageServletResponseImpl.java#10 $ * */ -// TODO: Refactor out HTTP specifcs (if possible). +// 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) class ImageServletResponseImpl extends HttpServletResponseWrapper implements ImageServletResponse { - private final ServletRequest mOriginalRequest; - private final ServletContext mContext; - private final ServletResponseStreamDelegate mStreamDelegate; + private ServletRequest originalRequest; + private final ServletContext context; + private final ServletResponseStreamDelegate streamDelegate; - private FastByteArrayOutputStream mBufferedOut; + private FastByteArrayOutputStream bufferedOut; - private RenderedImage mImage; - private String mOutputContentType; + private RenderedImage image; + private String outputContentType; - private String mOriginalContentType; - private int mOriginalContentLength = -1; + private String originalContentType; + private int originalContentLength = -1; /** * Creates an {@code ImageServletResponseImpl}. @@ -93,21 +95,21 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima */ public ImageServletResponseImpl(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final ServletContext pContext) { super(pResponse); - mOriginalRequest = pRequest; - mStreamDelegate = new ServletResponseStreamDelegate(pResponse) { + originalRequest = pRequest; + streamDelegate = new ServletResponseStreamDelegate(pResponse) { @Override protected OutputStream createOutputStream() throws IOException { - if (mOriginalContentLength >= 0) { - mBufferedOut = new FastByteArrayOutputStream(mOriginalContentLength); + if (originalContentLength >= 0) { + bufferedOut = new FastByteArrayOutputStream(originalContentLength); } else { - mBufferedOut = new FastByteArrayOutputStream(0); + bufferedOut = new FastByteArrayOutputStream(0); } - return mBufferedOut; + return bufferedOut; } }; - mContext = pContext; + context = pContext; } /** @@ -132,11 +134,11 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima */ public void setContentType(final String pMimeType) { // Throw exception is already set - if (mOriginalContentType != null) { + if (originalContentType != null) { throw new IllegalStateException("ContentType already set."); } - mOriginalContentType = pMimeType; + originalContentType = pMimeType; } /** @@ -146,7 +148,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima * @throws IOException */ public ServletOutputStream getOutputStream() throws IOException { - return mStreamDelegate.getOutputStream(); + return streamDelegate.getOutputStream(); } /** @@ -156,7 +158,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima * @throws IOException */ public PrintWriter getWriter() throws IOException { - return mStreamDelegate.getWriter(); + return streamDelegate.getWriter(); } /** @@ -165,11 +167,25 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima * @param pLength the content length */ public void setContentLength(final int pLength) { - if (mOriginalContentLength != -1) { + if (originalContentLength != -1) { throw new IllegalStateException("ContentLength already set."); } - mOriginalContentLength = pLength; + originalContentLength = pLength; + } + + @Override + public void setHeader(String name, String value) { + // NOTE: Clients could also specify content type/content length using the setHeader method, special handling + if (name != null && name.equals("Content-Length")) { + setContentLength(Integer.valueOf(value)); // Value might be too large, but we don't support that anyway + } + else if (name != null && name.equals("Content-Type")) { + setContentType(value); + } + else { + super.setHeader(name, value); + } } /** @@ -183,60 +199,79 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima String outputType = getOutputContentType(); // Force transcoding, if no other filtering is done - if (!outputType.equals(mOriginalContentType)) { + if (outputType != null && !outputType.equals(originalContentType)) { getImage(); } - // For known formats that don't support transparency, convert to opaque - if (("image/jpeg".equals(outputType) || "image/jpg".equals(outputType) - || "image/bmp".equals(outputType) || "image/x-bmp".equals(outputType)) && - mImage.getColorModel().getTransparency() != Transparency.OPAQUE) { - mImage = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_INT_RGB); - } - - if (mImage != null) { + if (image != null) { Iterator writers = ImageIO.getImageWritersByMIMEType(outputType); if (writers.hasNext()) { super.setContentType(outputType); OutputStream out = super.getOutputStream(); - - ImageWriter writer = (ImageWriter) writers.next(); try { - ImageWriteParam param = writer.getDefaultWriteParam(); - - Float requestQuality = (Float) mOriginalRequest.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY); - - // The default JPEG quality is not good enough, so always apply compression - if ((requestQuality != null || "jpeg".equalsIgnoreCase(getFormatNameSafe(writer))) && param.canWriteCompressed()) { - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionQuality(requestQuality != null ? requestQuality : 0.8f); - } - - ImageOutputStream stream = ImageIO.createImageOutputStream(out); - - writer.setOutput(stream); + ImageWriter writer = (ImageWriter) writers.next(); try { - writer.write(null, new IIOImage(mImage, null, null), param); + ImageWriteParam param = writer.getDefaultWriteParam(); + /////////////////// + // POST-PROCESS + // For known formats that don't support transparency, convert to opaque + if (isNonAlphaFormat(outputType) && image.getColorModel().getTransparency() != Transparency.OPAQUE) { + image = ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_RGB); + } + + Float requestQuality = (Float) originalRequest.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY); + + // The default JPEG quality is not good enough, so always adjust compression/quality + if ((requestQuality != null || "jpeg".equalsIgnoreCase(getFormatNameSafe(writer))) && param.canWriteCompressed()) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + // WORKAROUND: Known bug in GIFImageWriter in certain JDK versions, compression type is not set by default + if (param.getCompressionTypes() != null && param.getCompressionType() == null) { + param.setCompressionType(param.getCompressionTypes()[0]); // Just choose any, to keep param happy + } + + param.setCompressionQuality(requestQuality != null ? requestQuality : 0.8f); + } + + if ("gif".equalsIgnoreCase(getFormatNameSafe(writer)) && !(image.getColorModel() instanceof IndexColorModel) + && 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); + } + ////////////////// + ImageOutputStream stream = ImageIO.createImageOutputStream(out); + + writer.setOutput(stream); + try { + writer.write(null, new IIOImage(image, null, null), param); + } + finally { + stream.close(); + } } finally { - stream.close(); + writer.dispose(); } } finally { - writer.dispose(); out.flush(); } } else { - mContext.log("ERROR: No writer for content-type: " + outputType); + context.log("ERROR: No writer for content-type: " + outputType); throw new IIOException("Unable to transcode image: No suitable image writer found (content-type: " + outputType + ")."); } } else { - super.setContentType(mOriginalContentType); + super.setContentType(originalContentType); + ServletOutputStream out = super.getOutputStream(); + try { - mBufferedOut.writeTo(out); + if (bufferedOut != null) { + bufferedOut.writeTo(out); + } } finally { out.flush(); @@ -244,6 +279,11 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } } + private boolean isNonAlphaFormat(String outputType) { + return "image/jpeg".equals(outputType) || "image/jpg".equals(outputType) || + "image/bmp".equals(outputType) || "image/x-bmp".equals(outputType); + } + private String getFormatNameSafe(final ImageWriter pWriter) { try { return pWriter.getOriginatingProvider().getFormatNames()[0]; @@ -255,11 +295,11 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } public String getOutputContentType() { - return mOutputContentType != null ? mOutputContentType : mOriginalContentType; + return outputContentType != null ? outputContentType : originalContentType; } public void setOutputContentType(final String pImageFormat) { - mOutputContentType = pImageFormat; + outputContentType = pImageFormat; } /** @@ -269,7 +309,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima * response stream */ public void setImage(final RenderedImage pImage) { - mImage = pImage; + image = pImage; } /** @@ -281,14 +321,14 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima * @throws java.io.IOException if an I/O exception occurs during reading */ public BufferedImage getImage() throws IOException { - if (mImage == null) { + if (image == null) { // No content, no image - if (mBufferedOut == null) { + if (bufferedOut == null) { return null; } // Read from the byte buffer - InputStream byteStream = mBufferedOut.createInputStream(); + InputStream byteStream = bufferedOut.createInputStream(); ImageInputStream input = null; try { input = ImageIO.createImageInputStream(byteStream); @@ -304,26 +344,31 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima // Get default size int originalWidth = reader.getWidth(0); int originalHeight = reader.getHeight(0); - +////////////////// +// PRE-PROCESS (prepare): param, size, format?, request, response? + // TODO: AOI strategy? // Extract AOI from request - Rectangle aoi = extractAOIFromRequest(originalWidth, originalHeight); + Rectangle aoi = extractAOIFromRequest(originalWidth, originalHeight, originalRequest); + if (aoi != null) { param.setSourceRegion(aoi); originalWidth = aoi.width; originalHeight = aoi.height; } + // TODO: Size and subsampling strategy? // If possible, extract size from request - Dimension size = extractSizeFromRequest(originalWidth, originalHeight); - double readSubSamplingFactor = getReadSubsampleFactorFromRequest(); + Dimension size = extractSizeFromRequest(originalWidth, originalHeight, originalRequest); + double readSubSamplingFactor = getReadSubsampleFactorFromRequest(originalRequest); + if (size != null) { //System.out.println("Size: " + size); if (param.canSetSourceRenderSize()) { param.setSourceRenderSize(size); } else { - int subX = (int) Math.max(originalWidth / (double) (size.width * readSubSamplingFactor), 1.0); - int subY = (int) Math.max(originalHeight / (double) (size.height * readSubSamplingFactor), 1.0); + int subX = (int) Math.max(originalWidth / (size.width * readSubSamplingFactor), 1.0); + int subY = (int) Math.max(originalHeight / (size.height * readSubSamplingFactor), 1.0); if (subX > 1 || subY > 1) { param.setSourceSubsampling(subX, subY, subX > 1 ? subX / 2 : 0, subY > 1 ? subY / 2 : 0); @@ -334,43 +379,36 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima // Need base URI for SVG with links/stylesheets etc maybeSetBaseURIFromRequest(param); +///////////////////// + // Finally, read the image using the supplied parameter BufferedImage image = reader.read(0, param); - // If reader doesn't support dynamic sizing, scale now - if (image != null && size != null - && (image.getWidth() != size.width || image.getHeight() != size.height)) { + // TODO: If we sub-sampled, it would be a good idea to blur before resampling, + // to avoid jagged lines artifacts - int resampleAlgorithm = getResampleAlgorithmFromRequest(); - // NOTE: Only use createScaled if IndexColorModel, - // as it's more expensive due to color conversion - if (image.getColorModel() instanceof IndexColorModel) { - image = ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); - } - else { - image = ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); - } - } + // If reader doesn't support dynamic sizing, scale now + image = resampleImage(image, size); // Fill bgcolor behind image, if transparent - extractAndSetBackgroundColor(image); + extractAndSetBackgroundColor(image); // TODO: Move to flush/POST-PROCESS // Set image - mImage = image; + this.image = image; } finally { reader.dispose(); } } else { - mContext.log("ERROR: No suitable image reader found (content-type: " + mOriginalContentType + ")."); - mContext.log("ERROR: Available formats: " + getFormatsString()); + context.log("ERROR: No suitable image reader found (content-type: " + originalContentType + ")."); + context.log("ERROR: Available formats: " + getFormatsString()); - throw new IIOException("Unable to transcode image: No suitable image reader found (content-type: " + mOriginalContentType + ")."); + throw new IIOException("Unable to transcode image: No suitable image reader found (content-type: " + originalContentType + ")."); } // Free resources, as the image is now either read, or unreadable - mBufferedOut = null; + bufferedOut = null; } finally { if (input != null) { @@ -380,37 +418,49 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } // Image is usually a BufferedImage, but may also be a RenderedImage - return mImage != null ? ImageUtil.toBuffered(mImage) : null; + return image != null ? ImageUtil.toBuffered(image) : null; + } + + private BufferedImage resampleImage(final BufferedImage image, final Dimension size) { + if (image != null && size != null && (image.getWidth() != size.width || image.getHeight() != size.height)) { + int resampleAlgorithm = getResampleAlgorithmFromRequest(); + + // 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); + } + else { + return ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); + } + } + return image; } private int getResampleAlgorithmFromRequest() { - int resampleAlgoithm; - - Object algorithm = mOriginalRequest.getAttribute(ATTRIB_IMAGE_RESAMPLE_ALGORITHM); + 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)) { - resampleAlgoithm = (Integer) algorithm; + return (Integer) algorithm; } else { if (algorithm != null) { - mContext.log("WARN: Illegal image resampling algorithm: " + algorithm); + context.log("WARN: Illegal image resampling algorithm: " + algorithm); } - resampleAlgoithm = BufferedImage.SCALE_DEFAULT; + return BufferedImage.SCALE_DEFAULT; } - - return resampleAlgoithm; } - private double getReadSubsampleFactorFromRequest() { + private double getReadSubsampleFactorFromRequest(final ServletRequest pOriginalRequest) { double subsampleFactor; - Object factor = mOriginalRequest.getAttribute(ATTRIB_READ_SUBSAMPLING_FACTOR); + Object factor = pOriginalRequest.getAttribute(ATTRIB_READ_SUBSAMPLING_FACTOR); if (factor instanceof Number && ((Number) factor).doubleValue() >= 1.0) { subsampleFactor = ((Number) factor).doubleValue(); } else { if (factor != null) { - mContext.log("WARN: Illegal read subsampling factor: " + factor); + context.log("WARN: Illegal read subsampling factor: " + factor); } + subsampleFactor = 2.0; } @@ -420,7 +470,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima private void extractAndSetBackgroundColor(final BufferedImage pImage) { // TODO: bgColor request attribute instead of parameter? if (pImage.getColorModel().hasAlpha()) { - String bgColor = mOriginalRequest.getParameter("bg.color"); + String bgColor = originalRequest.getParameter("bg.color"); if (bgColor != null) { Color color = StringUtil.toColor(bgColor); @@ -451,7 +501,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } private void maybeSetBaseURIFromRequest(final ImageReadParam pParam) { - if (mOriginalRequest instanceof HttpServletRequest) { + if (originalRequest instanceof HttpServletRequest) { try { // If there's a setBaseURI method, we'll try to use that (uses reflection, to avoid dependency on plugins) Method setBaseURI; @@ -463,42 +513,43 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } // Get URL for resource and set as base - String baseURI = ServletUtil.getContextRelativeURI((HttpServletRequest) mOriginalRequest); + String baseURI = ServletUtil.getContextRelativeURI((HttpServletRequest) originalRequest); + + URL resourceURL = context.getResource(baseURI); - URL resourceURL = mContext.getResource(baseURI); if (resourceURL == null) { - resourceURL = ServletUtil.getRealURL(mContext, baseURI); + resourceURL = ServletUtil.getRealURL(context, baseURI); } if (resourceURL != null) { setBaseURI.invoke(pParam, resourceURL.toExternalForm()); } else { - mContext.log("WARN: Resource URL not found for URI: " + baseURI); + context.log("WARN: Resource URL not found for URI: " + baseURI); } } catch (Exception e) { - mContext.log("WARN: Could not set base URI: ", e); + context.log("WARN: Could not set base URI: ", e); } } } - private Dimension extractSizeFromRequest(final int pDefaultWidth, final int pDefaultHeight) { + private Dimension extractSizeFromRequest(final int pDefaultWidth, final int pDefaultHeight, final ServletRequest pOriginalRequest) { // TODO: Allow extraction from request parameters /* - int sizeW = ServletUtil.getIntParameter(mOriginalRequest, "size.w", -1); - int sizeH = ServletUtil.getIntParameter(mOriginalRequest, "size.h", -1); - boolean sizePercent = ServletUtil.getBooleanParameter(mOriginalRequest, "size.percent", false); - boolean sizeUniform = ServletUtil.getBooleanParameter(mOriginalRequest, "size.uniform", true); + int sizeW = ServletUtil.getIntParameter(originalRequest, "size.w", -1); + int sizeH = ServletUtil.getIntParameter(originalRequest, "size.h", -1); + boolean sizePercent = ServletUtil.getBooleanParameter(originalRequest, "size.percent", false); + boolean sizeUniform = ServletUtil.getBooleanParameter(originalRequest, "size.uniform", true); */ - Dimension size = (Dimension) mOriginalRequest.getAttribute(ATTRIB_SIZE); + Dimension size = (Dimension) pOriginalRequest.getAttribute(ATTRIB_SIZE); int sizeW = size != null ? size.width : -1; int sizeH = size != null ? size.height : -1; - Boolean b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_SIZE_PERCENT); + Boolean b = (Boolean) pOriginalRequest.getAttribute(ATTRIB_SIZE_PERCENT); boolean sizePercent = b != null && b; // default: false - b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_SIZE_UNIFORM); + b = (Boolean) pOriginalRequest.getAttribute(ATTRIB_SIZE_UNIFORM); boolean sizeUniform = b == null || b; // default: true if (sizeW >= 0 || sizeH >= 0) { @@ -508,26 +559,26 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima return size; } - private Rectangle extractAOIFromRequest(final int pDefaultWidth, final int pDefaultHeight) { + private Rectangle extractAOIFromRequest(final int pDefaultWidth, final int pDefaultHeight, final ServletRequest pOriginalRequest) { // TODO: Allow extraction from request parameters /* - int aoiX = ServletUtil.getIntParameter(mOriginalRequest, "aoi.x", -1); - int aoiY = ServletUtil.getIntParameter(mOriginalRequest, "aoi.y", -1); - int aoiW = ServletUtil.getIntParameter(mOriginalRequest, "aoi.w", -1); - int aoiH = ServletUtil.getIntParameter(mOriginalRequest, "aoi.h", -1); - boolean aoiPercent = ServletUtil.getBooleanParameter(mOriginalRequest, "aoi.percent", false); - boolean aoiUniform = ServletUtil.getBooleanParameter(mOriginalRequest, "aoi.uniform", false); + int aoiX = ServletUtil.getIntParameter(originalRequest, "aoi.x", -1); + int aoiY = ServletUtil.getIntParameter(originalRequest, "aoi.y", -1); + int aoiW = ServletUtil.getIntParameter(originalRequest, "aoi.w", -1); + int aoiH = ServletUtil.getIntParameter(originalRequest, "aoi.h", -1); + boolean aoiPercent = ServletUtil.getBooleanParameter(originalRequest, "aoi.percent", false); + boolean aoiUniform = ServletUtil.getBooleanParameter(originalRequest, "aoi.uniform", false); */ - Rectangle aoi = (Rectangle) mOriginalRequest.getAttribute(ATTRIB_AOI); + Rectangle aoi = (Rectangle) pOriginalRequest.getAttribute(ATTRIB_AOI); int aoiX = aoi != null ? aoi.x : -1; int aoiY = aoi != null ? aoi.y : -1; int aoiW = aoi != null ? aoi.width : -1; int aoiH = aoi != null ? aoi.height : -1; - Boolean b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_AOI_PERCENT); + Boolean b = (Boolean) pOriginalRequest.getAttribute(ATTRIB_AOI_PERCENT); boolean aoiPercent = b != null && b; // default: false - b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_AOI_UNIFORM); + b = (Boolean) pOriginalRequest.getAttribute(ATTRIB_AOI_UNIFORM); boolean aoiUniform = b != null && b; // default: false if (aoiX >= 0 || aoiY >= 0 || aoiW >= 0 || aoiH >= 0) { @@ -552,18 +603,18 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima * @param pHeight the new height of the image, or -1 if unknown * @param pPercent the constant specifying units for width and height * parameter (UNITS_PIXELS or UNITS_PERCENT) - * @param pUniformScale boolean specifying uniform scale or not + * @param pUniform boolean specifying uniform scale or not * @return a Dimension object, with the correct width and heigth * in pixels, for the scaled version of the image. */ - protected static Dimension getSize(int pOriginalWidth, int pOriginalHeight, + static Dimension getSize(int pOriginalWidth, int pOriginalHeight, int pWidth, int pHeight, - boolean pPercent, boolean pUniformScale) { + boolean pPercent, boolean pUniform) { - // If uniform, make sure width and height are scaled the same ammount + // If uniform, make sure width and height are scaled the same amount // (use ONLY height or ONLY width). // - // Algoritm: + // Algorithm: // if uniform // if newHeight not set // find ratio newWidth / oldWidth @@ -602,7 +653,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima // Else: No scale } else { - if (pUniformScale) { + if (pUniform) { if (pWidth >= 0 && pHeight >= 0) { // Compute both ratios ratio = (float) pWidth / (float) pOriginalWidth; @@ -616,7 +667,6 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima else { pHeight = Math.round((float) pOriginalHeight * ratio); } - } else if (pWidth >= 0) { // Find ratio from pWidth @@ -644,10 +694,10 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima return new Dimension(pWidth, pHeight); } - protected static Rectangle getAOI(int pOriginalWidth, int pOriginalHeight, + static Rectangle getAOI(int pOriginalWidth, int pOriginalHeight, int pX, int pY, int pWidth, int pHeight, - boolean pPercent, boolean pUniform) { - // Algoritm: + boolean pPercent, boolean pMaximizeToAspect) { + // Algorithm: // Try to get x and y (default 0,0). // Try to get width and height (default width-x, height-y) // @@ -669,7 +719,6 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima ratio = (float) pWidth / 100f; pWidth = Math.round((float) pOriginalWidth * ratio); pHeight = Math.round((float) pOriginalHeight * ratio); - } else if (pHeight >= 0) { // Find ratio from pHeight @@ -681,7 +730,7 @@ class ImageServletResponseImpl extends HttpServletResponseWrapper implements Ima } else { // Uniform - if (pUniform) { + if (pMaximizeToAspect) { if (pWidth >= 0 && pHeight >= 0) { // Compute both ratios ratio = (float) pWidth / (float) pHeight; diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java index 66e2a293..c2700cad 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java @@ -35,8 +35,8 @@ import java.awt.image.RenderedImage; /** * An {@code ImageFilter} that does nothing. Useful for debugging purposes. * - * @author $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java#2 $ + * @author Harald Kuhr + * @version $Id: NullImageFilter.java $ * */ public final class NullImageFilter extends ImageFilter { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java index 37af808e..98b692ce 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java @@ -29,7 +29,6 @@ package com.twelvemonkeys.servlet.image; import com.twelvemonkeys.image.ImageUtil; -import com.twelvemonkeys.lang.MathUtil; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.servlet.ServletUtil; @@ -78,7 +77,7 @@ import java.awt.image.RenderedImage; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java#1 $ + * @version $Id: RotateFilter.java#1 $ */ public class RotateFilter extends ImageFilter { @@ -159,7 +158,7 @@ public class RotateFilter extends ImageFilter { str = pReq.getParameter(PARAM_ANGLE_UNITS); if (!StringUtil.isEmpty(str) && ANGLE_DEGREES.equalsIgnoreCase(str)) { - angle = MathUtil.toRadians(angle); + angle = Math.toRadians(angle); } } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java index 8bd9ca07..825fa77c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java @@ -69,7 +69,7 @@ import java.lang.reflect.Field; * * @author Harald Kuhr * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java#1 $ + * @version $Id: ScaleFilter.java#1 $ * * @example <IMG src="/scale/test.jpg?scaleX=500&scaleUniform=false"> * @example <IMG src="/scale/test.png?scaleY=50&scaleUnits=PERCENT"> @@ -123,7 +123,7 @@ public class ScaleFilter extends ImageFilter { protected final static String PARAM_IMAGE = "image"; /** */ - protected int mDefaultScaleQuality = Image.SCALE_DEFAULT; + protected int defaultScaleQuality = Image.SCALE_DEFAULT; /** * Reads the image from the requested URL, scales it, and returns it in the @@ -188,11 +188,11 @@ public class ScaleFilter extends ImageFilter { } } - return mDefaultScaleQuality; + return defaultScaleQuality; } public void setDefaultScaleQuality(String pDefaultScaleQuality) { - mDefaultScaleQuality = getQuality(pDefaultScaleQuality); + defaultScaleQuality = getQuality(pDefaultScaleQuality); } /** diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java index 1fb6d497..d965decc 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java @@ -21,69 +21,69 @@ import java.io.IOException; * @see ImageServletResponse#ATTRIB_AOI * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java#1 $ + * @version $Id: SourceRenderFilter.java#1 $ */ public class SourceRenderFilter extends ImageFilter { - private String mSizeWidthParam = "size.w"; - private String mSizeHeightParam = "size.h"; - private String mSizePercentParam = "size.percent"; - private String mSizeUniformParam = "size.uniform"; + private String sizeWidthParam = "size.w"; + private String sizeHeightParam = "size.h"; + private String sizePercentParam = "size.percent"; + private String sizeUniformParam = "size.uniform"; - private String mRegionWidthParam = "aoi.w"; - private String mRegionHeightParam = "aoi.h"; - private String mRegionLeftParam = "aoi.x"; - private String mRegionTopParam = "aoi.y"; - private String mRegionPercentParam = "aoi.percent"; - private String mRegionUniformParam = "aoi.uniform"; + private String regionWidthParam = "aoi.w"; + private String regionHeightParam = "aoi.h"; + private String regionLeftParam = "aoi.x"; + private String regionTopParam = "aoi.y"; + private String regionPercentParam = "aoi.percent"; + private String regionUniformParam = "aoi.uniform"; public void setRegionHeightParam(String pRegionHeightParam) { - mRegionHeightParam = pRegionHeightParam; + regionHeightParam = pRegionHeightParam; } public void setRegionWidthParam(String pRegionWidthParam) { - mRegionWidthParam = pRegionWidthParam; + regionWidthParam = pRegionWidthParam; } public void setRegionLeftParam(String pRegionLeftParam) { - mRegionLeftParam = pRegionLeftParam; + regionLeftParam = pRegionLeftParam; } public void setRegionTopParam(String pRegionTopParam) { - mRegionTopParam = pRegionTopParam; + regionTopParam = pRegionTopParam; } public void setSizeHeightParam(String pSizeHeightParam) { - mSizeHeightParam = pSizeHeightParam; + sizeHeightParam = pSizeHeightParam; } public void setSizeWidthParam(String pSizeWidthParam) { - mSizeWidthParam = pSizeWidthParam; + sizeWidthParam = pSizeWidthParam; } public void setRegionPercentParam(String pRegionPercentParam) { - mRegionPercentParam = pRegionPercentParam; + regionPercentParam = pRegionPercentParam; } public void setRegionUniformParam(String pRegionUniformParam) { - mRegionUniformParam = pRegionUniformParam; + regionUniformParam = pRegionUniformParam; } public void setSizePercentParam(String pSizePercentParam) { - mSizePercentParam = pSizePercentParam; + sizePercentParam = pSizePercentParam; } public void setSizeUniformParam(String pSizeUniformParam) { - mSizeUniformParam = pSizeUniformParam; + sizeUniformParam = pSizeUniformParam; } public void init() throws ServletException { - if (mTriggerParams == null) { + if (triggerParams == null) { // Add all params as triggers - mTriggerParams = new String[]{mSizeWidthParam, mSizeHeightParam, - mSizeUniformParam, mSizePercentParam, - mRegionLeftParam, mRegionTopParam, - mRegionWidthParam, mRegionHeightParam, - mRegionUniformParam, mRegionPercentParam}; + triggerParams = new String[]{sizeWidthParam, sizeHeightParam, + sizeUniformParam, sizePercentParam, + regionLeftParam, regionTopParam, + regionWidthParam, regionHeightParam, + regionUniformParam, regionPercentParam}; } } @@ -101,37 +101,37 @@ public class SourceRenderFilter extends ImageFilter { // TODO: Max size configuration, to avoid DOS attacks? OutOfMemory // Size parameters - int width = ServletUtil.getIntParameter(pRequest, mSizeWidthParam, -1); - int height = ServletUtil.getIntParameter(pRequest, mSizeHeightParam, -1); + int width = ServletUtil.getIntParameter(pRequest, sizeWidthParam, -1); + int height = ServletUtil.getIntParameter(pRequest, sizeHeightParam, -1); if (width > 0 || height > 0) { pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE, new Dimension(width, height)); } // Size uniform/percent - boolean uniform = ServletUtil.getBooleanParameter(pRequest, mSizeUniformParam, true); + boolean uniform = ServletUtil.getBooleanParameter(pRequest, sizeUniformParam, true); if (!uniform) { pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.FALSE); } - boolean percent = ServletUtil.getBooleanParameter(pRequest, mSizePercentParam, false); + boolean percent = ServletUtil.getBooleanParameter(pRequest, sizePercentParam, false); if (percent) { pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE); } // Area of interest parameters - int x = ServletUtil.getIntParameter(pRequest, mRegionLeftParam, -1); // Default is center - int y = ServletUtil.getIntParameter(pRequest, mRegionTopParam, -1); // Default is center - width = ServletUtil.getIntParameter(pRequest, mRegionWidthParam, -1); - height = ServletUtil.getIntParameter(pRequest, mRegionHeightParam, -1); + int x = ServletUtil.getIntParameter(pRequest, regionLeftParam, -1); // Default is center + int y = ServletUtil.getIntParameter(pRequest, regionTopParam, -1); // Default is center + width = ServletUtil.getIntParameter(pRequest, regionWidthParam, -1); + height = ServletUtil.getIntParameter(pRequest, regionHeightParam, -1); if (width > 0 || height > 0) { pRequest.setAttribute(ImageServletResponse.ATTRIB_AOI, new Rectangle(x, y, width, height)); } // AOI uniform/percent - uniform = ServletUtil.getBooleanParameter(pRequest, mRegionUniformParam, false); + uniform = ServletUtil.getBooleanParameter(pRequest, regionUniformParam, false); if (uniform) { pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.TRUE); } - percent = ServletUtil.getBooleanParameter(pRequest, mRegionPercentParam, false); + percent = ServletUtil.getBooleanParameter(pRequest, regionPercentParam, false); if (percent) { pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE); } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/package_info.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/package_info.java index e12ed172..9aeedc02 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/package_info.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/package_info.java @@ -1,7 +1,6 @@ /** - * Contains various image-outputting servlets, that should run under any servlet engine. To create your own image servlet, simply subclass the servlet - * {@code ImageServlet}. Optionally implement the interface - * {@code ImagePainterServlet}, if you want to do painting. + * Contains various image-outputting filters, that should run under any + * servlet engine. *

    * Some of these methods may require use of the native graphics libraries * supported by the JVM, like the X libraries on Unix systems, and should be @@ -13,8 +12,8 @@ * AWT Enhancements and bugtraq report * 4281163 for more information on this issue. *

    - * If you cannot use JRE 1.4 for any reason, or do not want to use the X - * libraries, a possibilty is to use the + * If you cannot use JRE 1.4 or later, or do not want to use the X + * libraries, one possibility is to use the * PJA package (com.eteks.pja), * and start the JVM with the following options: *

    diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html b/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html index 2df9add1..e69de29b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html @@ -1,10 +0,0 @@ - - - -The TwelveMonkeys droplet TagLib. - -TODO: Insert taglib-descriptor here? - - - - diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package_info.java b/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package_info.java index 6681cfbc..e69de29b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package_info.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package_info.java @@ -1,4 +0,0 @@ -/** - * JSP support. - */ -package com.twelvemonkeys.servlet.jsp; \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java b/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java index e5daa6d5..e69de29b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java @@ -1,54 +0,0 @@ - -package com.twelvemonkeys.servlet.jsp.taglib; - -import java.io.File; -import java.util.Date; - -import javax.servlet.http.*; -import javax.servlet.jsp.*; -import javax.servlet.jsp.tagext.*; - -import com.twelvemonkeys.util.convert.*; - -/** - * Prints the last modified - */ - -public class LastModifiedTag extends TagSupport { - private String mFileName = null; - private String mFormat = null; - - public void setFile(String pFileName) { - mFileName = pFileName; - } - - public void setFormat(String pFormat) { - mFormat = pFormat; - } - - public int doStartTag() throws JspException { - File file = null; - - if (mFileName != null) { - file = new File(pageContext.getServletContext() - .getRealPath(mFileName)); - } - else { - HttpServletRequest request = - (HttpServletRequest) pageContext.getRequest(); - - // Get the file containing the servlet - file = new File(pageContext.getServletContext() - .getRealPath(request.getServletPath())); - } - - Date lastModified = new Date(file.lastModified()); - Converter conv = Converter.getInstance(); - - // Set the last modified value back - pageContext.setAttribute("lastModified", - conv.toString(lastModified, mFormat)); - - return Tag.EVAL_BODY_INCLUDE; - } -} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java index 30dd6711..42501e1e 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java @@ -1,13 +1,15 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.lang.ObjectAbstractTestCase; - -import java.util.*; -import java.net.URL; -import java.net.MalformedURLException; -import java.io.*; +import org.junit.Test; import javax.servlet.*; +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +import static org.junit.Assert.*; /** * FilterAbstractTestCase @@ -45,6 +47,7 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { return new MockFilterChain(); } + @Test public void testInitNull() { Filter filter = makeFilter(); @@ -65,6 +68,7 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } } + @Test public void testInit() { Filter filter = makeFilter(); @@ -79,6 +83,7 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } } + @Test public void testLifeCycle() throws ServletException { Filter filter = makeFilter(); @@ -90,6 +95,7 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } } + @Test public void testFilterBasic() throws ServletException, IOException { Filter filter = makeFilter(); @@ -103,22 +109,19 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } } + @Test public void testDestroy() { // TODO: Implement } static class MockFilterConfig implements FilterConfig { - private final Map mParams; + private final Map params; - MockFilterConfig() { - this(new HashMap()); - } - - MockFilterConfig(Map pParams) { + MockFilterConfig(Map pParams) { if (pParams == null) { throw new IllegalArgumentException("params == null"); } - mParams = pParams; + params = pParams; } public String getFilterName() { @@ -126,11 +129,11 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } public String getInitParameter(String pName) { - return (String) mParams.get(pName); + return params.get(pName); } public Enumeration getInitParameterNames() { - return Collections.enumeration(mParams.keySet()); + return Collections.enumeration(params.keySet()); } public ServletContext getServletContext() { @@ -138,20 +141,20 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } private static class MockServletContext implements ServletContext { - private final Map mAttributes; - private final Map mParams; + private final Map attributes; + private final Map params; MockServletContext() { - mAttributes = new HashMap(); - mParams = new HashMap(); + attributes = new HashMap(); + params = new HashMap(); } public Object getAttribute(String s) { - return mAttributes.get(s); + return attributes.get(s); } public Enumeration getAttributeNames() { - return Collections.enumeration(mAttributes.keySet()); + return Collections.enumeration(attributes.keySet()); } public ServletContext getContext(String s) { @@ -159,11 +162,11 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } public String getInitParameter(String s) { - return (String) mParams.get(s); + return (String) params.get(s); } public Enumeration getInitParameterNames() { - return Collections.enumeration(mParams.keySet()); + return Collections.enumeration(params.keySet()); } public int getMajorVersion() { @@ -223,40 +226,37 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } public void log(Exception exception, String s) { - // TODO: Implement } public void log(String s) { - // TODO: Implement } public void log(String s, Throwable throwable) { - // TODO: Implement } public void removeAttribute(String s) { - mAttributes.remove(s); + attributes.remove(s); } public void setAttribute(String s, Object obj) { - mAttributes.put(s, obj); + attributes.put(s, obj); } } } static class MockServletRequest implements ServletRequest { - final private Map mAttributes; + final private Map attributes; public MockServletRequest() { - mAttributes = new HashMap(); + attributes = new HashMap(); } public Object getAttribute(String pKey) { - return mAttributes.get(pKey); + return attributes.get(pKey); } public Enumeration getAttributeNames() { - return Collections.enumeration(mAttributes.keySet()); + return Collections.enumeration(attributes.keySet()); } public String getCharacterEncoding() { @@ -324,11 +324,11 @@ public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { } public void setAttribute(String pKey, Object pValue) { - mAttributes.put(pKey, pValue); + attributes.put(pKey, pValue); } public void removeAttribute(String pKey) { - mAttributes.remove(pKey); + attributes.remove(pKey); } public Locale getLocale() { diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java index fd81745f..92a036b2 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java @@ -1,10 +1,13 @@ package com.twelvemonkeys.servlet; -import java.io.IOException; -import java.util.Map; -import java.util.HashMap; +import org.junit.Test; import javax.servlet.*; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; /** * GenericFilterTestCase @@ -19,6 +22,7 @@ public final class GenericFilterTestCase extends FilterAbstractTestCase { return new GenericFilterImpl(); } + @Test public void testInitOncePerRequest() { // Default FALSE GenericFilter filter = new GenericFilterImpl(); @@ -30,12 +34,12 @@ public final class GenericFilterTestCase extends FilterAbstractTestCase { fail(e.getMessage()); } - assertFalse("OncePerRequest should default to false", filter.mOncePerRequest); + assertFalse("OncePerRequest should default to false", filter.oncePerRequest); filter.destroy(); // TRUE filter = new GenericFilterImpl(); - Map params = new HashMap(); + Map params = new HashMap(); params.put("once-per-request", "true"); try { @@ -45,12 +49,12 @@ public final class GenericFilterTestCase extends FilterAbstractTestCase { fail(e.getMessage()); } - assertTrue("oncePerRequest should be true", filter.mOncePerRequest); + assertTrue("oncePerRequest should be true", filter.oncePerRequest); filter.destroy(); // TRUE filter = new GenericFilterImpl(); - params = new HashMap(); + params = new HashMap(); params.put("oncePerRequest", "true"); try { @@ -60,10 +64,11 @@ public final class GenericFilterTestCase extends FilterAbstractTestCase { fail(e.getMessage()); } - assertTrue("oncePerRequest should be true", filter.mOncePerRequest); + assertTrue("oncePerRequest should be true", filter.oncePerRequest); filter.destroy(); } + @Test public void testFilterOnlyOnce() { final GenericFilterImpl filter = new GenericFilterImpl(); filter.setOncePerRequest(true); @@ -92,6 +97,7 @@ public final class GenericFilterTestCase extends FilterAbstractTestCase { filter.destroy(); } + @Test public void testFilterMultiple() { final GenericFilterImpl filter = new GenericFilterImpl(); diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java index db23185d..93df14a6 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java @@ -1,11 +1,12 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.io.NullOutputStream; - -import junit.framework.TestCase; +import org.junit.Test; import java.io.PrintWriter; +import static org.junit.Assert.*; + /** * ServletConfigExceptionTestCase * @@ -13,7 +14,8 @@ import java.io.PrintWriter; * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java#2 $ */ -public class ServletConfigExceptionTestCase extends TestCase { +public class ServletConfigExceptionTestCase { + @Test public void testThrowCatchPrintStacktrace() { try { throw new ServletConfigException("FooBar!"); @@ -23,6 +25,7 @@ public class ServletConfigExceptionTestCase extends TestCase { } } + @Test public void testThrowCatchGetNoCause() { try { throw new ServletConfigException("FooBar!"); @@ -35,6 +38,7 @@ public class ServletConfigExceptionTestCase extends TestCase { } } + @Test public void testThrowCatchInitCauseNull() { try { ServletConfigException e = new ServletConfigException("FooBar!"); @@ -49,6 +53,7 @@ public class ServletConfigExceptionTestCase extends TestCase { } } + @Test public void testThrowCatchInitCause() { //noinspection ThrowableInstanceNeverThrown Exception cause = new Exception(); @@ -66,6 +71,7 @@ public class ServletConfigExceptionTestCase extends TestCase { } } + @Test public void testThrowCatchGetNullCause() { try { throw new ServletConfigException("FooBar!", null); @@ -78,6 +84,7 @@ public class ServletConfigExceptionTestCase extends TestCase { } } + @Test public void testThrowCatchGetCause() { IllegalStateException cause = new IllegalStateException(); try { diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java index 976e506f..4b60c01c 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java @@ -35,7 +35,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas } private static class TestConfig implements ServletConfig, FilterConfig, ServletContext, Serializable, Cloneable { - Map mMap = new HashMap(); + Map map = new HashMap(); public String getServletName() { return "dummy"; // Not needed for this test @@ -55,12 +55,12 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas } public String getInitParameter(String s) { - return (String) mMap.get(s); + return (String) map.get(s); } public Enumeration getInitParameterNames() { //noinspection unchecked - return Collections.enumeration(mMap.keySet()); + return Collections.enumeration(map.keySet()); } public ServletContext getContext(String uripath) { @@ -157,7 +157,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas public Map makeFullMap() { ServletConfig config = new TestConfig(); - addSampleMappings(((TestConfig) config).mMap); + addSampleMappings(((TestConfig) config).map); return new ServletConfigMapAdapter(config); } } @@ -171,7 +171,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas public Map makeFullMap() { FilterConfig config = new TestConfig(); - addSampleMappings(((TestConfig) config).mMap); + addSampleMappings(((TestConfig) config).map); return new ServletConfigMapAdapter(config); } } @@ -185,7 +185,7 @@ public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCas public Map makeFullMap() { FilterConfig config = new TestConfig(); - addSampleMappings(((TestConfig) config).mMap); + addSampleMappings(((TestConfig) config).map); return new ServletConfigMapAdapter(config); } } diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfiguratorTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfiguratorTestCase.java new file mode 100644 index 00000000..63f9565f --- /dev/null +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfiguratorTestCase.java @@ -0,0 +1,346 @@ +package com.twelvemonkeys.servlet; + +import org.junit.Test; +import org.mockito.Matchers; + +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * ServletConfiguratorTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ServletConfiguratorTestCase.java,v 1.0 May 2, 2010 3:08:33 PM haraldk Exp$ + */ +public class ServletConfiguratorTestCase { + + // TODO: Test error conditions: + // - Missing name = ... or non-bean conforming method + // - Non-accessible? How..? + // - Missing required value + + // TODO: Clean up tests to test only one thing at a time + // - Public method + // - Public method with override + // - Public method overridden without annotation + // - Protected method + // - Protected method with override + // - Protected method overridden without annotation + // - Package protected method + // - Package protected method with override + // - Package protected method overridden without annotation + // - Private method + // - Multiple private methods with same signature (should invoke all, as private methods can't be overridden) + + @Test + public void testConfigureAnnotatedServlet() throws ServletConfigException { + AnnotatedServlet servlet = mock(AnnotatedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("x", "foo", "bar"))); + when(config.getInitParameter("x")).thenReturn("99"); + when(config.getInitParameter("foo")).thenReturn("Foo"); + when(config.getInitParameter("bar")).thenReturn("-1, 2, 0, 42"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setX(99); + verify(servlet, times(1)).setFoo("Foo"); + verify(servlet, times(1)).configTheBar(-1, 2, 0, 42); + } + + @Test + public void testConfigureAnnotatedFilter() throws ServletConfigException { + AnnotatedServlet servlet = mock(AnnotatedServlet.class); + + FilterConfig config = mock(FilterConfig.class); + when(config.getFilterName()).thenReturn("FooFilter"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("x", "foo", "bar"))); + when(config.getInitParameter("x")).thenReturn("99"); + when(config.getInitParameter("foo")).thenReturn("Foo"); + when(config.getInitParameter("bar")).thenReturn("-1, 2, 0, 42"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setX(99); + verify(servlet, times(1)).setFoo("Foo"); + verify(servlet, times(1)).configTheBar(-1, 2, 0, 42); + } + + @Test + public void testConfigurePrivateMethod() throws ServletConfigException { + AnnotatedServlet servlet = mock(AnnotatedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("private"))); + when(config.getInitParameter("private")).thenReturn("99"); + + ServletConfigurator.configure(servlet, config); + + // Verify + assertEquals(servlet.priv, "99"); + } + + @Test + public void testConfigurePrivateShadowedMethod() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @InitParam(name = "package-private") + abstract void setPrivate(String priv); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("private"))); + when(config.getInitParameter("private")).thenReturn("private"); + when(config.getInitParameter("package-private")).thenReturn("package"); + + ServletConfigurator.configure(servlet, config); + + // Verify + assertEquals(servlet.priv, "private"); + verify(servlet, times(1)).setPrivate("package"); + } + + @Test + public void testConfigureSubclassedServlet() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @InitParam(name = "flag") + abstract void configureMeToo(boolean flag); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("x", "foo", "bar", "flag"))); + when(config.getInitParameter("x")).thenReturn("99"); + when(config.getInitParameter("foo")).thenReturn("Foo"); + when(config.getInitParameter("bar")).thenReturn("-1, 2, 0, 42"); + when(config.getInitParameter("flag")).thenReturn("true"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setX(99); + verify(servlet, times(1)).setFoo("Foo"); + verify(servlet, times(1)).configTheBar(-1, 2, 0, 42); + verify(servlet, times(1)).configureMeToo(true); + } + + @Test + public void testConfigureAnnotatedServletWithLispStyle() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @InitParam(name = "the-explicit-x") + abstract public void setExplicitX(int x); + + @InitParam + abstract public void setTheOtherX(int x); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("the-explicit-x", "the-other-x"))); + when(config.getInitParameter("the-explicit-x")).thenReturn("-1"); + when(config.getInitParameter("the-other-x")).thenReturn("42"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setExplicitX(-1); + verify(servlet, times(1)).setTheOtherX(42); + } + + @Test + public void testConfigureSubclassedServletWithOverride() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @Override + @InitParam(name = "y") + public void setX(int x) { + } + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("x", "y"))); + when(config.getInitParameter("x")).thenReturn("99"); + when(config.getInitParameter("y")).thenReturn("-66"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setX(-66); + verify(servlet, times(1)).setX(anyInt()); // We don't want multiple invocations, only the overridden method + } + + @Test + public void testConfigureSubclassedServletWithOverrideNoParam() throws ServletConfigException { + // NOTE: We must allow overriding the methods without annotation present, in order to allow CGLib/proxies of the class... + abstract class SubclassedServlet extends AnnotatedServlet { + @Override + @InitParam(name = "") + public void setX(int x) { + } + + @Override + public void setFoo(String foo) { + } + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("x", "foo"))); + when(config.getInitParameter("x")).thenReturn("99"); + when(config.getInitParameter("foo")).thenReturn("Foo"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, never()).setX(anyInt()); + verify(servlet, times(1)).setFoo("Foo"); + verify(servlet, times(1)).setFoo(Matchers.any()); // We don't want multiple invocations + } + + // Test interface + @Test + public void testConfigureServletWithInterface() throws ServletConfigException { + abstract class InterfacedServlet implements Servlet, Annotated { + } + + InterfacedServlet servlet = mock(InterfacedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("foo"))); + when(config.getInitParameter("foo")).thenReturn("Foo"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).annotated("Foo"); + } + + // TODO: Test override/shadow of package protected method outside package + + @Test + public void testRequiredParameter() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @InitParam(required = true) + abstract void setRequired(String value); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("required"))); + when(config.getInitParameter("required")).thenReturn("the required value"); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setRequired("the required value"); + verify(servlet, times(1)).setRequired(Matchers.any()); // We don't want multiple invocations + } + + @Test + public void testMissingParameter() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @InitParam() + abstract void setNonRequired(String value); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Collections.emptyList())); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, never()).setNonRequired(Matchers.any()); // Simply not configured + } + + @Test(expected = ServletConfigException.class) + public void testMissingRequiredParameter() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @Override + @InitParam(required = true) + protected abstract void setFoo(String value); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Collections.emptyList())); + + ServletConfigurator.configure(servlet, config); // Should throw exception + } + + @Test + public void testMissingParameterDefaultValue() throws ServletConfigException { + abstract class SubclassedServlet extends AnnotatedServlet { + @InitParam(defaultValue = "1, 2, 3") + abstract void setNonRequired(int[] value); + } + + SubclassedServlet servlet = mock(SubclassedServlet.class); + + ServletConfig config = mock(ServletConfig.class); + when(config.getServletName()).thenReturn("FooServlet"); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Collections.emptyList())); + + ServletConfigurator.configure(servlet, config); + + // Verify + verify(servlet, times(1)).setNonRequired(new int[] {1, 2, 3}); + verify(servlet, times(1)).setNonRequired(Matchers.any()); + } + + + public interface Annotated { + @InitParam(name = "foo") + public void annotated(String an); + } + + public abstract class AnnotatedServlet implements Servlet, Filter { + String priv; + + // Implicit name "x" + @InitParam + public abstract void setX(int x); + + // Implicit name "foo" + @InitParam + protected abstract void setFoo(String foo); + + @InitParam(name = "bar") + abstract void configTheBar(int... bar); + + @InitParam(name = "private") + private void setPrivate(String priv) { + this.priv = priv; + } + } +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java index 6f0b180a..2a3c0861 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java @@ -1,20 +1,21 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.util.MapAbstractTestCase; -import org.jmock.Mock; -import org.jmock.core.Invocation; -import org.jmock.core.Stub; -import org.jmock.core.stub.CustomStub; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import javax.servlet.http.HttpServletRequest; import java.util.*; +import static org.mockito.Mockito.when; + /** * ServletConfigMapAdapterTestCase *

    * * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java#1 $ + * @version $Id: ServletHeadersMapAdapterTestCase.java#1 $ */ public class ServletHeadersMapAdapterTestCase extends MapAbstractTestCase { private static final List HEADER_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\""); @@ -43,24 +44,21 @@ public class ServletHeadersMapAdapterTestCase extends MapAbstractTestCase { } public Map makeEmptyMap() { - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getHeaderNames").will(returnValue(Collections.enumeration(Collections.emptyList()))); - mockRequest.stubs().method("getHeaders").will(returnValue(null)); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + when(request.getHeaderNames()).thenAnswer(returnEnumeration(Collections.emptyList())); - return new SerlvetHeadersMapAdapter((HttpServletRequest) mockRequest.proxy()); + return new ServletHeadersMapAdapter(request); } @Override public Map makeFullMap() { - Mock mockRequest = mock(HttpServletRequest.class); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + when(request.getHeaderNames()).thenAnswer(returnEnumeration(Arrays.asList(getSampleKeys()))); + when(request.getHeaders("Date")).thenAnswer(returnEnumeration(HEADER_VALUE_DATE)); + when(request.getHeaders("ETag")).thenAnswer(returnEnumeration(HEADER_VALUE_ETAG)); + when(request.getHeaders("X-Foo")).thenAnswer(returnEnumeration(HEADER_VALUE_FOO)); - mockRequest.stubs().method("getHeaderNames").will(returnEnumeration("ETag", "Date", "X-Foo")); - mockRequest.stubs().method("getHeaders").with(eq("Date")).will(returnEnumeration(HEADER_VALUE_DATE)); - mockRequest.stubs().method("getHeaders").with(eq("ETag")).will(returnEnumeration(HEADER_VALUE_ETAG)); - mockRequest.stubs().method("getHeaders").with(eq("X-Foo")).will(returnEnumeration(HEADER_VALUE_FOO)); - mockRequest.stubs().method("getHeaders").with(not(or(eq("Date"), or(eq("ETag"), eq("X-Foo"))))).will(returnValue(null)); - - return new SerlvetHeadersMapAdapter((HttpServletRequest) mockRequest.proxy()); + return new ServletHeadersMapAdapter(request); } @Override @@ -73,31 +71,25 @@ public class ServletHeadersMapAdapterTestCase extends MapAbstractTestCase { return new Object[] {HEADER_VALUE_DATE, HEADER_VALUE_ETAG, HEADER_VALUE_FOO}; } - @Override public Object[] getNewSampleValues() { // Needs to be same length but different values return new Object[3]; } - protected Stub returnEnumeration(final Object... pValues) { - return new EnumerationStub(Arrays.asList(pValues)); + protected static ReturnNewEnumeration returnEnumeration(final Collection collection) { + return new ReturnNewEnumeration(collection); } - protected Stub returnEnumeration(final List pValues) { - return new EnumerationStub(pValues); - } + private static class ReturnNewEnumeration implements Answer> { + private final Collection collection; - private static class EnumerationStub extends CustomStub { - private List mValues; - - public EnumerationStub(final List pValues) { - super("Returns a new enumeration"); - mValues = pValues; + private ReturnNewEnumeration(final Collection collection) { + this.collection = collection; } - public Object invoke(Invocation invocation) throws Throwable { - return Collections.enumeration(mValues); + public Enumeration answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(collection); } } -} \ No newline at end of file +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java index 9c4cc437..883c1ac6 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java @@ -1,14 +1,15 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.util.MapAbstractTestCase; -import org.jmock.Mock; -import org.jmock.core.Invocation; -import org.jmock.core.Stub; -import org.jmock.core.stub.CustomStub; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import javax.servlet.http.HttpServletRequest; import java.util.*; +import static org.mockito.Mockito.when; + /** * ServletConfigMapAdapterTestCase *

    @@ -43,24 +44,22 @@ public class ServletParametersMapAdapterTestCase extends MapAbstractTestCase { } public Map makeEmptyMap() { - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getParameterNames").will(returnValue(Collections.enumeration(Collections.emptyList()))); - mockRequest.stubs().method("getParameterValues").will(returnValue(null)); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + when(request.getParameterNames()).thenAnswer(returnEnumeration(Collections.emptyList())); - return new SerlvetParametersMapAdapter((HttpServletRequest) mockRequest.proxy()); + return new ServletParametersMapAdapter(request); } @Override public Map makeFullMap() { - Mock mockRequest = mock(HttpServletRequest.class); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - mockRequest.stubs().method("getParameterNames").will(returnEnumeration("tag", "date", "foo")); - mockRequest.stubs().method("getParameterValues").with(eq("date")).will(returnValue(PARAM_VALUE_DATE.toArray(new String[PARAM_VALUE_DATE.size()]))); - mockRequest.stubs().method("getParameterValues").with(eq("tag")).will(returnValue(PARAM_VALUE_ETAG.toArray(new String[PARAM_VALUE_ETAG.size()]))); - mockRequest.stubs().method("getParameterValues").with(eq("foo")).will(returnValue(PARAM_VALUE_FOO.toArray(new String[PARAM_VALUE_FOO.size()]))); - mockRequest.stubs().method("getParameterValues").with(not(or(eq("date"), or(eq("tag"), eq("foo"))))).will(returnValue(null)); + when(request.getParameterNames()).thenAnswer(returnEnumeration(Arrays.asList("tag", "date", "foo"))); + when(request.getParameterValues("date")).thenReturn(PARAM_VALUE_DATE.toArray(new String[PARAM_VALUE_DATE.size()])); + when(request.getParameterValues("tag")).thenReturn(PARAM_VALUE_ETAG.toArray(new String[PARAM_VALUE_ETAG.size()])); + when(request.getParameterValues("foo")).thenReturn(PARAM_VALUE_FOO.toArray(new String[PARAM_VALUE_FOO.size()])); - return new SerlvetParametersMapAdapter((HttpServletRequest) mockRequest.proxy()); + return new ServletParametersMapAdapter(request); } @Override @@ -79,24 +78,19 @@ public class ServletParametersMapAdapterTestCase extends MapAbstractTestCase { return new Object[3]; } - protected Stub returnEnumeration(final Object... pValues) { - return new EnumerationStub(Arrays.asList(pValues)); + protected static ReturnNewEnumeration returnEnumeration(final Collection collection) { + return new ReturnNewEnumeration(collection); } - protected Stub returnEnumeration(final List pValues) { - return new EnumerationStub(pValues); - } + private static class ReturnNewEnumeration implements Answer> { + private final Collection collection; - private static class EnumerationStub extends CustomStub { - private List mValues; - - public EnumerationStub(final List pValues) { - super("Returns a new enumeration"); - mValues = pValues; + private ReturnNewEnumeration(final Collection collection) { + this.collection = collection; } - public Object invoke(Invocation invocation) throws Throwable { - return Collections.enumeration(mValues); + public Enumeration answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(collection); } } } \ No newline at end of file diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/StaticContentServletTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/StaticContentServletTestCase.java new file mode 100644 index 00000000..922bb6cb --- /dev/null +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/StaticContentServletTestCase.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.servlet; + +import org.junit.Test; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.*; + + +/** + * StaticContentServletTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: StaticContentServletTestCase.java,v 1.0 12.12.11 15:10 haraldk Exp$ + */ +public class StaticContentServletTestCase { + + private static final String IMAGE_RESOURCE = "/12monkeys-splash.png"; + + private static String getFileSystemRoot() { + File root = getResourceAsFile(IMAGE_RESOURCE).getParentFile(); + return root.getAbsolutePath(); + } + + private static File getResourceAsFile(String resourceName) { + URL resource = StaticContentServletTestCase.class.getResource("/com/twelvemonkeys/servlet/image" + resourceName); + + try { + return new File(resource.toURI()); + } + catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Test(expected = ServletException.class) + public void testBadInitNoRoot() throws ServletException { + StaticContentServlet servlet = new StaticContentServlet(); + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Collections.emptyList())); + + servlet.init(config); + } + + @Test(expected = ServletException.class) + public void testBadInit() throws ServletException { + StaticContentServlet servlet = new StaticContentServlet(); + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn("foo/bar"); + + servlet.init(config); + } + + @Test + public void testNotFound() throws ServletException, IOException { + StaticContentServlet servlet = new StaticContentServlet(); + + ServletContext context = mock(ServletContext.class); + when(context.getServletContextName()).thenReturn("foo"); + when(context.getMimeType(anyString())).thenReturn("image/jpeg"); + + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn(getFileSystemRoot()); + when(config.getServletContext()).thenReturn(context); + + servlet.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getPathInfo()).thenReturn("/missing.jpg"); + when(request.getRequestURI()).thenReturn("/foo/missing.jpg"); + when(request.getContextPath()).thenReturn("/foo"); + + HttpServletResponse response = mock(HttpServletResponse.class); + + servlet.service(request, response); + + verify(response).sendError(HttpServletResponse.SC_NOT_FOUND, "/foo/missing.jpg"); + } + + @Test + public void testDirectoryListingForbidden() throws ServletException, IOException { + StaticContentServlet servlet = new StaticContentServlet(); + + ServletContext context = mock(ServletContext.class); + when(context.getServletContextName()).thenReturn("foo"); + when(context.getMimeType(anyString())).thenReturn("image/png"); + + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn(getFileSystemRoot()); + when(config.getServletContext()).thenReturn(context); + + servlet.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getPathInfo()).thenReturn("/"); // Attempt directory listing + when(request.getRequestURI()).thenReturn("/foo/"); // Attempt directory listing + when(request.getContextPath()).thenReturn("/foo"); + + ServletOutputStream stream = mock(ServletOutputStream.class); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(stream); + + servlet.service(request, response); + + verify(response).sendError(HttpServletResponse.SC_FORBIDDEN, "/foo/"); + } + + @Test + public void testGet() throws ServletException, IOException { + StaticContentServlet servlet = new StaticContentServlet(); + + ServletContext context = mock(ServletContext.class); + when(context.getServletContextName()).thenReturn("foo"); + when(context.getMimeType(anyString())).thenReturn("image/png"); + + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn(getFileSystemRoot()); + when(config.getServletContext()).thenReturn(context); + + servlet.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getPathInfo()).thenReturn(IMAGE_RESOURCE); + when(request.getRequestURI()).thenReturn("/foo" + IMAGE_RESOURCE); + when(request.getContextPath()).thenReturn("/foo"); + + ServletOutputStream stream = mock(ServletOutputStream.class); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(stream); + + servlet.service(request, response); + + verify(response).setStatus(HttpServletResponse.SC_OK); +// verify(stream, atLeastOnce()).write((byte[]) any(), anyInt(), anyInt()); // Mockito bug? + verify(stream, times(51)).write((byte[]) any(), anyInt(), anyInt()); // This test is fragile, but the above throws exception..? + verify(stream, atLeastOnce()).flush(); + } + + @Test + public void testGetConfiguredForSingleFile() throws ServletException, IOException { + StaticContentServlet servlet = new StaticContentServlet(); + + ServletContext context = mock(ServletContext.class); + when(context.getServletContextName()).thenReturn("foo"); + when(context.getMimeType(anyString())).thenReturn("text/plain"); + + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn(getFileSystemRoot() + "/foo.txt"); + when(config.getServletContext()).thenReturn(context); + + servlet.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getPathInfo()).thenReturn(null); + when(request.getRequestURI()).thenReturn("/foo"); + when(request.getContextPath()).thenReturn("/foo"); + + ServletOutputStream stream = mock(ServletOutputStream.class); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(stream); + + servlet.service(request, response); + + verify(response).setStatus(HttpServletResponse.SC_OK); +// verify(stream, atLeastOnce()).write((byte[]) any(), anyInt(), anyInt()); // Mockito bug? + verify(stream, times(1)).write((byte[]) any(), anyInt(), anyInt()); // This test is fragile, but the above throws exception..? + verify(stream, atLeastOnce()).flush(); + } + + @Test + public void testGetNotModified() throws ServletException, IOException { + StaticContentServlet servlet = new StaticContentServlet(); + + ServletContext context = mock(ServletContext.class); + when(context.getServletContextName()).thenReturn("foo"); + when(context.getMimeType(anyString())).thenReturn("image/png"); + + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn(getFileSystemRoot()); + when(config.getServletContext()).thenReturn(context); + + servlet.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getPathInfo()).thenReturn(IMAGE_RESOURCE); + when(request.getRequestURI()).thenReturn("/foo" + IMAGE_RESOURCE); + when(request.getContextPath()).thenReturn("/foo"); + when(request.getDateHeader("If-Modified-Since")).thenReturn(getResourceAsFile(IMAGE_RESOURCE).lastModified()); + + ServletOutputStream stream = mock(ServletOutputStream.class); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(stream); + + servlet.service(request, response); + + verify(response).setStatus(HttpServletResponse.SC_NOT_MODIFIED); + verifyZeroInteractions(stream); + } + + @Test + public void testHead() throws ServletException, IOException { + StaticContentServlet servlet = new StaticContentServlet(); + + ServletContext context = mock(ServletContext.class); + when(context.getServletContextName()).thenReturn("foo"); + when(context.getMimeType(anyString())).thenReturn("image/png"); + + ServletConfig config = mock(ServletConfig.class); + when(config.getInitParameterNames()).thenReturn(Collections.enumeration(Arrays.asList("root"))); + when(config.getInitParameter("root")).thenReturn(getFileSystemRoot()); + when(config.getServletContext()).thenReturn(context); + + servlet.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("HEAD"); + when(request.getPathInfo()).thenReturn(IMAGE_RESOURCE); + when(request.getRequestURI()).thenReturn("/foo" + IMAGE_RESOURCE); + when(request.getContextPath()).thenReturn("/foo"); + + ServletOutputStream stream = mock(ServletOutputStream.class); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(stream); + + servlet.service(request, response); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verifyZeroInteractions(stream); + } +} diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java index 2da59456..a36b2b11 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java @@ -1,13 +1,15 @@ package com.twelvemonkeys.servlet; import com.twelvemonkeys.io.OutputStreamAbstractTestCase; +import org.junit.Test; +import javax.servlet.Filter; +import javax.servlet.ServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import javax.servlet.Filter; -import javax.servlet.ServletResponse; +import static org.junit.Assert.*; /** * TrimWhiteSpaceFilterTestCase @@ -23,7 +25,6 @@ public class TrimWhiteSpaceFilterTestCase extends FilterAbstractTestCase { } public static final class TrimWSFilterOutputStreamTestCase extends OutputStreamAbstractTestCase { - protected OutputStream makeObject() { // NOTE: ByteArrayOutputStream does not implement flush or close... return makeOutputStream(new ByteArrayOutputStream(16)); @@ -33,6 +34,7 @@ public class TrimWhiteSpaceFilterTestCase extends FilterAbstractTestCase { return new TrimWhiteSpaceFilter.TrimWSFilterOutputStream(pWrapped); } + @Test public void testTrimWSOnlyWS() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(64); OutputStream trim = makeOutputStream(out); @@ -46,6 +48,7 @@ public class TrimWhiteSpaceFilterTestCase extends FilterAbstractTestCase { assertEquals("Should be trimmed", "\"\"", '"' + new String(out.toByteArray()) + '"'); } + @Test public void testTrimWSLeading() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(64); OutputStream trim = makeOutputStream(out); @@ -60,6 +63,7 @@ public class TrimWhiteSpaceFilterTestCase extends FilterAbstractTestCase { assertEquals("Should be trimmed", '"' + trimmed + '"', '"' + new String(out.toByteArray()) + '"'); } + @Test public void testTrimWSOffsetLength() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(64); OutputStream trim = makeOutputStream(out); diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java index 4c3b11b5..b5b69f45 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java @@ -1,19 +1,26 @@ package com.twelvemonkeys.servlet.cache; -import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.net.NetUtil; -import org.jmock.Mock; -import org.jmock.cglib.MockObjectTestCase; -import org.jmock.core.Invocation; -import org.jmock.core.stub.CustomStub; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + /** * CacheManagerTestCase * @@ -21,31 +28,38 @@ import java.util.*; * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java#2 $ */ -public class HTTPCacheTestCase extends MockObjectTestCase { - // TODO: Clean up! +public class HTTPCacheTestCase { + @Rule + public final TestName name = new TestName(); - private static final File TEMP_ROOT = new File(FileUtil.getTempDirFile(), "cache-test"); + // TODO: Replace with rule: TemporaryFolder + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Override - protected void setUp() throws Exception { - super.setUp(); - - assertTrue("Could not create temp dir, tests can not run", (TEMP_ROOT.exists() && TEMP_ROOT.isDirectory()) || TEMP_ROOT.mkdirs()); - // Clear temp dir - File[] files = TEMP_ROOT.listFiles(); - for (File file : files) { - file.delete(); - } + private File getTempRoot() { + return temporaryFolder.newFolder("cache-test"); } - @Override - protected void tearDown() throws Exception { - super.tearDown(); + private CacheRequest configureRequest(CacheRequest request, String pRequestURI) { + return configureRequest(request, "GET", pRequestURI, null, null); } + private CacheRequest configureRequest(CacheRequest request, String pMethod, String pRequestURI, Map> pParameters, final Map> pHeaders) { + reset(request); + + when(request.getRequestURI()).thenReturn(URI.create(pRequestURI)); + when(request.getParameters()).thenReturn(pParameters == null ? Collections.>emptyMap() : pParameters); + when(request.getHeaders()).thenReturn(pHeaders == null ? Collections.>emptyMap() : pHeaders); + when(request.getMethod()).thenReturn(pMethod); + + return request; + } + + @SuppressWarnings("deprecation") + @Test public void testCreateNegativeNoName() { try { - new HTTPCache(null, (ServletContext) newDummy(ServletContext.class), 500, 0, 10, true); + new HTTPCache(null, mock(ServletContext.class), 500, 0, 10, true); fail("Expected creation failure, no name"); } catch (IllegalArgumentException expected) { @@ -55,7 +69,7 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } try { - new HTTPCache("", (ServletContext) newDummy(ServletContext.class), 500, 0, 10, true); + new HTTPCache("", mock(ServletContext.class), 500, 0, 10, true); fail("Expected creation failure, empty name"); } catch (IllegalArgumentException expected) { @@ -65,6 +79,8 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } } + @SuppressWarnings("deprecation") + @Test public void testCreateNegativeNoContext() { try { new HTTPCache("Dummy", null, 500, 0, 10, true); @@ -78,6 +94,7 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } + @Test public void testCreateNegativeNoTempFolder() { try { new HTTPCache(null, 500, 0, 10, true); @@ -91,9 +108,10 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } } + @Test public void testCreateNegativeValues() { try { - new HTTPCache(TEMP_ROOT, -1, 0, 10, true); + new HTTPCache(getTempRoot(), -1, 0, 10, true); fail("Expected creation failure"); } catch (IllegalArgumentException expected) { @@ -103,7 +121,7 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } try { - new HTTPCache(TEMP_ROOT, 1000, -1, 10, false); + new HTTPCache(getTempRoot(), 1000, -1, 10, false); fail("Expected creation failure"); } catch (IllegalArgumentException expected) { @@ -113,7 +131,7 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } try { - new HTTPCache(TEMP_ROOT, 1000, 128, -1, true); + new HTTPCache(getTempRoot(), 1000, 128, -1, true); fail("Expected creation failure"); } catch (IllegalArgumentException expected) { @@ -123,216 +141,152 @@ public class HTTPCacheTestCase extends MockObjectTestCase { } } + @Test public void testCreate() { - new HTTPCache(TEMP_ROOT, 500, 0, 10, true); + new HTTPCache(getTempRoot(), 500, 0, 10, true); } + @SuppressWarnings("deprecation") + @Test public void testCreateServletContext() { - Mock mockContext = mock(ServletContext.class); + ServletContext mockContext = mock(ServletContext.class); // Currently context is used for tempdir and logging - mockContext.stubs().method("getAttribute").with(eq("javax.servlet.context.tempdir")).will(returnValue(TEMP_ROOT)); - new HTTPCache("cache", (ServletContext) mockContext.proxy(), 500, 0, 10, true); + when(mockContext.getAttribute(eq("javax.servlet.context.tempdir"))).thenReturn(getTempRoot()); + + new HTTPCache("cache", mockContext, 500, 0, 10, true); } + @Test public void testCacheableRequest() throws IOException, CacheException { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } private String createRequestURI() { - return "http://www.foo.com/" + getName() + ".bar"; + return "http://www.foo.com/" + name.getMethodName() + ".bar"; } + @Test public void testCacheableRequestWithParameters() throws IOException, CacheException { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); Map> parameters = new HashMap>(); parameters.put("foo", Collections.singletonList("bar")); parameters.put("params", Arrays.asList("une", "due", "tres")); - CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), parameters, null); + CacheRequest request = configureRequest(mock(CacheRequest.class), "GET", createRequestURI(), parameters, null); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class, "MY-RESOLVER-1"); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } - private CacheRequest configureRequest(Mock pMockRequest, String pRequestURI) { - return configureRequest(pMockRequest, "GET", pRequestURI, null, null); - } - - private CacheRequest configureRequest(Mock pMockRequest, String pMethod, String pRequestURI, Map> pParameters, final Map> pHeaders) { - pMockRequest.reset(); - pMockRequest.stubs().method("getRequestURI").will(returnValue(URI.create(pRequestURI))); - pMockRequest.stubs().method("getParameters").will(returnValue(pParameters == null ? Collections.emptyMap() : pParameters)); - pMockRequest.stubs().method("getHeaders").will(returnValue(pHeaders == null ? Collections.emptyMap() : pHeaders)); - pMockRequest.stubs().method("getMethod").will(returnValue(pMethod)); - return (CacheRequest) pMockRequest.proxy(); - } - + @Test public void testCacheablePersistentRepeatedRequest() throws IOException, CacheException { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class, "MY-RESOLVER-2"); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockResolver.verify(); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); // Reset result.reset(); - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + reset(response); + when(response.getOutputStream()).thenReturn(result); // Test request again, make sure resolve is executed exactly once - HTTPCache cache2 = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache2 = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); cache2.doCached(request, response, resolver); // Test that second response is equal to first assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } + @Test public void testCacheableRepeatedRequest() throws IOException, CacheException { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class, "MY-RESOLVER-3"); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); @@ -341,18 +295,17 @@ public class HTTPCacheTestCase extends MockObjectTestCase { assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockResolver.verify(); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); // Reset result.reset(); - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + reset(response); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); // Test request again, make sure resolve is executed exactly once cache.doCached(request, response, resolver); @@ -361,252 +314,171 @@ public class HTTPCacheTestCase extends MockObjectTestCase { assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } + @Test public void testNonCacheableRequestHeader() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), null, Collections.singletonMap("Cache-Control", Collections.singletonList("no-store"))); + CacheRequest request = configureRequest(mock(CacheRequest.class), "GET", createRequestURI(), null, Collections.singletonMap("Cache-Control", Collections.singletonList("no-store"))); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); // TODO: How do we know that the response was NOT cached? - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } + @Test public void testNonCacheableRequestHeaderRepeated() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); String requestURI = createRequestURI(); - ByteArrayOutputStream result = new ByteArrayOutputStream(); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, requestURI); + CacheRequest request = configureRequest(mock(CacheRequest.class), requestURI); + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); - Mock mockResponse = mock(CacheResponse.class); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockResolver.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset result.reset(); + reset(response, resolver); - mockRequest.reset(); - mockRequest.stubs().method("getRequestURI").will(returnValue(URI.create(requestURI))); - mockRequest.stubs().method("getParameters").will(returnValue(Collections.emptyMap())); - mockRequest.stubs().method("getHeaders").will(returnValue(Collections.singletonMap("Cache-Control", Collections.singletonList("no-cache")))); // Force non-cached version of cached content - mockRequest.stubs().method("getMethod").will(returnValue("GET")); + // Reconfigure + request = configureRequest(request, "GET", requestURI, null, Collections.singletonMap("Cache-Control", Collections.singletonList("no-cache"))); + when(response.getOutputStream()).thenReturn(result); + doAnswer(new ResolveAnswer(value)).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); + value[3] = 'B'; // Make sure we have a different value second time - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - - mockResolver.reset(); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub2") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - value[3] = 'B'; - - // This cache should not be cached + // This request should not be cached cache.doCached(request, response, resolver); - // Verify that second reponse is ok + // Verify that second response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } + @Test public void testNonCacheableResponseHeader() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-cache")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Cache-Control", "no-cache"); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, Collections.singletonMap("Cache-Control", Collections.singletonList("no-cache")))) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader(eq("Cache-Control"), eq("no-cache")); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } + @Test public void testNonCacheableResponseHeaderRepeated() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-store")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Cache-Control", "no-store"); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, Collections.singletonMap("Cache-Control", Collections.singletonList("no-store")))) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockRequest.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader(eq("Cache-Control"), eq("no-store")); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-store")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - - mockResolver.reset(); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Cache-Control", "no-store"); - res.getOutputStream().write(value); - - return null; - } - }); result.reset(); + reset(response, resolver); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, Collections.singletonMap("Cache-Control", Collections.singletonList("no-store")))) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); + value[3] = 'B'; // Repeat invocation @@ -616,84 +488,63 @@ public class HTTPCacheTestCase extends MockObjectTestCase { assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader(eq("Cache-Control"), eq("no-store")); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } // Test non-cacheable response + @Test public void testNonCacheableResponse() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); ByteArrayOutputStream result = new ByteArrayOutputStream(); - Mock mockResponse = mock(CacheResponse.class); - mockResponse.expects(once()).method("setStatus").with(eq(500)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(500); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, value, null)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } // Test non-cacheable response + @Test public void testNonCacheableResponseRepeated() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); ByteArrayOutputStream result = new ByteArrayOutputStream(); - Mock mockResponse = mock(CacheResponse.class); - mockResponse.expects(once()).method("setStatus").with(eq(500)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(500); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, value, null)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); @@ -703,64 +554,53 @@ public class HTTPCacheTestCase extends MockObjectTestCase { assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockResolver.verify(); + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); - // Test request again, should do new resolve... + // Reset result.reset(); + reset(response, resolver); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, value, null)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); value[3] = 'B'; - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(500)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - - mockResolver.reset(); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(500); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.getOutputStream().write(value); - - return null; - } - }); - // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + // Verify new resolve + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } - // Test that request headers are forwarded to resolver... + @Test public void testRequestHeadersForwarded() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); final Map> headers = new LinkedHashMap>(); headers.put("Cache-Control", Arrays.asList("no-cache")); headers.put("X-Custom", Arrays.asList("FOO", "BAR")); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, "HEAD", createRequestURI(), null, headers); + CacheRequest request = configureRequest(mock(CacheRequest.class), "HEAD", createRequestURI(), null, headers); + CacheResponse response = mock(CacheResponse.class); - Mock mockResponse = mock(CacheResponse.class); - CacheResponse response = (CacheResponse) mockResponse.proxy(); - - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.getArguments()[0]; Map> reqHeaders = req.getHeaders(); assertEquals(headers, reqHeaders); @@ -768,48 +608,42 @@ public class HTTPCacheTestCase extends MockObjectTestCase { // Make sure that we preserve insertion order Set>> expected = headers.entrySet(); Iterator>> actual = reqHeaders.entrySet().iterator(); + for (Map.Entry> entry : expected) { assertEquals(entry, actual.next()); } return null; } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + }).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); + + // Verify that custom resolve was invoked + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } // Test that response headers are preserved + @Test public void testCacheablePreserveResponseHeaders() throws IOException, CacheException { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); - + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); + ByteArrayOutputStream result = new ByteArrayOutputStream(); - Mock mockResponse = mock(CacheResponse.class); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("Cache-Control"), eq("public")); - mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("X-Custom"), eq("FOO")).id("firstCustom"); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("addHeader").with(eq("X-Custom"), eq("BAR")).after("firstCustom"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); final byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.stubs().method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + CacheResponse res = (CacheResponse) invocation.getArguments()[1]; - res.setStatus(HTTPCache.STATUS_OK); + res.setStatus(HttpServletResponse.SC_OK); res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); res.setHeader("Cache-Control", "public"); res.addHeader("X-Custom", "FOO"); @@ -818,215 +652,184 @@ public class HTTPCacheTestCase extends MockObjectTestCase { return null; } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + }).when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response).setHeader(eq("Cache-Control"), eq("public")); + + InOrder ordered = inOrder(response); + // TODO: This test is to fragile, as it relies on internal knowledge of the code tested + // (it doesn't really matter if first invocation is setHeader or addHeader) + // See if we can create a custom VerificationMode for "either/or" + ordered.verify(response).setHeader(eq("X-Custom"), eq("FOO")); + ordered.verify(response).addHeader(eq("X-Custom"), eq("BAR")); + + verify(response, atLeastOnce()).getOutputStream(); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } // Test Vary + @Test public void testVaryMissingRequestHeader() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("X-Foo"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); - final byte[] value = "foobar".getBytes("UTF-8"); + byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + HashMap> headers = new HashMap>(); + headers.put(HTTPCache.HEADER_CONTENT_TYPE, Arrays.asList("x-foo/bar")); + headers.put("Vary", Arrays.asList("X-Foo")); + headers.put("X-Foo", Arrays.asList("foobar")); + headers.put("X-Other", Arrays.asList("don't care")); - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader(HTTPCache.HEADER_CONTENT_TYPE, "x-foo/bar"); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "X-Foo"); - res.setHeader("X-Foo", "foobar header"); - res.setHeader("X-Other", "don't care"); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockRequest.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("X-Foo"), ANYTHING); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - result.reset(); + reset(response, resolver); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Repeat invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + + verifyNoMoreInteractions(resolver); } + @Test public void testVaryMissingRequestHeaderRepeated() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); - String requestURI = createRequestURI(); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, requestURI); + String requestURI = createRequestURI(); + CacheRequest request = configureRequest(mock(CacheRequest.class), requestURI); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); - final byte[] value = "foobar".getBytes("UTF-8"); + byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader(HTTPCache.HEADER_CONTENT_TYPE, "x-foo/bar"); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "X-Foo"); - res.setHeader("X-Other", "don't care"); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + HashMap> headers = new HashMap>(); + headers.put(HTTPCache.HEADER_CONTENT_TYPE, Arrays.asList("x-foo/bar")); + headers.put("Vary", Arrays.asList("X-Foo")); + headers.put("X-Other", Arrays.asList("don't care")); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockRequest.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset - request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar"))); - - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - - mockResolver.reset(); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "X-Foo"); - res.setHeader("X-Other", "don't care"); - res.getOutputStream().write(value); - - return null; - } - }); result.reset(); + request = configureRequest(request, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar"))); + reset(response, resolver); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); value[3] = 'B'; // Repeat invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - } + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + + // Different X-Foo header, must re-validate + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); + } + + @Test public void testVarySameResourceIsCached() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar value"))); + CacheRequest request = configureRequest(mock(CacheRequest.class), "GET", createRequestURI(), null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar value"))); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); - final byte[] value = "foobar".getBytes("UTF-8"); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + byte[] value = "foobar".getBytes("UTF-8"); - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "X-Foo"); - res.setHeader("X-Other", "don't care"); - res.getOutputStream().write(value); + HashMap> headers = new HashMap>(); + headers.put(HTTPCache.HEADER_CONTENT_TYPE, Arrays.asList("x-foo/bar")); + headers.put("Vary", Arrays.asList("X-Foo")); + headers.put("X-Other", Arrays.asList("don't care")); - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); @@ -1036,106 +839,84 @@ public class HTTPCacheTestCase extends MockObjectTestCase { assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockRequest.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - + reset(response); result.reset(); + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + // Repeat invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + + verifyNoMoreInteractions(resolver); } + @Test public void testVaryDifferentResources() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); - String requestURI = createRequestURI(); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foo"))); + String requestURI = createRequestURI(); + CacheRequest request = configureRequest(mock(CacheRequest.class), requestURI); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); - final byte[] value = "foo".getBytes("UTF-8"); + byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + HashMap> headers = new HashMap>(); + headers.put(HTTPCache.HEADER_CONTENT_TYPE, Arrays.asList("x-foo/bar")); + headers.put("Vary", Arrays.asList("X-Foo")); + headers.put("X-Other", Arrays.asList("don't care")); - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "X-Foo"); - res.setHeader("X-Other", "don't care"); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockRequest.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset - request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("bar"))); - - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - - mockResolver.reset(); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "X-Foo"); - res.setHeader("Cache-Control", "no-store"); - res.getOutputStream().write(value); - - return null; - } - }); result.reset(); + request = configureRequest(request, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("bar"))); + reset(response, resolver); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + headers.put("Cache-Control", Arrays.asList("no-store")); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); + value[0] = 'b'; value[1] = 'a'; value[2] = 'r'; @@ -1143,46 +924,40 @@ public class HTTPCacheTestCase extends MockObjectTestCase { // Repeat invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "X-Foo"); + verify(response, atLeastOnce()).setHeader("Cache-Control", "no-store"); + + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } + @Test public void testVaryStarNonCached() throws Exception { - HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + HTTPCache cache = new HTTPCache(getTempRoot(), 60000, 1024 * 1024, 10, true); // Custom setup - Mock mockRequest = mock(CacheRequest.class); - CacheRequest request = configureRequest(mockRequest, createRequestURI()); + CacheRequest request = configureRequest(mock(CacheRequest.class), createRequestURI()); - Mock mockResponse = mock(CacheResponse.class); ByteArrayOutputStream result = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("*")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - CacheResponse response = (CacheResponse) mockResponse.proxy(); + CacheResponse response = mock(CacheResponse.class); + when(response.getOutputStream()).thenReturn(result); - final byte[] value = "foobar".getBytes("UTF-8"); + byte[] value = "foobar".getBytes("UTF-8"); - Mock mockResolver = mock(ResponseResolver.class); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + HashMap> headers = new HashMap>(); + headers.put(HTTPCache.HEADER_CONTENT_TYPE, Arrays.asList("x-foo/bar")); + headers.put("Vary", Arrays.asList("*")); - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "*"); - res.getOutputStream().write(value); - - return null; - } - }); - ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + ResponseResolver resolver = mock(ResponseResolver.class); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Do the invocation cache.doCached(request, response, resolver); @@ -1192,117 +967,176 @@ public class HTTPCacheTestCase extends MockObjectTestCase { assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); - mockRequest.verify(); - mockResponse.verify(); - mockRequest.verify(); + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "*"); + + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); // Reset - mockResponse.reset(); - mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); - mockResponse.stubs().method("setHeader"); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); - mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("*")); - mockResponse.stubs().method("addHeader"); - mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); - - mockResolver.reset(); - mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { - public Void invoke(Invocation invocation) throws Throwable { - CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); - CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); - - res.setStatus(HTTPCache.STATUS_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); - res.setHeader("Vary", "*"); - res.getOutputStream().write(value); - - return null; - } - }); result.reset(); + reset(response, resolver); + + // Reconfigure + when(response.getOutputStream()).thenReturn(result); + headers.put("Cache-Control", Arrays.asList("no-store")); + doAnswer(new ResolveAnswer(HttpServletResponse.SC_OK, value, headers)) + .when(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); + value[3] = 'B'; // Repeat invocation cache.doCached(request, response, resolver); - // Verify that reponse is ok + // Verify that response is ok assertEquals(value.length, result.size()); assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); assertTrue(Arrays.equals(value, result.toByteArray())); + + verify(response).setStatus(HttpServletResponse.SC_OK); + verify(response, atLeastOnce()).getOutputStream(); + verify(response, atLeastOnce()).setHeader(eq("Date"), anyString()); + verify(response, atLeastOnce()).setHeader("Vary", "*"); + + verify(resolver).resolve(any(CacheRequest.class), any(CacheResponse.class)); } - /* + @Ignore("TODO") + @Test public void testVaryVariations() { fail("TODO"); } - public void testVarationsWithSameContentType() { + @Ignore("TODO") + @Test + public void testVariationsWithSameContentType() { // I believe there is a bug if two variations has same content type... fail("TODO"); } + @Ignore("TODO") + @Test // Test failing request (IOException) public void testIOException() { fail("TODO"); } + @Ignore("TODO") + @Test public void testCacheException() { fail("TODO"); } + @Ignore("TODO") + @Test public void testRuntimeException() { fail("TODO"); } + @Ignore("TODO") + @Test // Test failing (negative) HTTP response (401, 404, 410, 500, etc) public void testNegativeCache() { fail("TODO"); } + @Ignore("TODO") + @Test public void testNegativeCacheExpires() { fail("TODO"); } + @Ignore("TODO") + @Test // Test If-None-Match/ETag support public void testIfNoneMatch() { fail("TODO"); } + @Ignore("TODO") + @Test // Test If-Modified-Since support public void testIfModifiedSince() { fail("TODO"); } + @Ignore("TODO") + @Test // Test that data really expires when TTL is over public void testTimeToLive() { fail("TODO"); } + @Ignore("TODO") + @Test public void testMaxAgeRequest() { fail("TODO"); } - // Test that for requests with authorization, responses are not shared between different authorized users, unless response is marked as Cache-Control: public + @Ignore("TODO") + @Test + // Test that for requests with authorization, responses are not shared between different authorized users, unless response is marked as Cache-Control: public public void testAuthorizedRequestPublic() { fail("TODO"); } + @Ignore("TODO") + @Test public void testAuthorizedRequestPrivate() { fail("TODO"); } + @Ignore("TODO") + @Test public void testPutPostDeleteInvalidatesCache() { fail("TODO"); } + @Ignore("TODO") + @Test // TODO: Move out to separate package/test, just keep it here for PoC public void testClientRequest() { fail("Not implemented"); } + @Ignore("TODO") + @Test // TODO: Move out to separate package/test, just keep it here for PoC public void testServletRequest() { fail("Not implemented"); } - */ + + private static class ResolveAnswer implements Answer { + private final int status; + private final byte[] value; + private final Map> headers; + + public ResolveAnswer(byte[] value) { + this(HttpServletResponse.SC_OK, value, null); + } + + public ResolveAnswer(final int status, byte[] value, Map> headers) { + this.status = status; + this.value = value; + this.headers = headers != null ? headers : Collections.>emptyMap(); + } + + public Void answer(InvocationOnMock invocation) throws Throwable { + CacheResponse res = (CacheResponse) invocation.getArguments()[1]; + + res.setStatus(status); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + + for (Map.Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + res.addHeader(header.getKey(), value); + } + } + + res.getOutputStream().write(value); + + return null; + } + } } diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageFilterTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageFilterTestCase.java new file mode 100644 index 00000000..ebb8dce8 --- /dev/null +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageFilterTestCase.java @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS 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 COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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. + */ + +package com.twelvemonkeys.servlet.image; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.servlet.OutputStreamAdapter; +import com.twelvemonkeys.util.StringTokenIterator; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * ImageFilterTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: ImageFilterTestCase.java,v 1.0 07.04.11 14.14 haraldk Exp$ + */ +public class ImageFilterTestCase { + + @Test + public void passThroughIfNotTrigger() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter() { + @Override + protected boolean trigger(ServletRequest pRequest) { + return false; + } + }; + filter.init(filterConfig); + + // Request/response setup + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + + // Execute + filter.doFilter(request, response, chain); + + // Verifications + verify(chain).doFilter(request, response); + verify(response, never()).getOutputStream(); + verify(response, never()).getWriter(); + } + + @Test + public void normalFilter() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter(); + filter.init(filterConfig); + + // Request/response setup + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + ServletOutputStream out = spy(new OutputStreamAdapter(stream)); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(out); + + FilterChain chain = mock(FilterChain.class); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + HttpServletResponse response = (HttpServletResponse) invocation.getArguments()[1]; + + response.setContentType("image/png"); + response.setContentLength(104417); + InputStream stream = getClass().getResourceAsStream("/com/twelvemonkeys/servlet/image/12monkeys-splash.png"); + assertNotNull("Missing test resource", stream); + FileUtil.copy(stream, response.getOutputStream()); + + return null; + } + }).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + filter.doFilter(request, response, chain); + + // Verifications + int length = stream.size(); + + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + verify(response).setContentType("image/png"); + verify(response, atMost(1)).setContentLength(length); // setContentLength not implemented, avoid future bugs + verify(out, atLeastOnce()).flush(); + + // Extra verification here, until we come up with something better + assertTrue( + String.format("Unlikely length for PNG (please run manual check): %s bytes, expected about 85000 bytes", length), + length >= 60000 && length <= 120000 + ); + } + + @Test + public void filterNoContent() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter(); + filter.init(filterConfig); + + // Request/response setup + ServletOutputStream out = mock(ServletOutputStream.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(out); + + FilterChain chain = mock(FilterChain.class); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + HttpServletResponse response = (HttpServletResponse) invocation.getArguments()[1]; + + response.setContentType("image/x-imaginary"); + + return null; + } + }).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + filter.doFilter(request, response, chain); + + // Verifications + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + verify(response).setContentType("image/x-imaginary"); + verify(out, atLeastOnce()).flush(); + } + + @Test + public void triggerWhenTriggerParamsNull() throws ServletException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter(); + + filter.init(filterConfig); + + // Execute/Verifications + assertTrue(filter.trigger(mock(HttpServletRequest.class))); + } + + @Test + public void triggerWithTriggerParams() throws ServletException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("triggerParams")); + when(filterConfig.getInitParameter("triggerParams")).thenReturn("foo"); + + DummyFilter filter = new DummyFilter(); + + filter.init(filterConfig); + + // Request/response setup + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter("foo")).thenReturn("doit"); + + + // Execute/Verifications + assertTrue(filter.trigger(request)); + } + + @Test + public void dontTriggerWithoutTriggerParams() throws ServletException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("triggerParams")); + when(filterConfig.getInitParameter("triggerParams")).thenReturn("foo"); + + DummyFilter filter = new DummyFilter(); + + filter.init(filterConfig); + + // Request/response setup + HttpServletRequest request = mock(HttpServletRequest.class); + + + // Execute/Verifications + assertFalse(filter.trigger(request)); + } + + @Test + public void testChaining() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig fooConfig = mock(FilterConfig.class); + when(fooConfig.getFilterName()).thenReturn("foo"); + when(fooConfig.getServletContext()).thenReturn(context); + when(fooConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("")); + + final AtomicReference imageRef = new AtomicReference(); + final AtomicReference responseRef = new AtomicReference(); + + DummyFilter fooFilter = new DummyFilter() { + @Override + protected RenderedImage doFilter(BufferedImage image, ServletRequest request, ImageServletResponse response) throws IOException { + // NOTE: Post-filtering, this method is run after barFilter.doFilter + assertEquals(imageRef.get(), image); + assertEquals(responseRef.get(), response); + + return image; + } + }; + fooFilter.init(fooConfig); + + FilterConfig barConfig = mock(FilterConfig.class); + when(barConfig.getFilterName()).thenReturn("bar"); + when(barConfig.getServletContext()).thenReturn(context); + when(barConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("")); + + final DummyFilter barFilter = new DummyFilter() { + @Override + protected RenderedImage doFilter(BufferedImage image, ServletRequest request, ImageServletResponse response) throws IOException { + // NOTE: Post-filtering, this method is run before fooFilter.doFilter + Graphics2D graphics = image.createGraphics(); + try { + graphics.drawRect(10, 10, 100, 100); + } + finally { + graphics.dispose(); + } + + // Store references for later, make sure this is first and only set. + assertTrue(imageRef.compareAndSet(null, image)); + assertTrue(responseRef.compareAndSet(null, response)); + + return image; + } + }; + barFilter.init(barConfig); + + // Request/response setup + ServletOutputStream out = mock(ServletOutputStream.class); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(out); + + FilterChain chain = mock(FilterChain.class); + final AtomicBoolean first = new AtomicBoolean(false); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + HttpServletRequest request = (HttpServletRequest) invocation.getArguments()[0]; + HttpServletResponse response = (HttpServletResponse) invocation.getArguments()[1]; + + // Fake chaining here.. + if (first.compareAndSet(false, true)) { + barFilter.doFilter(request, response, (FilterChain) invocation.getMock()); + return null; + } + + // Otherwise, fake servlet/file response + response.setContentType("image/gif"); + InputStream stream = getClass().getResourceAsStream("/com/twelvemonkeys/servlet/image/tux.gif"); + assertNotNull("Missing test resource", stream); + FileUtil.copy(stream, response.getOutputStream()); + + return null; + } + }).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + fooFilter.doFilter(request, response, chain); + + // Verifications + verify(chain, times(2)).doFilter(any(ServletRequest.class), any(ImageServletResponse.class)); + verify(response).setContentType("image/gif"); + verify(out, atLeastOnce()).flush(); + + // NOTE: + // We verify that the image is the same in both ImageFilter implementations, to make sure the image is only + // decoded once, then encoded once. + // But this test is not necessarily 100% reliable... + } + + @Test(expected = ServletException.class) + public void passThroughIfNotTriggerException() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter() { + @Override + protected boolean trigger(ServletRequest pRequest) { + return false; + } + }; + filter.init(filterConfig); + + // Request/response setup + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + doThrow(new ServletException("Something went terribly wrong. Take shelter.")).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + try { + filter.doFilter(request, response, chain); + } + finally { + // Verifications + verify(chain).doFilter(request, response); + verify(response, never()).getOutputStream(); + verify(response, never()).getWriter(); + } + } + + @Test(expected = ServletException.class) + public void normalFilterException() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter(); + filter.init(filterConfig); + + // Request/response setup + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + ServletOutputStream out = spy(new OutputStreamAdapter(stream)); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(out); + + FilterChain chain = mock(FilterChain.class); + doThrow(new ServletException("Something went terribly wrong. Take shelter.")).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + try { + filter.doFilter(request, response, chain); + } + finally { + // Verifications + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + } + + @Test + public void passThroughIfNotTriggerSendError() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter() { + @Override + protected boolean trigger(ServletRequest pRequest) { + return false; + } + }; + filter.init(filterConfig); + + // Request/response setup + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + HttpServletResponse response = (HttpServletResponse) invocation.getArguments()[1]; + response.sendError(HttpServletResponse.SC_FORBIDDEN, "I'm afraid I can't do that."); + + return null; + } + }).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + filter.doFilter(request, response, chain); + + // Verifications + verify(chain).doFilter(request, response); + verify(response, never()).getOutputStream(); + verify(response, never()).getWriter(); + } + + @Test + public void normalFilterSendError() throws ServletException, IOException { + // Filter init & setup + ServletContext context = mock(ServletContext.class); + + FilterConfig filterConfig = mock(FilterConfig.class); + when(filterConfig.getFilterName()).thenReturn("dummy"); + when(filterConfig.getServletContext()).thenReturn(context); + when(filterConfig.getInitParameterNames()).thenReturn(new StringTokenIterator("foo, bar")); + + DummyFilter filter = new DummyFilter(); + filter.init(filterConfig); + + // Request/response setup + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + ServletOutputStream out = spy(new OutputStreamAdapter(stream)); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(out); + + FilterChain chain = mock(FilterChain.class); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + HttpServletResponse response = (HttpServletResponse) invocation.getArguments()[1]; + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "I've just picked up a fault in the AE35 unit."); + + return null; + } + }).when(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // Execute + filter.doFilter(request, response, chain); + + // Verifications + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + verify(response, atMost(1)).setContentLength(stream.size()); // setContentLength not implemented, avoid future bugs + verify(out, atLeastOnce()).flush(); + } + + private static class DummyFilter extends ImageFilter { + @Override + protected RenderedImage doFilter(BufferedImage image, ServletRequest request, ImageServletResponse response) throws IOException { + return image; + } + } +} 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 fd5456c8..5210151d 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java @@ -1,10 +1,10 @@ package com.twelvemonkeys.servlet.image; +import com.twelvemonkeys.image.BufferedImageIcon; import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.servlet.OutputStreamAdapter; -import org.jmock.Mock; -import org.jmock.cglib.MockObjectTestCase; +import org.junit.Before; import org.junit.Test; import javax.imageio.ImageIO; @@ -12,11 +12,19 @@ import javax.servlet.ServletContext; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + /** * ImageServletResponseImplTestCase * @@ -24,12 +32,12 @@ import java.util.Arrays; * @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 $ */ -public class ImageServletResponseImplTestCase extends MockObjectTestCase { - private static final String CONTENT_TYPE_BMP = "image/bmp"; - private static final String CONTENT_TYPE_FOO = "foo/bar"; - private static final String CONTENT_TYPE_GIF = "image/gif"; +public class ImageServletResponseImplTestCase { + private static final String CONTENT_TYPE_BMP = "image/bmp"; + private static final String CONTENT_TYPE_FOO = "foo/bar"; + private static final String CONTENT_TYPE_GIF = "image/gif"; private static final String CONTENT_TYPE_JPEG = "image/jpeg"; - private static final String CONTENT_TYPE_PNG = "image/png"; + private static final String CONTENT_TYPE_PNG = "image/png"; private static final String CONTENT_TYPE_TEXT = "text/plain"; private static final String IMAGE_NAME_PNG = "12monkeys-splash.png"; @@ -38,31 +46,24 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { private static final Dimension IMAGE_DIMENSION_PNG = new Dimension(300, 410); private static final Dimension IMAGE_DIMENSION_GIF = new Dimension(250, 250); - private HttpServletRequest mRequest; - private ServletContext mContext; + private HttpServletRequest request; + private ServletContext context; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void init() throws Exception { + request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").will(returnValue(null)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - mRequest = (HttpServletRequest) mockRequest.proxy(); - - Mock mockContext = mock(ServletContext.class); - mockContext.stubs().method("getResource").with(eq("/" + IMAGE_NAME_PNG)).will(returnValue(getClass().getResource(IMAGE_NAME_PNG))); - mockContext.stubs().method("getResource").with(eq("/" + IMAGE_NAME_GIF)).will(returnValue(getClass().getResource(IMAGE_NAME_GIF))); - mockContext.stubs().method("log").withAnyArguments(); // Just suppress the logging - mockContext.stubs().method("getMimeType").with(eq("file.bmp")).will(returnValue(CONTENT_TYPE_BMP)); - mockContext.stubs().method("getMimeType").with(eq("file.foo")).will(returnValue(CONTENT_TYPE_FOO)); - mockContext.stubs().method("getMimeType").with(eq("file.gif")).will(returnValue(CONTENT_TYPE_GIF)); - mockContext.stubs().method("getMimeType").with(eq("file.jpeg")).will(returnValue(CONTENT_TYPE_JPEG)); - mockContext.stubs().method("getMimeType").with(eq("file.png")).will(returnValue(CONTENT_TYPE_PNG)); - mockContext.stubs().method("getMimeType").with(eq("file.txt")).will(returnValue(CONTENT_TYPE_TEXT)); - mContext = (ServletContext) mockContext.proxy(); + 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.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); } private void fakeResponse(HttpServletRequest pRequest, ImageServletResponseImpl pImageResponse) throws IOException { @@ -78,7 +79,7 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { } else { String ext = name.substring(name.lastIndexOf(".")); - pImageResponse.setContentType(mContext.getMimeType("file" + ext)); + pImageResponse.setContentType(context.getMimeType("file" + ext)); pImageResponse.setContentLength(234); try { ServletOutputStream out = pImageResponse.getOutputStream(); @@ -95,15 +96,15 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { } } + @Test public void testBasicResponse() throws IOException { - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); - fakeResponse(mRequest, imageResponse); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); // Make sure image is correctly loaded BufferedImage image = imageResponse.getImage(); @@ -121,20 +122,24 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).setContentType(CONTENT_TYPE_PNG); + verify(response).getOutputStream(); } // Test that wrapper works as a no-op, in case the image does not need to be decoded // This is not a very common use case, as filters should avoid wrapping the response // for performance reasons, but we still want that to work - public void testNoOpResponse() throws IOException { - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); - fakeResponse(mRequest, imageResponse); + @Test + public void testNoOpResponse() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); // TODO: Is there a way we can avoid calling flush? // Flush image to wrapped response @@ -144,18 +149,21 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { // Test that image data is untouched assertTrue("Data differs", Arrays.equals(FileUtil.read(getClass().getResourceAsStream(IMAGE_NAME_PNG)), out.toByteArray())); + + verify(response).setContentType(CONTENT_TYPE_PNG); + verify(response).getOutputStream(); } // Transcode original PNG to JPEG with no other changes - public void testTranscodeResponse() throws IOException { - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_JPEG)); + @Test + public void testTranscodeResponsePNGToJPEG() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); - fakeResponse(mRequest, imageResponse); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); // Force transcode to JPEG imageResponse.setOutputContentType("image/jpeg"); @@ -179,7 +187,7 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertEquals(IMAGE_DIMENSION_PNG.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_PNG.height, outImage.getHeight()); - BufferedImage image = flatten(ImageIO.read(mContext.getResource("/" + IMAGE_NAME_PNG)), Color.BLACK); + BufferedImage image = flatten(ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)), Color.BLACK); /* tempFile = File.createTempFile("imageservlet-test-", ".png"); @@ -191,25 +199,115 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { // JPEG compression trashes the image completely... assertSimilarImage(image, outImage, 144f); + + verify(response).setContentType(CONTENT_TYPE_JPEG); + verify(response).getOutputStream(); + } + + // WORKAROUND: Bug in GIFImageWriteParam, compression type is not set by default + // (even if there's only one possible compression mode/type combo; MODE_EXPLICIT/"LZW") + @Test + public void testTranscodeResponsePNGToGIFWithQuality() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + 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); + + // Force transcode to GIF + imageResponse.setOutputContentType("image/gif"); + + // Flush image to wrapped response + imageResponse.flush(); + + 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()); + + BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); + + // Should keep transparency, but is now binary + assertSimilarImageTransparent(image, outImage, 120f); + + 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(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); + + // Force transcode to GIF + imageResponse.setOutputContentType("image/gif"); + + // Flush image to wrapped response + imageResponse.flush(); + + 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()); + + BufferedImage image = ImageIO.read(context.getResource("/" + IMAGE_NAME_PNG)); + + // Should keep transparency, but is now binary + assertSimilarImageTransparent(image, outImage, 120f); + + verify(response).setContentType(CONTENT_TYPE_GIF); + verify(response).getOutputStream(); + } + + private static void showIt(final BufferedImage expected, final BufferedImage actual, final BufferedImage diff) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0)); + panel.add(new BlackLabel("expected", expected)); + panel.add(new BlackLabel("actual", actual)); + if (diff != null) { + panel.add(new BlackLabel("diff", diff)); + } + JScrollPane scroll = new JScrollPane(panel); + scroll.setBorder(BorderFactory.createEmptyBorder()); + JOptionPane.showMessageDialog(null, scroll); + } + }); + } + catch (InterruptedException ignore) { + } + catch (InvocationTargetException ignore) { + } } @Test public void testTranscodeResponseIndexedCM() throws IOException { // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").will(returnValue(null)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_GIF)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_GIF); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_JPEG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Force transcode to JPEG @@ -221,28 +319,12 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertTrue("Content has no data", out.size() > 0); // Test that image data is still readable - /* - File tempFile = File.createTempFile("imageservlet-test-", ".jpeg"); - FileOutputStream stream = new FileOutputStream(tempFile); - out.writeTo(stream); - stream.close(); - System.err.println("open " + tempFile); - */ - BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); assertNotNull(outImage); assertEquals(IMAGE_DIMENSION_GIF.width, outImage.getWidth()); assertEquals(IMAGE_DIMENSION_GIF.height, outImage.getHeight()); - BufferedImage image = flatten(ImageIO.read(mContext.getResource("/" + IMAGE_NAME_GIF)), Color.WHITE); - - /* - tempFile = File.createTempFile("imageservlet-test-", ".png"); - stream = new FileOutputStream(tempFile); - ImageIO.write(image, "PNG", stream); - stream.close(); - System.err.println("open " + tempFile); - */ + BufferedImage image = flatten(ImageIO.read(context.getResource("/" + IMAGE_NAME_GIF)), Color.WHITE); assertSimilarImage(image, outImage, 96f); } @@ -268,33 +350,56 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { * * @param pExpected the expected image * @param pActual the actual image - * @param pArtifactThreshold the maximum allowed difference between the expected and actual pixel value + * @param pArtifactThreshold the maximum allowed difference between the expected and actual pixel value */ private void assertSimilarImage(final BufferedImage pExpected, final BufferedImage pActual, final float pArtifactThreshold) { for (int y = 0; y < pExpected.getHeight(); y++) { for (int x = 0; x < pExpected.getWidth(); x++) { - int original = pExpected.getRGB(x, y); + int expected = pExpected.getRGB(x, y); int actual = pActual.getRGB(x, y); // Multiply in the alpha component - float alpha = ((original >> 24) & 0xff) / 255f; + float alpha = ((expected >> 24) & 0xff) / 255f; - assertEquals(alpha * ((original >> 16) & 0xff), (actual >> 16) & 0xff, pArtifactThreshold); - assertEquals(alpha * ((original >> 8) & 0xff), (actual >> 8) & 0xff, pArtifactThreshold); - assertEquals(alpha * ((original) & 0xff), actual & 0xff, pArtifactThreshold); + assertEquals(alpha * ((expected >> 16) & 0xff), (actual >> 16) & 0xff, pArtifactThreshold); + assertEquals(alpha * ((expected >> 8) & 0xff), (actual >> 8) & 0xff, pArtifactThreshold); + assertEquals(alpha * ((expected) & 0xff), actual & 0xff, pArtifactThreshold); } } } - public void testReplaceResponse() throws IOException { - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_BMP)); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + private void assertSimilarImageTransparent(final BufferedImage pExpected, final BufferedImage pActual, final float pArtifactThreshold) { + 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); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); - fakeResponse(mRequest, imageResponse); + 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); + + + 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); + } + } + } + + @Test + public void testReplaceResponse() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); // Make sure image is correctly loaded BufferedImage image = imageResponse.getImage(); @@ -318,6 +423,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); assertSimilarImage(image, outImage, 0); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_BMP); } // TODO: Test with AOI attributes (rename thes to source-region?) @@ -325,41 +433,38 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { // More? // Make sure we don't change semantics here... + + @Test public void testNotFoundInput() throws IOException { - // Need speical setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").will(returnValue(null)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/monkey-business.gif")); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - mRequest = (HttpServletRequest) mockRequest.proxy(); + // Need special setup + request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/monkey-business.gif"); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("sendError").with(eq(404), ANYTHING); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); - fakeResponse(mRequest, imageResponse); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); + fakeResponse(request, imageResponse); + + verify(response).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString()); } // NOTE: This means it's up to some Filter to decide wether we should filter the given request + + @Test public void testUnsupportedInput() throws IOException { assertFalse("Test is invalid, rewrite test", ImageIO.getImageReadersByFormatName("txt").hasNext()); - // Need speical setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").will(returnValue(null)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/foo.txt")); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - mRequest = (HttpServletRequest) mockRequest.proxy(); + // Need special setup + request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/foo.txt"); - Mock mockResponse = mock(HttpServletResponse.class); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); - fakeResponse(mRequest, imageResponse); + fakeResponse(request, imageResponse); try { // Force transcode imageResponse.setOutputContentType("image/png"); @@ -371,22 +476,23 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { } catch (IOException e) { String message = e.getMessage().toLowerCase(); - assertTrue("Wrong message: " + e.getMessage(), message.indexOf("transcode") >= 0); - assertTrue("Wrong message: " + e.getMessage(), message.indexOf("reader") >= 0); - assertTrue("Wrong message: " + e.getMessage(), message.indexOf("text") >= 0); - // Failure here suggests a different failurfe condition than the one we expected + assertTrue("Wrong message: " + e.getMessage(), message.contains("transcode")); + assertTrue("Wrong message: " + e.getMessage(), message.contains("reader")); + assertTrue("Wrong message: " + e.getMessage(), message.contains("text")); + + // Failure here suggests a different error condition than the one we expected } } + @Test public void testUnsupportedOutput() throws IOException { assertFalse("Test is invalid, rewrite test", ImageIO.getImageWritersByFormatName("foo").hasNext()); - Mock mockResponse = mock(HttpServletResponse.class); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); - fakeResponse(mRequest, imageResponse); + fakeResponse(request, imageResponse); try { // Force transcode to unsupported format imageResponse.setOutputContentType("application/xml+foo"); @@ -398,10 +504,11 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { } catch (IOException e) { String message = e.getMessage().toLowerCase(); - assertTrue("Wrong message: " + e.getMessage(), message.indexOf("transcode") >= 0); - assertTrue("Wrong message: " + e.getMessage(), message.indexOf("writer") >= 0); - assertTrue("Wrong message: " + e.getMessage(), message.indexOf("foo") >= 0); - // Failure here suggests a different failurfe condition than the one we expected + assertTrue("Wrong message: " + e.getMessage(), message.contains("transcode")); + assertTrue("Wrong message: " + e.getMessage(), message.contains("writer")); + assertTrue("Wrong message: " + e.getMessage(), message.contains("foo")); + + // Failure here suggests a different error condition than the one we expected } } @@ -409,28 +516,22 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { // For example: Read a PNG with transparency and store as B/W WBMP - // TODO: Create ImageFilter test case, that tests normal use, as well as chaining @Test public void testReadWithSourceRegion() throws IOException { Rectangle sourceRegion = new Rectangle(100, 100, 100, 100); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -449,27 +550,26 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test public void testReadWithNonSquareSourceRegion() throws IOException { Rectangle sourceRegion = new Rectangle(100, 100, 100, 80); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -488,6 +588,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -496,22 +599,17 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Rectangle sourceRegion = new Rectangle(-1, -1, 300, 300); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI_UNIFORM)).thenReturn(true); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -551,6 +649,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -559,22 +660,17 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Rectangle sourceRegion = new Rectangle(-1, -1, 410, 300); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI_UNIFORM)).thenReturn(true); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -637,6 +733,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -644,21 +743,16 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Dimension size = new Dimension(100, 120); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -687,28 +781,26 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test public void testReadWithNonUniformResize() throws IOException { Dimension size = new Dimension(150, 150); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).will(returnValue(false)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).thenReturn(false); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -727,6 +819,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -735,22 +830,17 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Dimension size = new Dimension(100, 120); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -779,6 +869,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -786,23 +879,18 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Rectangle sourceRegion = new Rectangle(100, 100, 200, 200); Dimension size = new Dimension(150, 150); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).will(returnValue(false)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).thenReturn(false); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -821,6 +909,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -829,23 +920,18 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Dimension size = new Dimension(100, 120); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI_UNIFORM)).thenReturn(true); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -882,6 +968,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -890,23 +979,18 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Dimension size = new Dimension(150, 120); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI_UNIFORM)).thenReturn(true); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -947,6 +1031,9 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } @Test @@ -955,23 +1042,18 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { Dimension size = new Dimension(100, 120); // Custom setup - Mock mockRequest = mock(HttpServletRequest.class); - mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); - mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); - mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); - mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME_PNG)); - mockRequest.stubs().method("getParameter").will(returnValue(null)); - HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI_UNIFORM)).thenReturn(true); + when(request.getAttribute(ImageServletResponse.ATTRIB_AOI)).thenReturn(sourceRegion); + when(request.getAttribute(ImageServletResponse.ATTRIB_SIZE)).thenReturn(size); + when(request.getContextPath()).thenReturn("/ape"); + when(request.getRequestURI()).thenReturn("/ape/" + IMAGE_NAME_PNG); - Mock mockResponse = mock(HttpServletResponse.class); - mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); - HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(new OutputStreamAdapter(out)); - ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, context); fakeResponse(request, imageResponse); // Make sure image is correctly loaded @@ -992,27 +1074,36 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertNotNull(outImage); assertEquals(image.getWidth(), outImage.getWidth()); assertEquals(image.getHeight(), outImage.getHeight()); + + verify(response).getOutputStream(); + verify(response).setContentType(CONTENT_TYPE_PNG); } // ----------------------------------------------------------------------------------------------------------------- // Absolute AOI // ----------------------------------------------------------------------------------------------------------------- + + @Test public void testGetAOIAbsolute() { assertEquals(new Rectangle(10, 10, 100, 100), ImageServletResponseImpl.getAOI(200, 200, 10, 10, 100, 100, false, false)); } + @Test public void testGetAOIAbsoluteOverflowX() { assertEquals(new Rectangle(10, 10, 90, 100), ImageServletResponseImpl.getAOI(100, 200, 10, 10, 100, 100, false, false)); } + @Test public void testGetAOIAbsoluteOverflowW() { assertEquals(new Rectangle(0, 10, 100, 100), ImageServletResponseImpl.getAOI(100, 200, 0, 10, 110, 100, false, false)); } + @Test public void testGetAOIAbsoluteOverflowY() { assertEquals(new Rectangle(10, 10, 100, 90), ImageServletResponseImpl.getAOI(200, 100, 10, 10, 100, 100, false, false)); } + @Test public void testGetAOIAbsoluteOverflowH() { assertEquals(new Rectangle(10, 0, 100, 100), ImageServletResponseImpl.getAOI(200, 100, 10, 0, 100, 110, false, false)); } @@ -1020,6 +1111,7 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { // ----------------------------------------------------------------------------------------------------------------- // Uniform AOI centered // ----------------------------------------------------------------------------------------------------------------- + @Test public void testGetAOIUniformCenteredS2SUp() { assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 333, 333, false, true)); @@ -1148,6 +1240,7 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { // ----------------------------------------------------------------------------------------------------------------- // Absolute AOI centered // ----------------------------------------------------------------------------------------------------------------- + @Test public void testGetAOICenteredS2SUp() { assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 333, 333, false, false)); @@ -1303,4 +1396,19 @@ public class ImageServletResponseImplTestCase extends MockObjectTestCase { assertEquals(new Rectangle(0, 0, 100, 200), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 200, false, false)); } + // TODO: Test percent + + // TODO: Test getSize()... + + private static class BlackLabel extends JLabel { + public BlackLabel(final String text, final BufferedImage outImage) { + super(text, new BufferedImageIcon(outImage), JLabel.CENTER); + setOpaque(true); + setBackground(Color.BLACK); + setForeground(Color.WHITE); + setVerticalAlignment(JLabel.CENTER); + setVerticalTextPosition(JLabel.BOTTOM); + setHorizontalTextPosition(JLabel.CENTER); + } + } }